urace 0.3.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.
- urace/__init__.py +3 -0
- urace/__main__.py +3 -0
- urace/_common.py +399 -0
- urace/a2a_server.py +382 -0
- urace/analytics.py +146 -0
- urace/api_interface.py +197 -0
- urace/authenticated_journal.py +76 -0
- urace/authority.py +189 -0
- urace/auto_goals.py +124 -0
- urace/bootstrap.py +128 -0
- urace/circuit_breaker.py +107 -0
- urace/cli.py +10906 -0
- urace/cognitive_resume.py +243 -0
- urace/command_surface.py +381 -0
- urace/compaction.py +1060 -0
- urace/content_store.py +72 -0
- urace/context_relations.py +45 -0
- urace/continuation.py +80 -0
- urace/credit_ledger.py +1190 -0
- urace/crypto_wallet.py +209 -0
- urace/data_transport.py +159 -0
- urace/decision_feedback.py +212 -0
- urace/decision_record.py +363 -0
- urace/derived_artifacts.py +47 -0
- urace/destination.py +161 -0
- urace/equity_auto_trade.py +1090 -0
- urace/evolution_guard.py +506 -0
- urace/exchange_cli.py +1048 -0
- urace/executor.py +1215 -0
- urace/executor_capabilities.py +95 -0
- urace/external_metrics.py +163 -0
- urace/feature_catalog.py +188 -0
- urace/feature_equity.py +235 -0
- urace/feature_policy.py +21 -0
- urace/federation.py +146 -0
- urace/freeform_routing.py +359 -0
- urace/implementation_sequence.py +71 -0
- urace/inputs.py +478 -0
- urace/inspection.py +883 -0
- urace/integration_bridge.py +370 -0
- urace/intent_trace.py +31 -0
- urace/launcher.py +117 -0
- urace/ledger_compaction.py +494 -0
- urace/ledger_crypto.py +89 -0
- urace/mcp_server.py +138 -0
- urace/milestone_feedback.py +65 -0
- urace/milestone_suggestions.py +40 -0
- urace/multi_executor.py +139 -0
- urace/outcome_verification.py +349 -0
- urace/patterns.py +1348 -0
- urace/peer_discovery.py +1315 -0
- urace/peer_exchange.py +575 -0
- urace/peer_improvement.py +512 -0
- urace/peer_selection.py +99 -0
- urace/peer_work_market.py +1131 -0
- urace/planning_feedback.py +92 -0
- urace/portfolio.py +2432 -0
- urace/portfolio_github.py +99 -0
- urace/preflight.py +52 -0
- urace/primitives.py +2339 -0
- urace/product_creation.py +94 -0
- urace/product_similarity.py +599 -0
- urace/product_type_detector.py +63 -0
- urace/protocol_catalog.py +14 -0
- urace/resource_usage.py +138 -0
- urace/rules.py +619 -0
- urace/runtime_cycle_cli.py +19 -0
- urace/runtime_features.py +525 -0
- urace/runtime_product.py +67 -0
- urace/schema_export.py +42 -0
- urace/scope_priority.py +103 -0
- urace/self_metrics.py +160 -0
- urace/signed_ledger.py +128 -0
- urace/spec_audit.py +376 -0
- urace/stun_client.py +72 -0
- urace/token_ledger.py +144 -0
- urace/tokenomics.py +72 -0
- urace/trial.py +68 -0
- urace/trial_cli.py +53 -0
- urace/tui.py +399 -0
- urace/uncertainty_followup.py +21 -0
- urace/updater.py +426 -0
- urace/user_controls.py +38 -0
- urace/validation.py +218 -0
- urace/value_discovery.py +382 -0
- urace/value_trajectory.py +296 -0
- urace/version.py +6 -0
- urace/work_policy.py +35 -0
- urace/workflow_state.py +267 -0
- urace-0.3.0.dist-info/METADATA +10 -0
- urace-0.3.0.dist-info/RECORD +94 -0
- urace-0.3.0.dist-info/WHEEL +5 -0
- urace-0.3.0.dist-info/entry_points.txt +2 -0
- urace-0.3.0.dist-info/top_level.txt +1 -0
urace/__init__.py
ADDED
urace/__main__.py
ADDED
urace/_common.py
ADDED
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import contextlib
|
|
4
|
+
import fcntl
|
|
5
|
+
import datetime
|
|
6
|
+
import hashlib
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import tempfile
|
|
10
|
+
import time
|
|
11
|
+
import uuid
|
|
12
|
+
from contextlib import contextmanager
|
|
13
|
+
from datetime import datetime as _datetime, timezone
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Generator
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
RUNTIME_ROOT = Path(__file__).resolve().parents[1]
|
|
19
|
+
SYSTEM_ROOT = RUNTIME_ROOT.parent
|
|
20
|
+
ROOT = SYSTEM_ROOT.parent
|
|
21
|
+
CONFIG_PATH = Path(__file__).with_name("config.json")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def now() -> str:
|
|
25
|
+
return _datetime.now(timezone.utc).isoformat()
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def identifier(prefix: str) -> str:
|
|
29
|
+
return f"{prefix}-{uuid.uuid4().hex[:12]}"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def atomic_write_text(path: Path, content: str) -> None:
|
|
33
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
34
|
+
with tempfile.NamedTemporaryFile("w", encoding="utf-8", dir=path.parent, delete=False) as handle:
|
|
35
|
+
handle.write(content)
|
|
36
|
+
handle.flush()
|
|
37
|
+
os.fsync(handle.fileno())
|
|
38
|
+
temporary = Path(handle.name)
|
|
39
|
+
temporary.replace(path)
|
|
40
|
+
directory = os.open(path.parent, os.O_RDONLY)
|
|
41
|
+
try:
|
|
42
|
+
os.fsync(directory)
|
|
43
|
+
finally:
|
|
44
|
+
os.close(directory)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def atomic_write_json(path: Path, value: dict[str, Any]) -> None:
|
|
48
|
+
atomic_write_text(path, json.dumps(value, indent=2, sort_keys=True) + "\n")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def durable_unlink(path: Path) -> None:
|
|
52
|
+
path.unlink(missing_ok=True)
|
|
53
|
+
if path.parent.exists():
|
|
54
|
+
directory = os.open(path.parent, os.O_RDONLY)
|
|
55
|
+
try:
|
|
56
|
+
os.fsync(directory)
|
|
57
|
+
finally:
|
|
58
|
+
os.close(directory)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def process_exists(pid: int) -> bool:
|
|
62
|
+
try:
|
|
63
|
+
os.kill(pid, 0)
|
|
64
|
+
return True
|
|
65
|
+
except ProcessLookupError:
|
|
66
|
+
return False
|
|
67
|
+
except PermissionError:
|
|
68
|
+
return True
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def _publish_state_lock(lock: Path, marker: str) -> int:
|
|
72
|
+
# Publish only fully written metadata. A process killed before publication
|
|
73
|
+
# leaves no lock that another process could mistake for a live owner.
|
|
74
|
+
descriptor, temporary_name = tempfile.mkstemp(prefix=".state-lock-", dir=lock.parent)
|
|
75
|
+
temporary = Path(temporary_name)
|
|
76
|
+
try:
|
|
77
|
+
payload = marker.encode()
|
|
78
|
+
if os.write(descriptor, payload) != len(payload):
|
|
79
|
+
raise OSError("Incomplete state lock metadata write")
|
|
80
|
+
os.fsync(descriptor)
|
|
81
|
+
os.link(temporary, lock)
|
|
82
|
+
return descriptor
|
|
83
|
+
except BaseException:
|
|
84
|
+
os.close(descriptor)
|
|
85
|
+
raise
|
|
86
|
+
finally:
|
|
87
|
+
temporary.unlink(missing_ok=True)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@contextmanager
|
|
91
|
+
def _published_state_lock(path: Path) -> Generator[None, None, None]:
|
|
92
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
93
|
+
lock = path.with_suffix(".lock")
|
|
94
|
+
descriptor = None
|
|
95
|
+
try:
|
|
96
|
+
for attempt in range(2):
|
|
97
|
+
try:
|
|
98
|
+
marker = json.dumps({"pid": os.getpid(), "started_at": _datetime.now(timezone.utc).isoformat()})
|
|
99
|
+
descriptor = _publish_state_lock(lock, marker)
|
|
100
|
+
break
|
|
101
|
+
except FileExistsError as error:
|
|
102
|
+
try:
|
|
103
|
+
raw = lock.read_text(encoding="utf-8").strip()
|
|
104
|
+
owner = json.loads(raw)["pid"]
|
|
105
|
+
except (OSError, ValueError, KeyError, TypeError, json.JSONDecodeError):
|
|
106
|
+
try:
|
|
107
|
+
owner = int(raw.strip())
|
|
108
|
+
except (NameError, ValueError):
|
|
109
|
+
owner = -1
|
|
110
|
+
if attempt == 0 and not process_exists(owner):
|
|
111
|
+
try:
|
|
112
|
+
_obs_path = path.parent / "stale-lock-observations.jsonl"
|
|
113
|
+
_obs_entry = json.dumps({"at": _datetime.now(timezone.utc).isoformat(), "stale_pid": owner, "lock": str(lock), "disposition": "removed"})
|
|
114
|
+
from .authenticated_journal import append_row
|
|
115
|
+
append_row(_obs_path,json.loads(_obs_entry))
|
|
116
|
+
except OSError:
|
|
117
|
+
pass
|
|
118
|
+
lock.unlink(missing_ok=True)
|
|
119
|
+
continue
|
|
120
|
+
raise RuntimeError(f"state is locked by live process {owner}: {lock}") from error
|
|
121
|
+
yield
|
|
122
|
+
finally:
|
|
123
|
+
if descriptor is not None:
|
|
124
|
+
os.close(descriptor)
|
|
125
|
+
lock.unlink(missing_ok=True)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
@contextmanager
|
|
129
|
+
def state_lock(path: Path) -> Generator[None, None, None]:
|
|
130
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
131
|
+
# Keep this inode: unlinking an advisory lock file would let contenders
|
|
132
|
+
# acquire different inodes and enter the protected section concurrently.
|
|
133
|
+
guard = path.with_suffix(".lock.guard")
|
|
134
|
+
descriptor = os.open(guard, os.O_CREAT | os.O_RDWR, 0o600)
|
|
135
|
+
try:
|
|
136
|
+
try:
|
|
137
|
+
fcntl.flock(descriptor, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
138
|
+
except BlockingIOError as error:
|
|
139
|
+
raise RuntimeError(f"state is locked by live process (kernel guard): {guard}") from error
|
|
140
|
+
with _published_state_lock(path):
|
|
141
|
+
yield
|
|
142
|
+
finally:
|
|
143
|
+
# Closing also releases the kernel guard on exceptions and process exit.
|
|
144
|
+
os.close(descriptor)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def load_config() -> dict[str, Any]:
|
|
148
|
+
cfg = json.loads(CONFIG_PATH.read_text(encoding="utf-8"))
|
|
149
|
+
env_product = os.environ.get("URACE_ACTIVE_PRODUCT")
|
|
150
|
+
if env_product:
|
|
151
|
+
cfg["active_product"] = env_product
|
|
152
|
+
active=cfg.get("active_product")
|
|
153
|
+
entry=next((e for e in cfg.get("product_registry",[]) if e.get("name")==active and e.get("kind")=="bootstrapped" and e.get("status")=="ACTIVE"),None)
|
|
154
|
+
if entry:
|
|
155
|
+
from .product_creation import validate_entry
|
|
156
|
+
validate_entry(entry,ROOT)
|
|
157
|
+
registry=cfg["product_registry"];cfg={**cfg,**copy_mapping(entry["configuration"]),"product_registry":registry,"active_product":active,"_product_overlay_name":active,"product_improvement_enabled":entry.get("improvement_enabled",True)}
|
|
158
|
+
return cfg
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def copy_mapping(value):
|
|
162
|
+
return json.loads(json.dumps(value))
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def save_config(config: dict[str, Any]) -> None:
|
|
166
|
+
overlay=config.get('_product_overlay_name')
|
|
167
|
+
if overlay:
|
|
168
|
+
base=json.loads(CONFIG_PATH.read_text());registry=config.get('product_registry',base.get('product_registry',[]))
|
|
169
|
+
if config.get('active_product')==overlay:
|
|
170
|
+
entry=next((e for e in registry if e.get('name')==overlay and e.get('kind')=='bootstrapped'),None)
|
|
171
|
+
if entry is None:raise ValueError('Active isolated product disappeared')
|
|
172
|
+
entry['configuration']={k:v for k,v in config.items() if k not in ('product_registry','active_product','_product_overlay_name')}
|
|
173
|
+
from .product_creation import validate_entry
|
|
174
|
+
validate_entry(entry,ROOT)
|
|
175
|
+
else:base['active_product']=config.get('active_product')
|
|
176
|
+
base['product_registry']=registry;atomic_write_json(CONFIG_PATH,base);return
|
|
177
|
+
atomic_write_json(CONFIG_PATH,config)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def state_path(config: dict[str, Any]) -> Path:
|
|
181
|
+
active = config.get("active_product")
|
|
182
|
+
if active:
|
|
183
|
+
registry = config.get("product_registry", [])
|
|
184
|
+
entry = next(
|
|
185
|
+
(e for e in registry if e.get("name") == active and e.get("status", "ACTIVE") == "ACTIVE"),
|
|
186
|
+
None,
|
|
187
|
+
)
|
|
188
|
+
if entry:
|
|
189
|
+
path = Path(entry["state_path"])
|
|
190
|
+
return path if path.is_absolute() else RUNTIME_ROOT / path
|
|
191
|
+
path = Path(config["state_path"])
|
|
192
|
+
return path if path.is_absolute() else RUNTIME_ROOT / path
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def load_state(config: dict[str, Any]) -> dict[str, Any]:
|
|
196
|
+
path = state_path(config)
|
|
197
|
+
if not path.exists():
|
|
198
|
+
raise RuntimeError("URACE is not initialized; run `urace init`")
|
|
199
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def load_state_for_read(config: dict[str, Any]) -> dict[str, Any] | None:
|
|
203
|
+
import sys
|
|
204
|
+
try:
|
|
205
|
+
return load_state(config)
|
|
206
|
+
except (RuntimeError, OSError, json.JSONDecodeError) as error:
|
|
207
|
+
print(f"urace: {error}", file=sys.stderr)
|
|
208
|
+
return None
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _check_state_invariants(state: dict) -> None:
|
|
212
|
+
"""Raise ValueError if state violates core invariants.
|
|
213
|
+
|
|
214
|
+
Only enforced on bootstrapped states (state_version >= 1) to avoid
|
|
215
|
+
rejecting minimal test stubs or pre-init state documents.
|
|
216
|
+
"""
|
|
217
|
+
# Invariant: schema_version must not decrease
|
|
218
|
+
if state.get('schema_version', 1) < 1:
|
|
219
|
+
raise ValueError(f"Invariant violated: schema_version={state.get('schema_version')}")
|
|
220
|
+
# Only enforce structural invariants on post-bootstrap state documents
|
|
221
|
+
if int(state.get('state_version', 0)) < 1:
|
|
222
|
+
return
|
|
223
|
+
# Invariant: self_evolution_policy must be well-formed if present
|
|
224
|
+
sep = state.get('self_evolution_policy')
|
|
225
|
+
if sep is not None and (not isinstance(sep, dict) or 'mode' not in sep):
|
|
226
|
+
raise ValueError("Invariant violated: self_evolution_policy malformed")
|
|
227
|
+
# Invariant: authority must be non-empty if present (prevent accidental erasure)
|
|
228
|
+
auth = state.get('authority')
|
|
229
|
+
if auth is not None and not auth:
|
|
230
|
+
raise ValueError("Invariant violated: authority was present but is now empty")
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def save_state(config: dict[str, Any], state: dict[str, Any]) -> None:
|
|
234
|
+
path = state_path(config)
|
|
235
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
236
|
+
from .resource_usage import archive_calls
|
|
237
|
+
archive_calls(config, state)
|
|
238
|
+
_check_state_invariants(state)
|
|
239
|
+
state["state_version"] += 1
|
|
240
|
+
state["updated_at"] = now()
|
|
241
|
+
atomic_write_json(path, state)
|
|
242
|
+
from .resource_usage import write_projection
|
|
243
|
+
try:write_projection(config,state)
|
|
244
|
+
except OSError:
|
|
245
|
+
# State is already durable. Reporting can use its existing full-state fallback.
|
|
246
|
+
import sys
|
|
247
|
+
print('urace: receipt read model unavailable; full-state fallback retained',file=sys.stderr)
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def transaction_path(config: dict[str, Any]) -> Path:
|
|
251
|
+
return state_path(config).parent / "transaction.json"
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def input_directory(config: dict[str, Any]) -> Path:
|
|
255
|
+
return state_path(config).parent / "inbox"
|
|
256
|
+
|
|
257
|
+
|
|
258
|
+
# ── §62h Technique Applicability Gate ────────────────────────────────────────
|
|
259
|
+
|
|
260
|
+
_TECHNIQUE_NAMES = {"T1", "T2", "T3", "T4", "T5", "T6"}
|
|
261
|
+
_SD_ALWAYS_TRUE = {"T1", "T2", "T3", "T4", "T6"}
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def technique_applicability_gate(
|
|
265
|
+
technique: str,
|
|
266
|
+
state: dict,
|
|
267
|
+
config: dict,
|
|
268
|
+
*,
|
|
269
|
+
applied_this_cycle: set | None = None,
|
|
270
|
+
) -> dict:
|
|
271
|
+
"""Evaluate TechniqueApplicabilityGate for one technique.
|
|
272
|
+
|
|
273
|
+
Returns TechniqueApplicationRecord (verdict, reason, sd_result, applied_at).
|
|
274
|
+
Caller must append to state['technique_application_log'] if desired.
|
|
275
|
+
"""
|
|
276
|
+
from datetime import datetime, timezone
|
|
277
|
+
_at = datetime.now(timezone.utc).isoformat()
|
|
278
|
+
applied_this_cycle = applied_this_cycle or set()
|
|
279
|
+
|
|
280
|
+
# Guard 1: dormant with no pending obligation
|
|
281
|
+
if state.get("lifecycle_state") == "DORMANT":
|
|
282
|
+
pending_log = state.get("technique_application_log", [])
|
|
283
|
+
has_obligation = any(
|
|
284
|
+
r.get("technique") == technique and r.get("verdict") == "DEFER"
|
|
285
|
+
for r in pending_log[-20:]
|
|
286
|
+
)
|
|
287
|
+
if not has_obligation:
|
|
288
|
+
return {"id": identifier("ta"), "technique": technique, "verdict": "SKIP",
|
|
289
|
+
"reason": "dormant_no_obligation", "applied_at": _at}
|
|
290
|
+
|
|
291
|
+
# Guard 2: idempotent — already applied this cycle
|
|
292
|
+
if technique in applied_this_cycle:
|
|
293
|
+
return {"id": identifier("ta"), "technique": technique, "verdict": "SKIP",
|
|
294
|
+
"reason": "already_applied_this_cycle", "applied_at": _at}
|
|
295
|
+
|
|
296
|
+
# Guard 3: self-determinable → APPLY
|
|
297
|
+
sd_result = technique in _SD_ALWAYS_TRUE or technique not in {"T5"}
|
|
298
|
+
if sd_result:
|
|
299
|
+
return {"id": identifier("ta"), "technique": technique, "verdict": "APPLY",
|
|
300
|
+
"reason": "self_determinable", "applied_at": _at, "sd_result": True}
|
|
301
|
+
|
|
302
|
+
# Guard 4: SD=False, check credits
|
|
303
|
+
executor_state = state.get("executor_state", {})
|
|
304
|
+
daily_calls = executor_state.get("calls_today", 0)
|
|
305
|
+
daily_limit = config.get("executor", {}).get("max_executor_calls_per_30d", 20)
|
|
306
|
+
credits_remaining = None if daily_limit is None else max(0, daily_limit - daily_calls)
|
|
307
|
+
technique_credit_cost = 1 # one executor call to determine semantic boundary
|
|
308
|
+
if credits_remaining is None or credits_remaining > technique_credit_cost:
|
|
309
|
+
return {"id": identifier("ta"), "technique": technique, "verdict": "DELEGATE_EXECUTOR",
|
|
310
|
+
"reason": "sd_false_credits_available", "applied_at": _at,
|
|
311
|
+
"sd_result": False, "executor_delegated": False, "credits_consumed": 0}
|
|
312
|
+
|
|
313
|
+
# Guard 5: defer
|
|
314
|
+
return {"id": identifier("ta"), "technique": technique, "verdict": "DEFER",
|
|
315
|
+
"reason": "sd_false_insufficient_credits", "applied_at": _at, "sd_result": False}
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
# ── §62i AP11/AP12/AP18 helper functions ─────────────────────────────────────
|
|
319
|
+
|
|
320
|
+
def bridge_record(
|
|
321
|
+
bridge_name: str,
|
|
322
|
+
source_schema: str,
|
|
323
|
+
target_schema: str,
|
|
324
|
+
*,
|
|
325
|
+
loss_declared: bool = False,
|
|
326
|
+
dropped_fields: list | None = None,
|
|
327
|
+
) -> dict:
|
|
328
|
+
"""AP11 — produce a BridgeRecord for a bridge translation event."""
|
|
329
|
+
from datetime import datetime, timezone
|
|
330
|
+
r: dict = {
|
|
331
|
+
"id": identifier("br"),
|
|
332
|
+
"bridge_name": bridge_name,
|
|
333
|
+
"source_schema": source_schema,
|
|
334
|
+
"target_schema": target_schema,
|
|
335
|
+
"translated_at": datetime.now(timezone.utc).isoformat(),
|
|
336
|
+
}
|
|
337
|
+
if loss_declared:
|
|
338
|
+
r["loss_declared"] = True
|
|
339
|
+
if dropped_fields:
|
|
340
|
+
r["dropped_fields"] = dropped_fields
|
|
341
|
+
return r
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def orchestrator_record(
|
|
345
|
+
orchestrator_name: str,
|
|
346
|
+
started_at: str,
|
|
347
|
+
phases_completed: list,
|
|
348
|
+
outcome: str,
|
|
349
|
+
*,
|
|
350
|
+
completed_at: str | None = None,
|
|
351
|
+
failure_phase: str | None = None,
|
|
352
|
+
postconditions_met: bool | None = None,
|
|
353
|
+
) -> dict:
|
|
354
|
+
"""AP12 — produce an OrchestratorRecord for a completed orchestration."""
|
|
355
|
+
r: dict = {
|
|
356
|
+
"id": identifier("orch"),
|
|
357
|
+
"orchestrator_name": orchestrator_name,
|
|
358
|
+
"started_at": started_at,
|
|
359
|
+
"phases_completed": phases_completed,
|
|
360
|
+
"outcome": outcome,
|
|
361
|
+
}
|
|
362
|
+
if completed_at:
|
|
363
|
+
r["completed_at"] = completed_at
|
|
364
|
+
if failure_phase:
|
|
365
|
+
r["failure_phase"] = failure_phase
|
|
366
|
+
if postconditions_met is not None:
|
|
367
|
+
r["postconditions_met"] = postconditions_met
|
|
368
|
+
return r
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def scheduled_task_due(task: dict, now_ts: float) -> bool:
|
|
372
|
+
"""Return True if a ScheduledTask is due for execution."""
|
|
373
|
+
from datetime import datetime, timezone
|
|
374
|
+
schedule = task.get("schedule", {})
|
|
375
|
+
kind = schedule.get("kind")
|
|
376
|
+
next_run = schedule.get("next_run_at")
|
|
377
|
+
if not next_run:
|
|
378
|
+
return kind == "IMMEDIATE"
|
|
379
|
+
try:
|
|
380
|
+
due_ts = datetime.fromisoformat(next_run).timestamp()
|
|
381
|
+
return now_ts >= due_ts
|
|
382
|
+
except (ValueError, TypeError):
|
|
383
|
+
return False
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def nullable_limit(settings: dict, key: str, default: int) -> int | None:
|
|
387
|
+
"""An explicit null is unlimited; absence keeps the compatibility default."""
|
|
388
|
+
value=settings.get(key,default)
|
|
389
|
+
return None if value is None else int(value)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def integration_context() -> dict[str, str]:
|
|
393
|
+
"""Correlation only: environment metadata never grants authority or budget."""
|
|
394
|
+
keys={'job_id':'URACE_BRIDGE_JOB','root_request_id':'URACE_BRIDGE_ROOT_REQUEST','project_id':'URACE_BRIDGE_PROJECT','executor_id':'URACE_BRIDGE_EXECUTOR'}
|
|
395
|
+
result={}
|
|
396
|
+
for key,env in keys.items():
|
|
397
|
+
value=os.environ.get(env)
|
|
398
|
+
if value and len(value)<=512 and all(ord(c)>=32 for c in value):result[key]=value
|
|
399
|
+
return result if 'job_id' in result else {}
|