cortex-runtime 0.3.0__tar.gz → 0.4.0__tar.gz

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.
Files changed (101) hide show
  1. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/PKG-INFO +1 -1
  2. cortex_runtime-0.4.0/cortex/schemas/v1/configuration.schema.json +203 -0
  3. cortex_runtime-0.4.0/cortex/tools/kernel/replica/__init__.py +56 -0
  4. cortex_runtime-0.4.0/cortex/tools/kernel/replica/identity.py +70 -0
  5. cortex_runtime-0.4.0/cortex/tools/kernel/replica/lease.py +227 -0
  6. cortex_runtime-0.4.0/cortex/tools/kernel/replica/ledger.py +292 -0
  7. cortex_runtime-0.4.0/cortex/tools/kernel/replica/lifecycle.py +75 -0
  8. cortex_runtime-0.4.0/cortex/tools/kernel/replica/router.py +324 -0
  9. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/pyproject.toml +1 -1
  10. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/.gitignore +0 -0
  11. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/LICENSE +0 -0
  12. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/NOTICE +0 -0
  13. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/README.md +0 -0
  14. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/__init__.py +0 -0
  15. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/__main__.py +0 -0
  16. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/_research/__init__.py +0 -0
  17. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/_research/architecture_gate.py +0 -0
  18. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/_research/crash_semantics.py +0 -0
  19. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/_research/recovery.py +0 -0
  20. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/_research/timeout_cancellation.py +0 -0
  21. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/_telemetry/__init__.py +0 -0
  22. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/_telemetry/benchmark.py +0 -0
  23. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/_telemetry/collector.py +0 -0
  24. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/_telemetry/models.py +0 -0
  25. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/cbe/__init__.py +0 -0
  26. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/cbe/decoder.py +0 -0
  27. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/cbe/encoder.py +0 -0
  28. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/cbe/errors.py +0 -0
  29. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/cbe/normalization.py +0 -0
  30. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/cbe/streaming.py +0 -0
  31. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/cbe/types.py +0 -0
  32. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/cbe/vectors.py +0 -0
  33. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/client.py +0 -0
  34. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/compat.py +0 -0
  35. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/exceptions.py +0 -0
  36. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/plugin.py +0 -0
  37. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/py.typed +0 -0
  38. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/schema/__init__.py +0 -0
  39. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/schema/events.py +0 -0
  40. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/__init__.py +0 -0
  41. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/cli/__init__.py +0 -0
  42. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/cli/main.py +0 -0
  43. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/cli/runner.py +0 -0
  44. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/cli/scaffolder.py +0 -0
  45. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/gen_test_bin.py +0 -0
  46. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/kernel/__init__.py +0 -0
  47. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/kernel/actors/__init__.py +0 -0
  48. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/kernel/actors/executor.py +0 -0
  49. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/kernel/actors/planner.py +0 -0
  50. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/kernel/context.py +0 -0
  51. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/kernel/drivers/__init__.py +0 -0
  52. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/kernel/drivers/mock_robot.py +0 -0
  53. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/kernel/drivers/rtl_verilator.py +0 -0
  54. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/kernel/graph/__init__.py +0 -0
  55. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/kernel/graph/analyzer.py +0 -0
  56. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/kernel/graph/execution_graph.py +0 -0
  57. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/kernel/mailbox.py +0 -0
  58. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/kernel/plugin/__init__.py +0 -0
  59. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/kernel/plugin/loader.py +0 -0
  60. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/kernel/plugin/manifest.py +0 -0
  61. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/kernel/registry.py +0 -0
  62. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/kernel/schema/__init__.py +0 -0
  63. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/kernel/schema/contract.py +0 -0
  64. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/kernel/schema/event.py +0 -0
  65. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/kernel/schema/message.py +0 -0
  66. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/kernel/schema/workflow.py +0 -0
  67. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/kernel/services/__init__.py +0 -0
  68. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/kernel/services/event_store.py +0 -0
  69. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/kernel/services/execution_intelligence.py +0 -0
  70. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/kernel/services/graph_builder.py +0 -0
  71. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/kernel/services/replay.py +0 -0
  72. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/kernel/services/verification.py +0 -0
  73. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/kernel/transport.py +0 -0
  74. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/run_phase2_verification.sh +0 -0
  75. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/verification/__init__.py +0 -0
  76. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/verification/adapters/__init__.py +0 -0
  77. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/verification/adapters/base.py +0 -0
  78. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/verification/adapters/coq.py +0 -0
  79. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/verification/adapters/rtl.py +0 -0
  80. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/verification/adapters/rust.py +0 -0
  81. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/verification/archive.py +0 -0
  82. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/verification/bus.py +0 -0
  83. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/verification/contract.py +0 -0
  84. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/verification/engine.py +0 -0
  85. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/verification/generator/__init__.py +0 -0
  86. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/verification/generator/composer.py +0 -0
  87. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/verification/generator/program.py +0 -0
  88. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/verification/generator/state.py +0 -0
  89. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/verification/invariants/__init__.py +0 -0
  90. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/verification/invariants/capability.py +0 -0
  91. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/verification/metrics/__init__.py +0 -0
  92. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/verification/metrics/base.py +0 -0
  93. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/verification/metrics/opcode.py +0 -0
  94. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/verification/metrics/state_space.py +0 -0
  95. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/verification/metrics/trap.py +0 -0
  96. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/verification/mutation.py +0 -0
  97. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/verification/oracle.py +0 -0
  98. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/verification/schema/__init__.py +0 -0
  99. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/verification/schema/event.py +0 -0
  100. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/verification/shrink.py +0 -0
  101. {cortex_runtime-0.3.0 → cortex_runtime-0.4.0}/cortex/tools/verify.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: cortex-runtime
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Spatiotemporal authority and semantic verification framework for autonomous workflows
5
5
  Project-URL: Homepage, https://github.com/Iradukunda-Fils/Cortex
