pgc-runtime 2.0.0__py3-none-any.whl
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.
- pgc_runtime-2.0.0.dist-info/METADATA +181 -0
- pgc_runtime-2.0.0.dist-info/RECORD +28 -0
- pgc_runtime-2.0.0.dist-info/WHEEL +5 -0
- pgc_runtime-2.0.0.dist-info/entry_points.txt +2 -0
- pgc_runtime-2.0.0.dist-info/licenses/LICENSE +67 -0
- pgc_runtime-2.0.0.dist-info/licenses/NOTICE +11 -0
- pgc_runtime-2.0.0.dist-info/top_level.txt +1 -0
- runtime/__init__.py +0 -0
- runtime/api.py +71 -0
- runtime/boot.py +147 -0
- runtime/cli.py +387 -0
- runtime/conformance.py +249 -0
- runtime/ct_errors.py +33 -0
- runtime/ct_execute.py +84 -0
- runtime/ct_executor.py +312 -0
- runtime/dispatcher.py +448 -0
- runtime/evidence.py +245 -0
- runtime/examine/__init__.py +126 -0
- runtime/examine/classifier.py +350 -0
- runtime/examine/cli.py +35 -0
- runtime/examine/hint_engine.py +245 -0
- runtime/examine/locator.py +148 -0
- runtime/examine/parser.py +223 -0
- runtime/examine/reporter.py +110 -0
- runtime/loader.py +360 -0
- runtime/memory.py +125 -0
- runtime/scheduler.py +264 -0
- runtime/trace_viz.py +276 -0
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""
|
|
2
|
+
reporter.py — Diagnostic report formatting.
|
|
3
|
+
|
|
4
|
+
Governed by: Trace Examiner spec §8
|
|
5
|
+
|
|
6
|
+
Formats DiagnosticReport to terminal output.
|
|
7
|
+
Deterministic, stable format. Pure function.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
|
|
14
|
+
from runtime.examine.classifier import FailureClass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
_SE_SUCCESS_STATUSES = frozenset({"SUCCESS", "ACK", "completed"})
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@dataclass
|
|
21
|
+
class SideEffectOutcome:
|
|
22
|
+
"""Outcome of a side-effect capability node."""
|
|
23
|
+
|
|
24
|
+
cc_code: str
|
|
25
|
+
result_status: str
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def succeeded(self) -> bool:
|
|
29
|
+
return self.result_status in _SE_SUCCESS_STATUSES
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class DiagnosticReport:
|
|
34
|
+
"""
|
|
35
|
+
Complete diagnostic report for a trace examination.
|
|
36
|
+
|
|
37
|
+
Per spec §6 data structure.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
execution_id: str
|
|
41
|
+
workflow_code: str
|
|
42
|
+
has_structural_failure: bool
|
|
43
|
+
failure_class: FailureClass | None
|
|
44
|
+
failing_node: str | None
|
|
45
|
+
reason: str
|
|
46
|
+
artifact_path: str | None
|
|
47
|
+
fix_hint: str
|
|
48
|
+
side_effect_outcomes: list[SideEffectOutcome]
|
|
49
|
+
|
|
50
|
+
def format(self) -> str:
|
|
51
|
+
"""Format report for terminal output per spec §8."""
|
|
52
|
+
if not self.has_structural_failure:
|
|
53
|
+
return self._format_success()
|
|
54
|
+
return self._format_failure()
|
|
55
|
+
|
|
56
|
+
def _format_failure(self) -> str:
|
|
57
|
+
sep = "=" * 60
|
|
58
|
+
lines = [
|
|
59
|
+
sep,
|
|
60
|
+
"[trace-examiner] STRUCTURAL FAILURE DETECTED",
|
|
61
|
+
sep,
|
|
62
|
+
f"Trace ID: {self.execution_id}",
|
|
63
|
+
f"Workflow: {self.workflow_code}",
|
|
64
|
+
f"Failing Node: {self.failing_node or '(workflow-level)'}",
|
|
65
|
+
f"Failure Class: {self.failure_class.value if self.failure_class else 'UNKNOWN'}",
|
|
66
|
+
f"Reason: {self.reason}",
|
|
67
|
+
f"Artifact: {self.artifact_path or '(unresolved)'}",
|
|
68
|
+
f"Fix: {self.fix_hint}",
|
|
69
|
+
sep,
|
|
70
|
+
]
|
|
71
|
+
return "\n".join(lines)
|
|
72
|
+
|
|
73
|
+
def _format_success(self) -> str:
|
|
74
|
+
parts: list[str] = []
|
|
75
|
+
|
|
76
|
+
if self.failure_class == FailureClass.BUSINESS_VIOLATION:
|
|
77
|
+
sep = "-" * 60
|
|
78
|
+
parts.extend([
|
|
79
|
+
sep,
|
|
80
|
+
"[trace-examiner] BUSINESS VIOLATION (not escalated)",
|
|
81
|
+
sep,
|
|
82
|
+
f"Trace ID: {self.execution_id}",
|
|
83
|
+
f"Workflow: {self.workflow_code}",
|
|
84
|
+
f"Node: {self.failing_node or '(unknown)'}",
|
|
85
|
+
f"Reason: {self.reason}",
|
|
86
|
+
sep,
|
|
87
|
+
])
|
|
88
|
+
else:
|
|
89
|
+
parts.append(
|
|
90
|
+
f"[trace-examiner] {self.workflow_code} — "
|
|
91
|
+
f"{self.execution_id} — no structural failures"
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
if self.side_effect_outcomes:
|
|
95
|
+
parts.append(self._format_side_effect_outcomes())
|
|
96
|
+
|
|
97
|
+
return "\n".join(parts)
|
|
98
|
+
|
|
99
|
+
def _format_side_effect_outcomes(self) -> str:
|
|
100
|
+
"""Format side-effect outcomes as a business outcome summary."""
|
|
101
|
+
lines: list[str] = []
|
|
102
|
+
sep = "-" * 60
|
|
103
|
+
lines.append(sep)
|
|
104
|
+
lines.append("[trace-examiner] SIDE-EFFECT OUTCOMES")
|
|
105
|
+
lines.append(sep)
|
|
106
|
+
for outcome in self.side_effect_outcomes:
|
|
107
|
+
indicator = "OK" if outcome.succeeded else "FAILED"
|
|
108
|
+
lines.append(f" [{indicator}] {outcome.cc_code}: {outcome.result_status}")
|
|
109
|
+
lines.append(sep)
|
|
110
|
+
return "\n".join(lines)
|
runtime/loader.py
ADDED
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
"""
|
|
2
|
+
loader.py — Token-native snapshot loader for the runtime.
|
|
3
|
+
|
|
4
|
+
Reads the tokenized_snapshot package for a given domain/structure, verifies
|
|
5
|
+
the topology hash against the trust attestation, and returns a frozen
|
|
6
|
+
RuntimePackage dataclass.
|
|
7
|
+
|
|
8
|
+
The loader is the only file that touches the filesystem. Everything above
|
|
9
|
+
it (dispatcher, scheduler, memory, evidence) works only against the frozen
|
|
10
|
+
RuntimePackage.
|
|
11
|
+
|
|
12
|
+
Consumed from the assembled snapshot root (the assembler product; see
|
|
13
|
+
snapshot_assembler/doc/SNAPSHOT_ASSEMBLY_CONTRACT.md):
|
|
14
|
+
tokenized/<domain>/dispatch.json — routing (per-WF), pipeline, entry, bindings
|
|
15
|
+
tokenized/<domain>/handlers.json — ct, cs, rb_policy
|
|
16
|
+
tokenized/<domain>/metadata.json — projection_hash for trust verification
|
|
17
|
+
trust/<domain>/structure_attestation.json — tokenized_projection_hash
|
|
18
|
+
vocabulary/<domain>/forward.json — int_addr_hex → FQDN
|
|
19
|
+
vocabulary/<domain>/reverse.json — FQDN → int_addr_hex
|
|
20
|
+
|
|
21
|
+
Verification contract:
|
|
22
|
+
metadata.json[projection_hash] == structure_attestation.json[tokenized_projection_hash]
|
|
23
|
+
Mismatch → hard failure. No silent skip, no fallback.
|
|
24
|
+
|
|
25
|
+
Address conversion:
|
|
26
|
+
Vocabulary files use hex strings ("0x0035"). The runtime works with ints.
|
|
27
|
+
forward: {int → FQDN} (converted on load)
|
|
28
|
+
reverse: {FQDN → int} (converted on load)
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
from __future__ import annotations
|
|
32
|
+
|
|
33
|
+
import json
|
|
34
|
+
from dataclasses import dataclass
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
from typing import Any
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# ---------------------------------------------------------------------------
|
|
40
|
+
# Frozen dataclasses — the RuntimePackage and its sub-tables
|
|
41
|
+
# ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True)
|
|
44
|
+
class DispatchTable:
|
|
45
|
+
"""
|
|
46
|
+
Integer-keyed routing substrate from dispatch.json.
|
|
47
|
+
|
|
48
|
+
routing: {wf_addr: {cc_addr: {condition_addr: {"addr": next_cc_addr, "key": node_key}}}}
|
|
49
|
+
terminal: {wf_addr: {cc_addr: {condition_addr: {"exit": node_key, "type": "EXIT"}}}}
|
|
50
|
+
pipeline: {cc_addr: [step, ...]}
|
|
51
|
+
entry: {wf_addr: {"start": cc_addr, "start_key": node_key, "rb": rb_addr, "in": in_addr}}
|
|
52
|
+
bindings: {wf_addr: {node_key: {input_name: path_or_literal}}}
|
|
53
|
+
|
|
54
|
+
Each pipeline step is a named-field execution instruction record:
|
|
55
|
+
{
|
|
56
|
+
"addr": int, # CT or CS integer address
|
|
57
|
+
"op": str|None, # null for CT, operation name for CS
|
|
58
|
+
"inputs": dict|None, # resolved input bindings ($.inputs.X, $.results.step_id.X, literals)
|
|
59
|
+
"outputs": dict|None, # surface mapping for CT steps ({surface_name: "$.capability_result.field"})
|
|
60
|
+
"on_result": dict|None, # continuation semantics ({"SUCCESS": "continue", "VIOLATION": "exit"})
|
|
61
|
+
"step_id": str, # symbolic step name for $.results.<step_id>.<field> references
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
Routing values carry both the next CC address and the target node_key so that
|
|
65
|
+
the scheduler can disambiguate distinct WF usages of the same shared CC
|
|
66
|
+
(e.g. four denial audit nodes that all bind to CC_RECORD_DENIED_ACTION_V0).
|
|
67
|
+
|
|
68
|
+
Bindings are keyed by node_key (not CC address) for the same reason.
|
|
69
|
+
|
|
70
|
+
All semantics are compiler-materialized. The dispatcher is a blind executor.
|
|
71
|
+
The nested dicts are plain Python dicts (not frozen) — callers must not mutate.
|
|
72
|
+
"""
|
|
73
|
+
routing: dict[int, dict[int, dict[int, Any]]]
|
|
74
|
+
# Declared endings. An outcome in neither `routing` nor `terminal` is one the declarations do
|
|
75
|
+
# not answer for, and the traversal refuses rather than ending (`3a` EX-5, `3c` RT-9).
|
|
76
|
+
terminal: dict[int, dict[int, dict[int, Any]]]
|
|
77
|
+
# The input contract each IN gate admits against. An IN that declares none has nothing to
|
|
78
|
+
# determine, and absence is not permission (AI-6).
|
|
79
|
+
admission: dict[int, dict[str, Any]]
|
|
80
|
+
pipeline: dict[int, list[dict]]
|
|
81
|
+
entry: dict[int, dict[str, Any]] # entry may carry "actor" (FQDN) — Authority attribution
|
|
82
|
+
bindings: dict[int, dict[str, dict[str, Any]]]
|
|
83
|
+
emits: dict[int, dict[int, dict[str, str]]] # {wf_addr: {cc_addr: {outcome: EV_FQDN}}} — Observation
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass(frozen=True)
|
|
87
|
+
class HandlersTable:
|
|
88
|
+
"""
|
|
89
|
+
Implementation dispatch table from handlers.json.
|
|
90
|
+
|
|
91
|
+
ct: {ct_addr: {"ct_ir": {...}}}
|
|
92
|
+
cs: {cs_addr: {"handler_ref": {...}, "cs_metadata": {...}}}
|
|
93
|
+
rb_policy: {rb_addr: {cs_addr: policy_config}}
|
|
94
|
+
wf_storage: {wf_addr: storage_structure_artifact}
|
|
95
|
+
|
|
96
|
+
`wf_storage` is present only for an act that declares a reach: the compiler composed the
|
|
97
|
+
descriptions of the bindings it operates under and marked every entity `owned` or `consulted`.
|
|
98
|
+
An act that consults nothing is absent from it and resolves against its own binding exactly as
|
|
99
|
+
before.
|
|
100
|
+
|
|
101
|
+
All dict keys are ints.
|
|
102
|
+
"""
|
|
103
|
+
ct: dict[int, dict[str, Any]]
|
|
104
|
+
cs: dict[int, dict[str, Any]]
|
|
105
|
+
rb_policy: dict[int, dict[int, Any]]
|
|
106
|
+
wf_storage: dict[int, dict[str, Any]]
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
@dataclass(frozen=True)
|
|
110
|
+
class VocabIndex:
|
|
111
|
+
"""
|
|
112
|
+
Bidirectional FQDN ↔ integer address index from vocabulary_snapshot.
|
|
113
|
+
|
|
114
|
+
forward: {int_addr → FQDN}
|
|
115
|
+
reverse: {FQDN → int_addr}
|
|
116
|
+
"""
|
|
117
|
+
forward: dict[int, str]
|
|
118
|
+
reverse: dict[str, int]
|
|
119
|
+
|
|
120
|
+
def fqdn(self, addr: int) -> str:
|
|
121
|
+
"""Resolve integer address to FQDN. Returns hex string if not found."""
|
|
122
|
+
return self.forward.get(addr, f"0x{addr:04X}")
|
|
123
|
+
|
|
124
|
+
def addr(self, fqdn: str) -> int:
|
|
125
|
+
"""Resolve FQDN to integer address. Raises KeyError if not found."""
|
|
126
|
+
if fqdn not in self.reverse:
|
|
127
|
+
raise KeyError(f"FQDN not in vocab: {fqdn!r}")
|
|
128
|
+
return self.reverse[fqdn]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@dataclass(frozen=True)
|
|
132
|
+
class RuntimePackage:
|
|
133
|
+
"""
|
|
134
|
+
Complete frozen runtime substrate for one domain/structure.
|
|
135
|
+
|
|
136
|
+
Produced by load_domain(). All fields are read-only after construction.
|
|
137
|
+
The runtime (scheduler, dispatcher, evidence) operates exclusively against
|
|
138
|
+
this object — no further filesystem access.
|
|
139
|
+
"""
|
|
140
|
+
domain: str
|
|
141
|
+
dispatch: DispatchTable
|
|
142
|
+
handlers: HandlersTable
|
|
143
|
+
vocab: VocabIndex
|
|
144
|
+
# The snapshot this package was loaded from. Retained so a capability that observes the
|
|
145
|
+
# composition can be bound to the one it is executing from, rather than being told about a
|
|
146
|
+
# snapshot by its caller — a workflow must not be able to reason about a different composition
|
|
147
|
+
# than the one it is running inside.
|
|
148
|
+
snapshot_root: str = ""
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# ---------------------------------------------------------------------------
|
|
152
|
+
# Public API
|
|
153
|
+
# ---------------------------------------------------------------------------
|
|
154
|
+
|
|
155
|
+
def load_domain(
|
|
156
|
+
snapshot_root: str | Path,
|
|
157
|
+
domain: str,
|
|
158
|
+
*,
|
|
159
|
+
expected_tokenized_hash: str | None = None,
|
|
160
|
+
) -> RuntimePackage:
|
|
161
|
+
"""
|
|
162
|
+
Load and verify one domain's tokenized snapshot from the assembled snapshot root.
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
snapshot_root: Absolute path to the assembled snapshot root (contains
|
|
166
|
+
tokenized/ trust/ vocabulary/ per domain). See the assembly contract.
|
|
167
|
+
domain: Domain/structure identifier (e.g. "platform", "blockchain").
|
|
168
|
+
expected_tokenized_hash: If given (the manifest's tokenized projection_hash), the
|
|
169
|
+
on-disk metadata.projection_hash MUST equal it — anchors this domain to
|
|
170
|
+
the manifest root of trust. None skips the manifest anchor (bare load).
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
Frozen RuntimePackage ready for execution.
|
|
174
|
+
|
|
175
|
+
Raises:
|
|
176
|
+
FileNotFoundError: Required snapshot file is missing.
|
|
177
|
+
RuntimeError: Hash verification fails (tampered or stale snapshot).
|
|
178
|
+
ValueError: Malformed snapshot content.
|
|
179
|
+
"""
|
|
180
|
+
root = Path(snapshot_root)
|
|
181
|
+
|
|
182
|
+
tok_dir = root / "tokenized" / domain
|
|
183
|
+
trust_dir = root / "trust" / domain
|
|
184
|
+
vocab_dir = root / "vocabulary" / domain
|
|
185
|
+
|
|
186
|
+
# --- Load raw JSON files ---
|
|
187
|
+
dispatch_raw = _load_json(tok_dir / "dispatch.json")
|
|
188
|
+
handlers_raw = _load_json(tok_dir / "handlers.json")
|
|
189
|
+
metadata_raw = _load_json(tok_dir / "metadata.json")
|
|
190
|
+
attestation = _load_json(trust_dir / "structure_attestation.json")
|
|
191
|
+
forward_raw = _load_json(vocab_dir / "forward.json")
|
|
192
|
+
reverse_raw = _load_json(vocab_dir / "reverse.json")
|
|
193
|
+
|
|
194
|
+
# --- Trust verification (compiler self-consistency: tokenized == attestation) ---
|
|
195
|
+
_verify_hash(
|
|
196
|
+
actual = metadata_raw.get("projection_hash", ""),
|
|
197
|
+
expected = attestation.get("tokenized_projection_hash", ""),
|
|
198
|
+
domain = domain,
|
|
199
|
+
)
|
|
200
|
+
|
|
201
|
+
# --- Manifest anchor (root of trust): on-disk hash MUST equal the manifest's claim ---
|
|
202
|
+
if expected_tokenized_hash is not None:
|
|
203
|
+
actual = metadata_raw.get("projection_hash", "")
|
|
204
|
+
if actual != expected_tokenized_hash:
|
|
205
|
+
raise RuntimeError(
|
|
206
|
+
f"[{domain}] Manifest anchor failure: on-disk projection_hash={actual!r} "
|
|
207
|
+
f"!= manifest tokenized projection_hash={expected_tokenized_hash!r}"
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# --- Build DispatchTable ---
|
|
211
|
+
dispatch = _build_dispatch(dispatch_raw)
|
|
212
|
+
|
|
213
|
+
# --- Build HandlersTable ---
|
|
214
|
+
handlers = _build_handlers(handlers_raw)
|
|
215
|
+
|
|
216
|
+
# --- Build VocabIndex ---
|
|
217
|
+
vocab = _build_vocab(forward_raw, reverse_raw)
|
|
218
|
+
|
|
219
|
+
return RuntimePackage(
|
|
220
|
+
domain = domain,
|
|
221
|
+
dispatch = dispatch,
|
|
222
|
+
handlers = handlers,
|
|
223
|
+
vocab = vocab,
|
|
224
|
+
snapshot_root = str(root),
|
|
225
|
+
)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
# ---------------------------------------------------------------------------
|
|
229
|
+
# Internal helpers
|
|
230
|
+
# ---------------------------------------------------------------------------
|
|
231
|
+
|
|
232
|
+
def _load_json(path: Path) -> dict:
|
|
233
|
+
if not path.exists():
|
|
234
|
+
raise FileNotFoundError(f"Required snapshot file missing: {path}")
|
|
235
|
+
with path.open(encoding="utf-8") as f:
|
|
236
|
+
return json.load(f)
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def _verify_hash(actual: str, expected: str, domain: str) -> None:
|
|
240
|
+
if not actual or not expected:
|
|
241
|
+
raise RuntimeError(
|
|
242
|
+
f"[{domain}] Trust verification failed: "
|
|
243
|
+
f"projection_hash or tokenized_projection_hash is empty."
|
|
244
|
+
)
|
|
245
|
+
if actual != expected:
|
|
246
|
+
raise RuntimeError(
|
|
247
|
+
f"[{domain}] Snapshot integrity failure: "
|
|
248
|
+
f"metadata.projection_hash={actual!r} "
|
|
249
|
+
f"!= attestation.tokenized_projection_hash={expected!r}"
|
|
250
|
+
)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _build_dispatch(raw: dict) -> DispatchTable:
|
|
254
|
+
"""
|
|
255
|
+
Parse dispatch.json into integer-keyed DispatchTable.
|
|
256
|
+
|
|
257
|
+
JSON keys are strings (JSON spec). Addresses are int values.
|
|
258
|
+
Outer WF and CC keys are converted to int. Routing values are
|
|
259
|
+
{"addr": int, "key": str} dicts — preserved as-is. Bindings keys
|
|
260
|
+
are node_key strings — preserved as-is (not int-converted).
|
|
261
|
+
"""
|
|
262
|
+
routing: dict[int, dict[int, dict[int, Any]]] = {}
|
|
263
|
+
for wf_key, cc_map in raw.get("routing", {}).items():
|
|
264
|
+
wf_addr = int(wf_key)
|
|
265
|
+
routing[wf_addr] = {
|
|
266
|
+
int(cc_key): {int(cond): tgt for cond, tgt in cond_map.items()}
|
|
267
|
+
for cc_key, cond_map in cc_map.items()
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
terminal: dict[int, dict[int, dict[int, Any]]] = {}
|
|
271
|
+
for wf_key, cc_map in raw.get("terminal", {}).items():
|
|
272
|
+
wf_addr = int(wf_key)
|
|
273
|
+
terminal[wf_addr] = {
|
|
274
|
+
int(cc_key): {int(cond): tgt for cond, tgt in cond_map.items()}
|
|
275
|
+
for cc_key, cond_map in cc_map.items()
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
admission: dict[int, dict[str, Any]] = {
|
|
279
|
+
int(k): v for k, v in raw.get("admission", {}).items()
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
pipeline: dict[int, list[dict]] = {}
|
|
283
|
+
for cc_key, steps in raw.get("pipeline", {}).items():
|
|
284
|
+
cc_addr = int(cc_key)
|
|
285
|
+
pipeline[cc_addr] = steps # list of named-field step dicts
|
|
286
|
+
|
|
287
|
+
entry: dict[int, dict[str, Any]] = {}
|
|
288
|
+
for wf_key, e in raw.get("entry", {}).items():
|
|
289
|
+
wf_addr = int(wf_key)
|
|
290
|
+
# int-convert only numeric values (start, rb, in); leave start_key as str
|
|
291
|
+
entry[wf_addr] = {
|
|
292
|
+
k: (int(v) if isinstance(v, (int, float)) else v)
|
|
293
|
+
for k, v in e.items()
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
bindings: dict[int, dict[str, dict[str, Any]]] = {}
|
|
297
|
+
for wf_key, node_map in raw.get("bindings", {}).items():
|
|
298
|
+
wf_addr = int(wf_key)
|
|
299
|
+
# node_map keys are node_key strings (e.g. "CC_NORMALIZE_AGENT_REQUEST_V0")
|
|
300
|
+
bindings[wf_addr] = {node_key: inp for node_key, inp in node_map.items()}
|
|
301
|
+
|
|
302
|
+
# emits: {wf_addr: {cc_addr: {outcome_str: EV_FQDN}}} — domain events to emit on a CC outcome
|
|
303
|
+
emits: dict[int, dict[int, dict[str, str]]] = {}
|
|
304
|
+
for wf_key, cc_map in raw.get("emits", {}).items():
|
|
305
|
+
emits[int(wf_key)] = {
|
|
306
|
+
int(cc_key): {outcome: ev for outcome, ev in outcome_map.items()}
|
|
307
|
+
for cc_key, outcome_map in cc_map.items()
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
return DispatchTable(
|
|
311
|
+
routing = routing,
|
|
312
|
+
terminal = terminal,
|
|
313
|
+
admission = admission,
|
|
314
|
+
pipeline = pipeline,
|
|
315
|
+
entry = entry,
|
|
316
|
+
bindings = bindings,
|
|
317
|
+
emits = emits,
|
|
318
|
+
)
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def _build_handlers(raw: dict) -> HandlersTable:
|
|
322
|
+
"""
|
|
323
|
+
Parse handlers.json into integer-keyed HandlersTable.
|
|
324
|
+
"""
|
|
325
|
+
ct: dict[int, dict[str, Any]] = {
|
|
326
|
+
int(k): v for k, v in raw.get("ct", {}).items()
|
|
327
|
+
}
|
|
328
|
+
cs: dict[int, dict[str, Any]] = {
|
|
329
|
+
int(k): v for k, v in raw.get("cs", {}).items()
|
|
330
|
+
}
|
|
331
|
+
rb_policy: dict[int, dict[int, Any]] = {}
|
|
332
|
+
for rb_key, cs_map in raw.get("rb_policy", {}).items():
|
|
333
|
+
rb_addr = int(rb_key)
|
|
334
|
+
rb_policy[rb_addr] = {int(cs_key): policy for cs_key, policy in cs_map.items()}
|
|
335
|
+
|
|
336
|
+
wf_storage: dict[int, dict[str, Any]] = {
|
|
337
|
+
int(k): v for k, v in raw.get("wf_storage", {}).items()
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
return HandlersTable(ct=ct, cs=cs, rb_policy=rb_policy, wf_storage=wf_storage)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _build_vocab(forward_raw: dict, reverse_raw: dict) -> VocabIndex:
|
|
344
|
+
"""
|
|
345
|
+
Build bidirectional vocab index.
|
|
346
|
+
|
|
347
|
+
forward.json keys are hex strings like "0x0035" → FQDN string.
|
|
348
|
+
reverse.json keys are FQDN strings → hex string like "0x0035".
|
|
349
|
+
|
|
350
|
+
Both are converted to int ↔ FQDN mappings.
|
|
351
|
+
"""
|
|
352
|
+
forward: dict[int, str] = {}
|
|
353
|
+
for hex_key, fqdn in forward_raw.items():
|
|
354
|
+
forward[int(hex_key, 16)] = fqdn
|
|
355
|
+
|
|
356
|
+
reverse: dict[str, int] = {}
|
|
357
|
+
for fqdn, hex_val in reverse_raw.items():
|
|
358
|
+
reverse[fqdn] = int(hex_val, 16)
|
|
359
|
+
|
|
360
|
+
return VocabIndex(forward=forward, reverse=reverse)
|
runtime/memory.py
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
"""
|
|
2
|
+
memory.py — Execution context for a single workflow run.
|
|
3
|
+
|
|
4
|
+
Holds the initial payload and accumulates CC result surfaces as the
|
|
5
|
+
workflow progresses. Provides JSONPath resolution for CC input bindings.
|
|
6
|
+
|
|
7
|
+
Path grammar (compile-time allocated, runtime resolved):
|
|
8
|
+
$.payload.<field> — field from the workflow payload
|
|
9
|
+
$.inputs.<field> — alias for $.payload.<field> (CC-step scope)
|
|
10
|
+
$.results.<cc_addr>.<field> — field from a previous CC's result surface
|
|
11
|
+
<anything else> — literal value (returned as-is)
|
|
12
|
+
|
|
13
|
+
No dynamic path construction. All paths are emitted by the compiler.
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
from __future__ import annotations
|
|
17
|
+
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ExecutionContext:
|
|
22
|
+
"""
|
|
23
|
+
Mutable execution context for one workflow invocation.
|
|
24
|
+
|
|
25
|
+
- payload: initial input dict (never mutated after construction)
|
|
26
|
+
- results: accumulated CC result surfaces keyed by integer CC address
|
|
27
|
+
- actor: the Authority actor context this workflow executes under (FQDN), bound at WF entry
|
|
28
|
+
and carried through the run. Present for attribution/propagation only — no
|
|
29
|
+
authorization is enforced here (that belongs to the authority model).
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
__slots__ = ("_payload", "_results", "_actor")
|
|
33
|
+
|
|
34
|
+
def __init__(self, payload: dict[str, Any], actor: str | None = None) -> None:
|
|
35
|
+
self._payload: dict[str, Any] = dict(payload)
|
|
36
|
+
self._results: dict[int, dict[str, Any]] = {}
|
|
37
|
+
self._actor: str | None = actor
|
|
38
|
+
|
|
39
|
+
def record_result(self, cc_addr: int, surface: dict[str, Any]) -> None:
|
|
40
|
+
"""Store a CC's output surface for downstream bindings."""
|
|
41
|
+
self._results[cc_addr] = dict(surface)
|
|
42
|
+
|
|
43
|
+
def resolve(self, path: str) -> Any:
|
|
44
|
+
"""
|
|
45
|
+
Resolve a binding path to its value.
|
|
46
|
+
|
|
47
|
+
Supports:
|
|
48
|
+
$.payload.<field> — payload lookup (nested via dots)
|
|
49
|
+
$.inputs.<field> — same as $.payload.<field>
|
|
50
|
+
$.results.<cc_addr>.<field> — previous CC result lookup
|
|
51
|
+
<literal> — returned as-is
|
|
52
|
+
"""
|
|
53
|
+
if not isinstance(path, str):
|
|
54
|
+
if isinstance(path, dict):
|
|
55
|
+
return {k: self.resolve(v) for k, v in path.items()}
|
|
56
|
+
if isinstance(path, (list, tuple)):
|
|
57
|
+
return [self.resolve(v) for v in path]
|
|
58
|
+
return path # int, float, bool, None — returned as-is
|
|
59
|
+
|
|
60
|
+
if path.startswith("$.payload."):
|
|
61
|
+
return _nested_get(self._payload, path[len("$.payload."):])
|
|
62
|
+
|
|
63
|
+
if path.startswith("$.inputs."):
|
|
64
|
+
return _nested_get(self._payload, path[len("$.inputs."):])
|
|
65
|
+
|
|
66
|
+
if path.startswith("$.results."):
|
|
67
|
+
# Format: $.results.<cc_addr>.<field>[.<nested>...]
|
|
68
|
+
after = path[len("$.results."):]
|
|
69
|
+
dot = after.find(".")
|
|
70
|
+
if dot < 0:
|
|
71
|
+
return None # malformed path
|
|
72
|
+
try:
|
|
73
|
+
cc_addr = int(after[:dot])
|
|
74
|
+
except ValueError:
|
|
75
|
+
return None # non-integer CC addr in path
|
|
76
|
+
field_path = after[dot + 1:]
|
|
77
|
+
surface = self._results.get(cc_addr)
|
|
78
|
+
if surface is None:
|
|
79
|
+
return None
|
|
80
|
+
return _nested_get(surface, field_path)
|
|
81
|
+
|
|
82
|
+
# Literal value
|
|
83
|
+
return path
|
|
84
|
+
|
|
85
|
+
def resolve_inputs(self, bindings: dict[str, Any]) -> dict[str, Any]:
|
|
86
|
+
"""
|
|
87
|
+
Resolve a full bindings dict → concrete input values.
|
|
88
|
+
|
|
89
|
+
Each value is a path string or a literal. Returns a plain dict
|
|
90
|
+
with the same keys and resolved values.
|
|
91
|
+
"""
|
|
92
|
+
return {k: self.resolve(v) for k, v in bindings.items()}
|
|
93
|
+
|
|
94
|
+
@property
|
|
95
|
+
def payload(self) -> dict[str, Any]:
|
|
96
|
+
return self._payload
|
|
97
|
+
|
|
98
|
+
@property
|
|
99
|
+
def actor(self) -> str | None:
|
|
100
|
+
"""Actor context (FQDN) this workflow executes under. None if no actor is bound."""
|
|
101
|
+
return self._actor
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def results(self) -> dict[int, dict[str, Any]]:
|
|
105
|
+
return dict(self._results)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
# ---------------------------------------------------------------------------
|
|
109
|
+
# Internal helpers
|
|
110
|
+
# ---------------------------------------------------------------------------
|
|
111
|
+
|
|
112
|
+
def _nested_get(obj: Any, dotted_key: str) -> Any:
|
|
113
|
+
"""
|
|
114
|
+
Traverse a nested dict by a dot-separated key path.
|
|
115
|
+
|
|
116
|
+
Example: _nested_get({"a": {"b": 3}}, "a.b") → 3
|
|
117
|
+
Returns None for any missing key.
|
|
118
|
+
"""
|
|
119
|
+
parts = dotted_key.split(".")
|
|
120
|
+
current = obj
|
|
121
|
+
for part in parts:
|
|
122
|
+
if not isinstance(current, dict):
|
|
123
|
+
return None
|
|
124
|
+
current = current.get(part)
|
|
125
|
+
return current
|