velophex 0.2.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.
Files changed (43) hide show
  1. velophex/__init__.py +38 -0
  2. velophex/_json.py +297 -0
  3. velophex/agents/__init__.py +130 -0
  4. velophex/agents/_errors.py +75 -0
  5. velophex/agents/_loop.py +436 -0
  6. velophex/agents/_tools.py +168 -0
  7. velophex/agents/_tracing.py +87 -0
  8. velophex/agents/testing.py +249 -0
  9. velophex/cli/__init__.py +5 -0
  10. velophex/cli/main.py +548 -0
  11. velophex/cli/publish.py +152 -0
  12. velophex/cli/sync.py +90 -0
  13. velophex/cli/templates.py +126 -0
  14. velophex/contracts/__init__.py +103 -0
  15. velophex/contracts/_model.py +26 -0
  16. velophex/contracts/agents.py +110 -0
  17. velophex/contracts/diagnostics.py +157 -0
  18. velophex/contracts/executor.py +1921 -0
  19. velophex/contracts/package.py +391 -0
  20. velophex/contracts/project.py +1008 -0
  21. velophex/contracts/runtimes.py +226 -0
  22. velophex/contracts/signin.py +53 -0
  23. velophex/contracts/validation.py +158 -0
  24. velophex/entrypoints.py +329 -0
  25. velophex/errors.py +44 -0
  26. velophex/packaging.py +465 -0
  27. velophex/py.typed +0 -0
  28. velophex/runtime/__init__.py +471 -0
  29. velophex/runtime/_session.py +192 -0
  30. velophex/runtime/client.py +957 -0
  31. velophex/runtime/models.py +326 -0
  32. velophex/signing.py +425 -0
  33. velophex/worker/__init__.py +27 -0
  34. velophex/worker/__main__.py +118 -0
  35. velophex/worker/framing.py +248 -0
  36. velophex/worker/loader.py +105 -0
  37. velophex/worker/outbox.py +147 -0
  38. velophex/worker/session.py +519 -0
  39. velophex-0.2.0.dist-info/METADATA +313 -0
  40. velophex-0.2.0.dist-info/RECORD +43 -0
  41. velophex-0.2.0.dist-info/WHEEL +4 -0
  42. velophex-0.2.0.dist-info/entry_points.txt +2 -0
  43. velophex-0.2.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,226 @@