6
6
  Project-URL: Documentation, https://github.com/Iradukunda-Fils/Cortex#readme
@@ -0,0 +1,203 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://cortex.security/schemas/v1/configuration.schema.json",
4
+ "title": "CortexDesiredConfiguration",
5
+ "description": "Normative canonical schema for Cortex Control Plane desired execution state. Excludes derived identity metadata (config_hash, config_generation).",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": [
9
+ "schema_version",
10
+ "gateway",
11
+ "replica_group",
12
+ "sandbox",
13
+ "resource_limits"
14
+ ],
15
+ "properties": {
16
+ "schema_version": {
17
+ "type": "string",
18
+ "enum": ["1.0.0"]
19
+ },
20
+ "gateway": {
21
+ "type": "object",
22
+ "additionalProperties": false,
23
+ "required": [
24
+ "max_queue_depth",
25
+ "max_worker_inflight",
26
+ "queue_timeout_sec",
27
+ "dispatch_deadline_sec",
28
+ "selection_policy",
29
+ "journal_path",
30
+ "fsync_policy"
31
+ ],
32
+ "properties": {
33
+ "max_queue_depth": {
34
+ "type": "integer",
35
+ "minimum": 1,
36
+ "maximum": 10000,
37
+ "default": 1000,
38
+ "description": "Maximum aggregate queued invocations across all replica groups before ERR_QUEUE_FULL"
39
+ },
40
+ "max_worker_inflight": {
41
+ "type": "integer",
42
+ "minimum": 1,
43
+ "maximum": 100,
44
+ "default": 10,
45
+ "description": "Maximum concurrent inflight invocations assigned per ready worker instance"
46
+ },
47
+ "queue_timeout_sec": {
48
+ "type": "number",
49
+ "minimum": 0.1,
50
+ "maximum": 300.0,
51
+ "default": 30.0,
52
+ "description": "Timeout in seconds before queued invocation raises ERR_QUEUE_TIMEOUT"
53
+ },
54
+ "dispatch_deadline_sec": {
55
+ "type": "number",
56
+ "minimum": 0.1,
57
+ "maximum": 60.0,
58
+ "default": 5.0,
59
+ "description": "Maximum seconds allowed to execute atomic dispatch pipeline"
60
+ },
61
+ "selection_policy": {
62
+ "type": "string",
63
+ "enum": ["least_inflight_deterministic", "round_robin_deterministic"],
64
+ "default": "least_inflight_deterministic",
65
+ "description": "Algorithm used by RoutingPolicy for worker selection"
66
+ },
67
+ "journal_path": {
68
+ "type": "string",
69
+ "pattern": "^/(?:[a-zA-Z0-9_-]+/)*[a-zA-Z0-9._-]+$",
70
+ "default": "/var/log/cortex/invocation_journal.jsonl",
71
+ "description": "Absolute filesystem path to append-only JSON-lines invocation state journal"
72
+ },
73
+ "fsync_policy": {
74
+ "type": "string",
75
+ "enum": ["always", "batch", "never"],
76
+ "default": "always",
77
+ "description": "Fsync discipline for persistent ledger journal writes"
78
+ }
79
+ }
80
+ },
81
+ "replica_group": {
82
+ "type": "object",
83
+ "additionalProperties": false,
84
+ "required": [
85
+ "group_id",
86
+ "min_replicas",
87
+ "max_replicas",
88
+ "drain_deadline_sec"
89
+ ],
90
+ "properties": {
91
+ "group_id": {
92
+ "type": "string",
93
+ "pattern": "^[a-z0-9_-]+$",
94
+ "description": "Unique alphanumeric ReplicaGroup identifier"
95
+ },
96
+ "min_replicas": {
97
+ "type": "integer",
98
+ "minimum": 1,
99
+ "maximum": 1000,
100
+ "default": 1
101
+ },
102
+ "max_replicas": {
103
+ "type": "integer",
104
+ "minimum": 1,
105
+ "maximum": 1000,
106
+ "default": 10
107
+ },
108
+ "drain_deadline_sec": {
109
+ "type": "number",
110
+ "minimum": 1.0,
111
+ "maximum": 300.0,
112
+ "default": 30.0,
113
+ "description": "Seconds allowed for graceful worker drain before forced SIGKILL"
114
+ }
115
+ }
116
+ },
117
+ "sandbox": {
118
+ "type": "object",
119
+ "additionalProperties": false,
120
+ "required": [
121
+ "profile_name",
122
+ "required_capabilities",
123
+ "allowed_syscalls",
124
+ "landlock_paths",
125
+ "read_only_root",
126
+ "allowed_write_paths"
127
+ ],
128
+ "properties": {
129
+ "profile_name": {
130
+ "type": "string",
131
+ "enum": ["Profile_A_Linux_Strict"],
132
+ "description": "Isolation profile name. Permissive/Development profiles MUST NOT be used in production schemas."
133
+ },
134
+ "required_capabilities": {
135
+ "type": "array",
136
+ "items": {
137
+ "type": "string",
138
+ "pattern": "^[a-z0-9_-]+\\.[a-z0-9._-]+$"
139
+ },
140
+ "uniqueItems": true,
141
+ "description": "Explicit capability identifiers (format: namespace.action)"
142
+ },
143
+ "allowed_syscalls": {
144
+ "type": "array",
145
+ "items": {
146
+ "type": "string",
147
+ "pattern": "^[a-zA-Z0-9_]+$"
148
+ },
149
+ "uniqueItems": true,
150
+ "description": "Whitelist of allowed Linux system call names for Seccomp filter"
151
+ },
152
+ "landlock_paths": {
153
+ "type": "array",
154
+ "items": {
155
+ "type": "string",
156
+ "pattern": "^/(?:[a-zA-Z0-9_-]+/)*[a-zA-Z0-9._-]*$"
157
+ },
158
+ "uniqueItems": true,
159
+ "description": "Filesystem path boundaries restricted by Landlock ABI"
160
+ },
161
+ "read_only_root": {
162
+ "type": "boolean",
163
+ "default": true,
164
+ "description": "Enforce read-only root filesystem mounting"
165
+ },
166
+ "allowed_write_paths": {
167
+ "type": "array",
168
+ "items": {
169
+ "type": "string",
170
+ "pattern": "^/tmp/sandbox_[a-zA-Z0-9_-]+$"
171
+ },
172
+ "default": ["/tmp/sandbox_default"],
173
+ "uniqueItems": true,
174
+ "description": "Strictly restricted write path boundaries inside worker mount namespace"
175
+ }
176
+ }
177
+ },
178
+ "resource_limits": {
179
+ "type": "object",
180
+ "additionalProperties": false,
181
+ "required": [
182
+ "memory_limit_mb",
183
+ "cpu_quota_percent"
184
+ ],
185
+ "properties": {
186
+ "memory_limit_mb": {
187
+ "type": "integer",
188
+ "minimum": 64,
189
+ "maximum": 32768,
190
+ "default": 512,
191
+ "description": "Cgroups v2 hard memory ceiling in megabytes per worker"
192
+ },
193
+ "cpu_quota_percent": {
194
+ "type": "integer",
195
+ "minimum": 10,
196
+ "maximum": 1600,
197
+ "default": 100,
198
+ "description": "CPU quota allocation percentage per worker"
199
+ }
200
+ }
201
+ }
202
+ }
203
+ }
@@ -0,0 +1,56 @@
1
+ """
2
+ Cortex Multi-Replica Kernel Subsystem (Phases 1–4)
3
+
4
+ Provides modular components for execution identity coordinates, Gateway lease fencing,
5
+ durable invocation state ledgers, worker lifecycle state tracking, and routing & dispatch.
6
+ """
7
+
8
+ from cortex.tools.kernel.replica.identity import (
9
+ ExecutionIdentity,
10
+ OwnershipIdentity,
11
+ StaleConfigGenerationError,
12
+ )
13
+ from cortex.tools.kernel.replica.lease import LeaseManager, StaleLeaseError
14
+ from cortex.tools.kernel.replica.ledger import (
15
+ TERMINAL_STATES,
16
+ InvocationState,
17
+ InvocationStateLedger,
18
+ RecoveryBucket,
19
+ )
20
+ from cortex.tools.kernel.replica.lifecycle import WorkerLifecycleStage, WorkerLifecycleTracker
21
+ from cortex.tools.kernel.replica.router import (
22
+ CandidateResolver,
23
+ ExecutionClass,
24
+ GatewayDispatcher,
25
+ NoEligibleWorkerNow,
26
+ QueueFullError,
27
+ QueueTimeoutError,
28
+ RoutingDecisionEvent,
29
+ RoutingPolicy,
30
+ StateDomainKey,
31
+ WorkerRef,
32
+ )
33
+
34
+ __all__ = [
35
+ "ExecutionIdentity",
36
+ "OwnershipIdentity",
37
+ "StaleConfigGenerationError",
38
+ "InvocationState",
39
+ "RecoveryBucket",
40
+ "InvocationStateLedger",
41
+ "TERMINAL_STATES",
42
+ "LeaseManager",
43
+ "StaleLeaseError",
44
+ "WorkerLifecycleStage",
45
+ "WorkerLifecycleTracker",
46
+ "WorkerRef",
47
+ "ExecutionClass",
48
+ "StateDomainKey",
49
+ "RoutingDecisionEvent",
50
+ "CandidateResolver",
51
+ "RoutingPolicy",
52
+ "GatewayDispatcher",
53
+ "NoEligibleWorkerNow",
54
+ "QueueFullError",
55
+ "QueueTimeoutError",
56
+ ]
@@ -0,0 +1,70 @@
1
+ """
2
+ Cortex Replica Coordinate System (Phase 1)
3
+
4
+ Explicitly separates runtime worker execution coordinates (ExecutionIdentity)
5
+ from Gateway lease ownership coordinates (OwnershipIdentity).
6
+ """
7
+
8
+ from dataclasses import dataclass
9
+
10
+
11
+ class StaleConfigGenerationError(Exception):
12
+ """Raised when a worker operates under a stale ConfigGeneration or mismatched ConfigHash."""
13
+
14
+ pass
15
+
16
+
17
+ @dataclass(frozen=True)
18
+ class ExecutionIdentity:
19
+ """Uniquely identifies a specific worker execution attempt runtime coordinate.
20
+
21
+ Every worker carries both a ReplicaGeneration (deployment wave) and a
22
+ ConfigGeneration (immutable configuration snapshot version + SHA-256 hash).
23
+ The Gateway rejects workers whose config_generation or config_hash does not
24
+ match the active deployment.
25
+ """
26
+
27
+ group_id: str
28
+ instance_id: str
29
+ generation: int
30
+ config_generation: int
31
+ attempt_id: int
32
+ config_hash: str = ""
33
+
34
+ def __post_init__(self) -> None:
35
+ if not isinstance(self.group_id, str) or not self.group_id.strip():
36
+ raise ValueError("group_id must be a non-empty string")
37
+ if not isinstance(self.instance_id, str) or not self.instance_id.strip():
38
+ raise ValueError("instance_id must be a non-empty string")
39
+ if not isinstance(self.generation, int) or self.generation < 1:
40
+ raise ValueError("generation must be a positive integer >= 1")
41
+ if not isinstance(self.config_generation, int) or self.config_generation < 1:
42
+ raise ValueError("config_generation must be a positive integer >= 1")
43
+ if not isinstance(self.attempt_id, int) or self.attempt_id < 1:
44
+ raise ValueError("attempt_id must be a positive integer >= 1")
45
+
46
+ def coordinate_string(self) -> str:
47
+ """Returns string representation for audit logs."""
48
+ hash_suffix = f":h{self.config_hash[:8]}" if self.config_hash else ""
49
+ return f"{self.group_id}:{self.instance_id}:g{self.generation}:cfg{self.config_generation}{hash_suffix}:a{self.attempt_id}"
50
+
51
+
52
+ @dataclass(frozen=True)
53
+ class OwnershipIdentity:
54
+ """Uniquely identifies Gateway authority and lease ownership for an invocation."""
55
+
56
+ invocation_id: str
57
+ lease_id: str
58
+ lease_epoch: int
59
+
60
+ def __post_init__(self) -> None:
61
+ if not isinstance(self.invocation_id, str) or not self.invocation_id.strip():
62
+ raise ValueError("invocation_id must be a non-empty string")
63
+ if not isinstance(self.lease_id, str) or not self.lease_id.strip():
64
+ raise ValueError("lease_id must be a non-empty string")
65
+ if not isinstance(self.lease_epoch, int) or self.lease_epoch < 1:
66
+ raise ValueError("lease_epoch must be a positive integer >= 1")
67
+
68
+ def coordinate_string(self) -> str:
69
+ """Returns string representation for audit logs."""
70
+ return f"inv:{self.invocation_id}:lease:{self.lease_id}:ep{self.lease_epoch}"
@@ -0,0 +1,227 @@
1
+ """
2
+ Linearizable Gateway Lease Manager (Phase 2 & 4)
3
+
4
+ Enforces atomic, mutually exclusive commit and revocation fencing bound to monotonic
5
+ LeaseEpoch counters. Stale commits are rejected with StaleLeaseError.
6
+
7
+ LINEARIZABILITY DOMAIN: Single Gateway Authority Domain.
8
+ Future multi-process or multi-node Gateway implementations MUST preserve
9
+ linearizable mutually exclusive commit and revocation fencing at this boundary.
10
+ """
11
+
12
+ import threading
13
+ import uuid
14
+ from dataclasses import dataclass
15
+ from typing import TYPE_CHECKING, Dict, Optional
16
+
17
+ from cortex.tools.kernel.replica.identity import OwnershipIdentity
18
+ from cortex.tools.kernel.replica.lifecycle import WorkerLifecycleStage
19
+
20
+ if TYPE_CHECKING:
21
+ from cortex.tools.kernel.replica.router import WorkerRef
22
+
23
+
24
+ class StaleLeaseError(Exception):
25
+ """Raised when a commit attempt uses a revoked or stale lease epoch (ERR_STALE_LEASE_EPOCH)."""
26
+
27
+ pass
28
+
29
+
30
+ @dataclass
31
+ class LeaseRecord:
32
+ ownership_identity: OwnershipIdentity
33
+ assigned_worker_id: str
34
+ active: bool = True
35
+ committed: bool = False
36
+
37
+
38
+ class LeaseManager:
39
+ """Thread-safe, linearizable Gateway Lease Authority (Single Gateway Authority Domain)."""
40
+
41
+ def __init__(self) -> None:
42
+ self._lock = threading.Lock()
43
+ # invocation_id -> LeaseRecord
44
+ self._active_leases: Dict[str, LeaseRecord] = {}
45
+ # invocation_id -> current_epoch
46
+ self._latest_epoch: Dict[str, int] = {}
47
+ # worker_id -> WorkerRef (active registered Gateway worker state)
48
+ self._worker_registry: Dict[str, "WorkerRef"] = {}
49
+
50
+ def register_worker_state(self, worker_ref: "WorkerRef") -> None:
51
+ """Register or update active worker replica state in the Gateway TCB registry."""
52
+ with self._lock:
53
+ self._worker_registry[worker_ref.instance_id] = worker_ref
54
+
55
+ def unregister_worker(self, worker_id: str) -> None:
56
+ """Remove worker replica from Gateway TCB registry on eviction or process death."""
57
+ with self._lock:
58
+ self._worker_registry.pop(worker_id, None)
59
+
60
+ def revalidate_candidate(
61
+ self,
62
+ worker_ref: "WorkerRef",
63
+ active_config_gen: int,
64
+ active_config_hash: str,
65
+ active_sandbox_hash: str,
66
+ active_cap_hash: str,
67
+ max_inflight: int = 10,
68
+ ) -> bool:
69
+ """Atomically revalidate a proposed candidate snapshot inside the Gateway TCB lock."""
70
+ with self._lock:
71
+ return self._revalidate_unlocked(
72
+ worker_ref=worker_ref,
73
+ active_config_gen=active_config_gen,
74
+ active_config_hash=active_config_hash,
75
+ active_sandbox_hash=active_sandbox_hash,
76
+ active_cap_hash=active_cap_hash,
77
+ max_inflight=max_inflight,
78
+ )
79
+
80
+ def _revalidate_unlocked(
81
+ self,
82
+ worker_ref: "WorkerRef",
83
+ active_config_gen: int,
84
+ active_config_hash: str,
85
+ active_sandbox_hash: str,
86
+ active_cap_hash: str,
87
+ max_inflight: int,
88
+ ) -> bool:
89
+ registered = self._worker_registry.get(worker_ref.instance_id)
90
+ if not registered:
91
+ return False
92
+
93
+ if registered.lifecycle_version != worker_ref.lifecycle_version:
94
+ return False
95
+
96
+ if registered.stage != WorkerLifecycleStage.READY:
97
+ return False
98
+
99
+ if registered.config_generation != active_config_gen:
100
+ return False
101
+
102
+ if registered.config_hash != active_config_hash:
103
+ return False
104
+
105
+ if registered.sandbox_profile_hash != active_sandbox_hash:
106
+ return False
107
+
108
+ if registered.capability_envelope_hash != active_cap_hash:
109
+ return False
110
+
111
+ active_count = sum(
112
+ 1 for rec in self._active_leases.values()
113
+ if rec.assigned_worker_id == worker_ref.instance_id and rec.active and not rec.committed
114
+ )
115
+ if active_count >= max_inflight:
116
+ return False
117
+
118
+ return True
119
+
120
+ def grant_lease_with_revalidation(
121
+ self,
122
+ invocation_id: str,
123
+ worker_ref: "WorkerRef",
124
+ active_config_gen: int,
125
+ active_config_hash: str,
126
+ active_sandbox_hash: str,
127
+ active_cap_hash: str,
128
+ max_inflight: int = 10,
129
+ ) -> Optional[OwnershipIdentity]:
130
+ """Atomically revalidates candidate AND grants lease inside a single lock acquisition (Linearization Point)."""
131
+ with self._lock:
132
+ if not self._revalidate_unlocked(
133
+ worker_ref=worker_ref,
134
+ active_config_gen=active_config_gen,
135
+ active_config_hash=active_config_hash,
136
+ active_sandbox_hash=active_sandbox_hash,
137
+ active_cap_hash=active_cap_hash,
138
+ max_inflight=max_inflight,
139
+ ):
140
+ return None
141
+
142
+ current_epoch = self._latest_epoch.get(invocation_id, 0) + 1
143
+ self._latest_epoch[invocation_id] = current_epoch
144
+
145
+ lease_id = str(uuid.uuid4())
146
+ ownership = OwnershipIdentity(
147
+ invocation_id=invocation_id,
148
+ lease_id=lease_id,
149
+ lease_epoch=current_epoch,
150
+ )
151
+
152
+ record = LeaseRecord(
153
+ ownership_identity=ownership,
154
+ assigned_worker_id=worker_ref.instance_id,
155
+ active=True,
156
+ committed=False,
157
+ )
158
+ self._active_leases[invocation_id] = record
159
+ return ownership
160
+
161
+ def grant_lease(self, invocation_id: str, worker_id: str) -> OwnershipIdentity:
162
+ """Atomically grants an epoch-bound lease to a worker for an invocation."""
163
+ with self._lock:
164
+ current_epoch = self._latest_epoch.get(invocation_id, 0) + 1
165
+ self._latest_epoch[invocation_id] = current_epoch
166
+
167
+ lease_id = str(uuid.uuid4())
168
+ ownership = OwnershipIdentity(
169
+ invocation_id=invocation_id,
170
+ lease_id=lease_id,
171
+ lease_epoch=current_epoch,
172
+ )
173
+
174
+ record = LeaseRecord(
175
+ ownership_identity=ownership,
176
+ assigned_worker_id=worker_id,
177
+ active=True,
178
+ committed=False,
179
+ )
180
+ self._active_leases[invocation_id] = record
181
+ return ownership
182
+
183
+ def commit_invocation(self, invocation_id: str, lease_epoch: int) -> bool:
184
+ """Atomically commits an invocation under a specific lease epoch.
185
+
186
+ Must be mutually exclusive with revoke_lease. If lease_epoch != active lease epoch,
187
+ or lease is inactive, raises StaleLeaseError.
188
+ """
189
+ with self._lock:
190
+ record = self._active_leases.get(invocation_id)
191
+ if not record:
192
+ raise StaleLeaseError(f"ERR_STALE_LEASE_EPOCH: No active lease found for {invocation_id}")
193
+
194
+ if not record.active:
195
+ raise StaleLeaseError(f"ERR_STALE_LEASE_EPOCH: Lease for {invocation_id} has been revoked")
196
+
197
+ if record.ownership_identity.lease_epoch != lease_epoch:
198
+ raise StaleLeaseError(
199
+ f"ERR_STALE_LEASE_EPOCH: Commit epoch {lease_epoch} does not match active epoch {record.ownership_identity.lease_epoch}"
200
+ )
201
+
202
+ record.committed = True
203
+ record.active = False
204
+ return True
205
+
206
+ def revoke_lease(self, invocation_id: str, lease_epoch: int) -> bool:
207
+ """Atomically revokes a lease for an invocation under a specific lease epoch.
208
+
209
+ Must be mutually exclusive with commit_invocation. Returns True if lease was revoked.
210
+ """
211
+ with self._lock:
212
+ record = self._active_leases.get(invocation_id)
213
+ if not record:
214
+ return False
215
+
216
+ if record.ownership_identity.lease_epoch == lease_epoch and record.active:
217
+ record.active = False
218
+ return True
219
+ return False
220
+
221
+ def is_lease_valid(self, invocation_id: str, lease_epoch: int) -> bool:
222
+ """Returns True iff the lease is active and matches lease_epoch."""
223
+ with self._lock:
224
+ record = self._active_leases.get(invocation_id)
225
+ if not record:
226
+ return False
227
+ return record.active and record.ownership_identity.lease_epoch == lease_epoch