1
+ """Runtime kinds and the runner's shared vocabulary (phases, levels, fault kinds).
2
+
3
+ Mirrors ``VeloPhex.Platform.Contracts.Runtimes`` and ``…Contracts.Runner.RunnerVocabulary``.
4
+ A runtime kind stays an open token: the platform recognises a runtime by registering an
5
+ adapter for its kind, never by switching over a closed enum (ADR-P001), so this module
6
+ offers constants and a validator rather than an ``Enum``.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Final
12
+
13
+ from velophex.contracts.validation import is_runtime_kind
14
+
15
+ __all__ = [
16
+ "MAXIMUM_RUNTIME_KIND_LENGTH",
17
+ "AttemptOutcomes",
18
+ "ControlSignals",
19
+ "ExecutionPhases",
20
+ "FaultCodes",
21
+ "FaultKinds",
22
+ "LogLevels",
23
+ "LogStages",
24
+ "PhaseStatuses",
25
+ "RuntimeKind",
26
+ "RuntimeStatuses",
27
+ "SignalReasons",
28
+ "parse_runtime_kind",
29
+ ]
30
+
31
+ MAXIMUM_RUNTIME_KIND_LENGTH: Final = 32
32
+
33
+
34
+ class RuntimeKind:
35
+ """The runtime kinds the platform ships with."""
36
+
37
+ VELOPHEX_WORKFLOW: Final = "velophex-workflow"
38
+ PYTHON: Final = "python"
39
+
40
+
41
+ def parse_runtime_kind(value: str | None) -> str:
42
+ """Return *value* if it is a well-formed runtime kind, else raise :class:`ValueError`."""
43
+ if not is_runtime_kind(value):
44
+ raise ValueError(
45
+ f"{value!r} is not a runtime kind. Use a lowercase token such as 'velophex-workflow' or 'python'."
46
+ )
47
+
48
+ assert value is not None
49
+ return value
50
+
51
+
52
+ class RuntimeStatuses:
53
+ """Health of a runtime a runner reports it has installed.
54
+
55
+ Two values, not three. A runtime the machine does not have is *absent* from the report, not
56
+ "Unavailable": the Orchestrator places a job only on a runner that reports the runtimes the
57
+ job needs, so a missing entry already says everything a third status could. ``IsKnown`` on the
58
+ .NET side rejects anything else, which means a reporter that invented one would be refused.
59
+ """
60
+
61
+ AVAILABLE: Final = "Available"
62
+ DEGRADED: Final = "Degraded"
63
+
64
+
65
+ class ExecutionPhases:
66
+ """Runtime-neutral log phases. Runtime-specific detail belongs in ``subPhase``."""
67
+
68
+ DOWNLOAD: Final = "Download"
69
+ VERIFY: Final = "Verify"
70
+ EXTRACT: Final = "Extract"
71
+ ENVIRONMENT_PREPARE: Final = "EnvironmentPrepare"
72
+ DEPENDENCY_RESTORE: Final = "DependencyRestore"
73
+ EXECUTION: Final = "Execution"
74
+ CLEANUP: Final = "Cleanup"
75
+
76
+ ALL: Final = (DOWNLOAD, VERIFY, EXTRACT, ENVIRONMENT_PREPARE, DEPENDENCY_RESTORE, EXECUTION, CLEANUP)
77
+
78
+
79
+ class LogStages:
80
+ """The two stages a log line belongs to; derived from the phase when it is omitted."""
81
+
82
+ PREPARATION: Final = "Preparation"
83
+ EXECUTION: Final = "Execution"
84
+
85
+
86
+ class PhaseStatuses:
87
+ """Status of a preparation phase."""
88
+
89
+ STARTED: Final = "Started"
90
+ SUCCEEDED: Final = "Succeeded"
91
+ FAILED: Final = "Failed"
92
+ SKIPPED: Final = "Skipped"
93
+
94
+
95
+ class LogLevels:
96
+ """Log levels of the runner protocol; the worker maps :mod:`logging` levels onto these."""
97
+
98
+ TRACE: Final = "Trace"
99
+ DEBUG: Final = "Debug"
100
+ INFORMATION: Final = "Information"
101
+ WARNING: Final = "Warning"
102
+ ERROR: Final = "Error"
103
+ CRITICAL: Final = "Critical"
104
+
105
+
106
+ class FaultKinds:
107
+ """How an attempt failed. The kind drives retries."""
108
+
109
+ BUSINESS: Final = "Business"
110
+ APPLICATION: Final = "Application"
111
+ TIMEOUT: Final = "Timeout"
112
+ INFRASTRUCTURE: Final = "Infrastructure"
113
+
114
+
115
+ class FaultCodes:
116
+ """Well-known fault codes shared by the runner and the Orchestrator."""
117
+
118
+ RUNTIME_NOT_INSTALLED: Final = "RUNTIME_NOT_INSTALLED"
119
+ RUNTIME_DEGRADED: Final = "RUNTIME_DEGRADED"
120
+ PACKAGE_VERIFICATION_FAILED: Final = "PACKAGE_VERIFICATION_FAILED"
121
+ ENVIRONMENT_PREPARATION_FAILED: Final = "ENVIRONMENT_PREPARATION_FAILED"
122
+ EXECUTOR_START_FAILED: Final = "EXECUTOR_START_FAILED"
123
+ EXECUTOR_CRASHED: Final = "EXECUTOR_CRASHED"
124
+ EXECUTOR_PROTOCOL_VIOLATION: Final = "EXECUTOR_PROTOCOL_VIOLATION"
125
+
126
+ #: The start inputs do not fit the handler's signature. Mirrors the workflow executor.
127
+ INVALID_INPUT: Final = "INVALID_INPUT"
128
+
129
+ #: project.json, the entry point or the handler module could not be loaded.
130
+ INVALID_PACKAGE: Final = "INVALID_PACKAGE"
131
+
132
+ #: The handler raised something that is not a BusinessError.
133
+ UNHANDLED_EXCEPTION: Final = "UNHANDLED_EXCEPTION"
134
+
135
+ #: The handler's outputs do not fit in one executor frame (16 MiB by default). Application,
136
+ #: not retryable.
137
+ RESULT_TOO_LARGE: Final = "RESULT_TOO_LARGE"
138
+
139
+ #: The handler returned a value with no JSON form (a ``datetime``, a set, NaN). Application,
140
+ #: not retryable.
141
+ RESULT_NOT_SERIALIZABLE: Final = "RESULT_NOT_SERIALIZABLE"
142
+
143
+ #: A workflow suspended itself and no host is holding its instance. A Python worker never
144
+ #: reports this, but the table is the platform's, so a Python tool that reads a fault off the
145
+ #: Orchestrator must be able to name it.
146
+ WORKFLOW_SUSPENDED: Final = "WORKFLOW_SUSPENDED"
147
+
148
+ #: The executor or the robot was lost after the automation may have acted (crash, lost
149
+ #: lease, forced timeout). Never retried; an operator reconciles. A Python worker never
150
+ #: reports it, but a tool reading a fault off the Orchestrator must be able to name it.
151
+ OUTCOME_UNKNOWN: Final = "VXR-OUTCOME-UNKNOWN"
152
+
153
+ #: An unattended job was refused before launch because no run-as account is configured.
154
+ RUNAS_REQUIRED: Final = "VXR-RUNAS-REQUIRED"
155
+
156
+ #: A workflow package was built under a looser expression policy than the tenant's (engine
157
+ #: ADR 0036). A Python worker never reports it; a tool reading faults must be able to name it.
158
+ EXPRESSION_POLICY_LOOSER: Final = "EXPRESSION_POLICY_LOOSER"
159
+
160
+ # Contracts 0.8.37, agents: the gateway's own refusal codes, carried unchanged into the fault.
161
+
162
+ #: The workspace's AI budget is spent. Application, not retryable.
163
+ AI_BUDGET_EXHAUSTED: Final = "ai_budget_exhausted"
164
+
165
+ #: The workspace's AI policy refused the call. Application, not retryable.
166
+ AI_POLICY_DENIED: Final = "ai_policy_denied"
167
+
168
+ #: The model provider failed after the gateway's retries. Infrastructure.
169
+ AI_PROVIDER_UNAVAILABLE: Final = "ai_provider_unavailable"
170
+
171
+ #: The model call is invalid, e.g. an unknown binding. Application, not retryable.
172
+ AI_REQUEST_INVALID: Final = "ai_request_invalid"
173
+
174
+ #: The answer did not satisfy the output schema after one repair turn. Application, not retryable.
175
+ AGENT_OUTPUT_INVALID: Final = "agent_output_invalid"
176
+
177
+ #: The run reached a guardrail (steps, tokens, timeout). Application, not retryable.
178
+ AGENT_LIMIT_EXCEEDED: Final = "agent_limit_exceeded"
179
+
180
+ #: A tool call ended without a known outcome; reconciled by a person, never replayed.
181
+ TOOL_OUTCOME_UNKNOWN: Final = "tool_outcome_unknown"
182
+
183
+
184
+ class FaultRemediations:
185
+ """Where the remediation of every ``VXR-*`` fault code is documented (contracts 0.8.9).
186
+
187
+ The remediation table itself lives in the .NET contract; Python only needs the page.
188
+ """
189
+
190
+ REFERENCE_PAGE: Final = "VeloPhex.Robot/docs/fault-codes.md"
191
+
192
+
193
+ class AttemptOutcomes:
194
+ """Terminal outcome of an attempt or a sub-invocation."""
195
+
196
+ SUCCEEDED: Final = "Succeeded"
197
+ FAILED: Final = "Failed"
198
+ CANCELED: Final = "Canceled"
199
+
200
+ #: The run waits and was checkpointed; the job resumes on a later attempt (durable execution,
201
+ #: ADR-P015, Contracts 0.8.22). A Python automation never suspends; the value is mirrored so a
202
+ #: client reading an Orchestrator's attempts knows it.
203
+ SUSPENDED: Final = "Suspended"
204
+
205
+
206
+ class ControlSignals:
207
+ """What the Orchestrator asks a running attempt to do."""
208
+
209
+ NONE: Final = "None"
210
+
211
+ #: Ask the automation to end gracefully.
212
+ STOP: Final = "Stop"
213
+
214
+ #: Terminate the automation immediately.
215
+ KILL: Final = "Kill"
216
+
217
+
218
+ class SignalReasons:
219
+ """Why a stop was asked for."""
220
+
221
+ USER: Final = "User"
222
+ TIMEOUT: Final = "Timeout"
223
+
224
+ #: ``canceled.reason`` only: the workflow ended itself with Terminate Workflow (contracts
225
+ #: 0.8.11). The run is neither Succeeded nor Failed.
226
+ TERMINATED: Final = "Terminated"
@@ -0,0 +1,53 @@
1
+ """The states a sign-in passes through, in every client that signs a person in.
2
+
3
+ Mirrors ``VeloPhex.Platform.Contracts.Identity.SignInStates`` (design review 2026-09-23, D1,
4
+ D2, D11). The copy table (``SignInCopy``) stays in the .NET contract for now: the CLI has no
5
+ sign-in of its own yet, and when it gets one it prints these states through the same table
6
+ rather than words of its own.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ from typing import Final
12
+
13
+ __all__ = ["SignInStates"]
14
+
15
+
16
+ class SignInStates:
17
+ """The vocabulary, by value; ``test_vocabulary_mirror`` holds it to the C# source."""
18
+
19
+ SIGNED_OUT: Final = "SignedOut"
20
+ REQUESTING_CODE: Final = "RequestingCode"
21
+ CODE_SHOWN: Final = "CodeShown"
22
+ WAITING_FOR_APPROVAL: Final = "WaitingForApproval"
23
+ APPROVED_COMPLETING: Final = "ApprovedCompleting"
24
+ APPROVED: Final = "Approved"
25
+ EXPIRED: Final = "Expired"
26
+ DENIED: Final = "Denied"
27
+ NO_TENANT: Final = "NoTenant"
28
+ UNREACHABLE: Final = "Unreachable"
29
+ REFUSED: Final = "Refused"
30
+ SESSION_EXPIRED: Final = "SessionExpired"
31
+
32
+ ALL: Final = (
33
+ SIGNED_OUT,
34
+ REQUESTING_CODE,
35
+ CODE_SHOWN,
36
+ WAITING_FOR_APPROVAL,
37
+ APPROVED_COMPLETING,
38
+ APPROVED,
39
+ EXPIRED,
40
+ DENIED,
41
+ NO_TENANT,
42
+ UNREACHABLE,
43
+ REFUSED,
44
+ SESSION_EXPIRED,
45
+ )
46
+
47
+ #: States the flow cannot leave by itself: the person acts, or the client starts over.
48
+ TERMINAL: Final = (APPROVED, EXPIRED, DENIED, NO_TENANT, UNREACHABLE, REFUSED, SESSION_EXPIRED)
49
+
50
+ @classmethod
51
+ def is_known(cls, state: str | None) -> bool:
52
+ """Whether *state* is one of the contract's states (``SignInStates.IsKnown`` in .NET)."""
53
+ return state is not None and state in cls.ALL
@@ -0,0 +1,158 @@
1
+ """Lexical rules shared with ``VeloPhex.Platform.Contracts.Validation.ContractPatterns``.
2
+
3
+ The patterns are the ones in ``schemas/project.schema.json``; they are repeated here because
4
+ a Python project is validated before it is packed, when no .NET tool is in reach.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import re
10
+ from typing import Final
11
+
12
+ __all__ = [
13
+ "MAXIMUM_PACKAGE_ID_LENGTH",
14
+ "MAXIMUM_PATH_LENGTH",
15
+ "MAXIMUM_VERSION_RANGE_LENGTH",
16
+ "has_build_metadata",
17
+ "is_capability",
18
+ "is_entry_point_id",
19
+ "is_package_id",
20
+ "is_python_handler",
21
+ "is_runtime_kind",
22
+ "is_safe_relative_path",
23
+ "is_semantic_version",
24
+ "is_sha512_hex",
25
+ "is_version_range",
26
+ ]
27
+
28
+ MAXIMUM_PACKAGE_ID_LENGTH: Final = 100
29
+ MAXIMUM_PATH_LENGTH: Final = 260
30
+ MAXIMUM_VERSION_RANGE_LENGTH: Final = 256
31
+
32
+ _PACKAGE_ID = re.compile(r"^\w+([_.-]\w+)*$")
33
+ # SemVer 2.0 without build metadata; byte for byte ContractPatterns.SemanticVersionPattern and
34
+ # project.schema.json's $defs/semanticVersion.
35
+ _SEMANTIC_VERSION = re.compile(
36
+ r"^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)"
37
+ r"(?:-((?:0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*)"
38
+ r"(?:\.(?:0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*))*))?$"
39
+ )
40
+ # The same with a mandatory build-metadata part: what used to be accepted and is now refused with
41
+ # its own diagnostic (VPC1028), so the message can say why.
42
+ _SEMANTIC_VERSION_WITH_BUILD_METADATA = re.compile(
43
+ r"^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)"
44
+ r"(?:-((?:0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*)"
45
+ r"(?:\.(?:0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*))*))?"
46
+ r"\+([0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*)$"
47
+ )
48
+ _ENTRY_POINT_ID = re.compile(r"^[A-Za-z][A-Za-z0-9_.-]{0,63}$")
49
+ _PYTHON_HANDLER = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)*:[A-Za-z_][A-Za-z0-9_]*$")
50
+ _CAPABILITY = re.compile(r"^[a-z][a-z0-9-]{0,31}$")
51
+ _SHA512_HEX = re.compile(r"^[0-9a-f]{128}$")
52
+ _RUNTIME_KIND = re.compile(r"^[a-z][a-z0-9]*(-[a-z0-9]+)*$")
53
+
54
+ # [exact] | (lower?,upper?) | bare, where a version is 1-4 numeric parts with an optional SemVer
55
+ # pre-release and build metadata, and a floating version ends in '*'. Byte for byte the expression
56
+ # in ContractPatterns.VersionRangePattern and in project.schema.json's $defs/versionRange.
57
+ _VERSION_RANGE = re.compile(
58
+ r"^\s*(?:\[\s*[0-9]+(?:\.[0-9]+){0,3}(?:-[0-9A-Za-z][0-9A-Za-z.-]*)?(?:\+[0-9A-Za-z][0-9A-Za-z.-]*)?\s*\]"
59
+ r"|[\[(]\s*(?:[0-9]+(?:\.[0-9]+){0,3}(?:-[0-9A-Za-z][0-9A-Za-z.-]*)?(?:\+[0-9A-Za-z][0-9A-Za-z.-]*)?"
60
+ r"|\*|[0-9]+(?:\.[0-9]+){0,2}\.\*|[0-9]+(?:\.[0-9]+){0,3}-[0-9A-Za-z.-]*\*)?\s*,\s*"
61
+ r"(?:[0-9]+(?:\.[0-9]+){0,3}(?:-[0-9A-Za-z][0-9A-Za-z.-]*)?(?:\+[0-9A-Za-z][0-9A-Za-z.-]*)?)?\s*[\])]"
62
+ r"|[0-9]+(?:\.[0-9]+){0,3}(?:-[0-9A-Za-z][0-9A-Za-z.-]*)?(?:\+[0-9A-Za-z][0-9A-Za-z.-]*)?"
63
+ r"|\*|[0-9]+(?:\.[0-9]+){0,2}\.\*|[0-9]+(?:\.[0-9]+){0,3}-[0-9A-Za-z.-]*\*)\s*$"
64
+ )
65
+
66
+ _FORBIDDEN_CHARACTERS: Final[frozenset[str]] = frozenset('<>"|?*')
67
+
68
+
69
+ def is_package_id(value: str | None) -> bool:
70
+ """True for a NuGet-compatible package id, which is also the project name."""
71
+ return bool(value) and len(value or "") <= MAXIMUM_PACKAGE_ID_LENGTH and _PACKAGE_ID.match(value or "") is not None
72
+
73
+
74
+ def is_semantic_version(value: str | None) -> bool:
75
+ """True for SemVer 2.0 without a leading ``v`` and without build metadata.
76
+
77
+ ``1.0.0`` and ``1.0.0-beta.1``, never ``1.0.0+build.7``: NuGet drops build metadata from a
78
+ package's identity, so a version carrying it would name one artefact in the manifest and
79
+ another on the feed (``ContractPatterns.IsSemanticVersion``; audit PRJ-01).
80
+ """
81
+ return bool(value) and len(value or "") <= 128 and _SEMANTIC_VERSION.match(value or "") is not None
82
+
83
+
84
+ def has_build_metadata(value: str | None) -> bool:
85
+ """True for a SemVer 2.0 version that is valid but for its build metadata (``1.0.0+build.7``)."""
86
+ return (
87
+ bool(value) and len(value or "") <= 128 and _SEMANTIC_VERSION_WITH_BUILD_METADATA.match(value or "") is not None
88
+ )
89
+
90
+
91
+ def is_entry_point_id(value: str | None) -> bool:
92
+ """True for an entry point id."""
93
+ return bool(value) and _ENTRY_POINT_ID.match(value or "") is not None
94
+
95
+
96
+ def is_python_handler(value: str | None) -> bool:
97
+ """True for a ``module.path:function`` handler (ADR-P002, OD-11)."""
98
+ return bool(value) and len(value or "") <= 256 and _PYTHON_HANDLER.match(value or "") is not None
99
+
100
+
101
+ def is_capability(value: str | None) -> bool:
102
+ """True for an execution capability token such as ``network``."""
103
+ return bool(value) and _CAPABILITY.match(value or "") is not None
104
+
105
+
106
+ def is_sha512_hex(value: str | None) -> bool:
107
+ """True for a lower-case hex SHA-512."""
108
+ return value is not None and len(value) == 128 and _SHA512_HEX.match(value) is not None
109
+
110
+
111
+ def is_version_range(value: str | None) -> bool:
112
+ """True for a NuGet version range, the value side of ``project.json`` -> ``dependencies``.
113
+
114
+ Three spellings: an exact version ``[1.0.0]``, an interval with either bound optional
115
+ (``[1.0.0,2.0.0)``, ``(,2.0.0]``, ``[1.0.0,)``) or a bare version ``1.0.0``, which NuGet reads
116
+ as "that version or higher". Floating versions (``*``, ``1.0.*``, ``1.0.0-*``) are accepted
117
+ where NuGet accepts them: the bare form and the lower bound, never the upper one.
118
+
119
+ Lexical only, like :func:`is_semantic_version`: the SDK must be able to validate a project
120
+ before it is packed, with no NuGet client anywhere in reach.
121
+ """
122
+ return (
123
+ bool(value)
124
+ and len(value or "") <= MAXIMUM_VERSION_RANGE_LENGTH
125
+ and _VERSION_RANGE.match(value or "") is not None
126
+ )
127
+
128
+
129
+ def is_runtime_kind(value: str | None) -> bool:
130
+ """True for a runtime kind token such as ``python``."""
131
+ return bool(value) and len(value or "") <= 32 and _RUNTIME_KIND.match(value or "") is not None
132
+
133
+
134
+ def is_safe_relative_path(value: str | None) -> bool:
135
+ """True for a relative path that cannot escape its root on any platform.
136
+
137
+ Forward slashes only, no rooted or drive paths, no empty, ``.`` or ``..`` segments, no
138
+ characters Windows forbids and no segment that ends in a dot or a space. These are exactly
139
+ the rules of ``ContractPatterns.IsSafeRelativePath``; the stricter archive rules (length,
140
+ reserved device names) belong to the package builder, which mirrors ``PackagePath``.
141
+ """
142
+ if not value or len(value) > MAXIMUM_PATH_LENGTH:
143
+ return False
144
+
145
+ if value.startswith("/") or "\\" in value or ":" in value:
146
+ return False
147
+
148
+ for segment in value.split("/"):
149
+ if not segment or segment in (".", ".."):
150
+ return False
151
+
152
+ if segment.endswith((" ", ".")):
153
+ return False
154
+
155
+ if any(character in _FORBIDDEN_CHARACTERS or ord(character) < 0x20 for character in segment):
156
+ return False
157
+
158
+ return True