Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
56368e0
chore(deps): bump tornado from 6.5.7 to 6.5.8 in /envs/wildfire_env
dependabot[bot] Sep 1, 2026
541c023
chore(deps): bump nltk from 3.9.4 to 3.10.3 in /envs/textarena_env
dependabot[bot] Sep 1, 2026
7321d73
chore(deps): bump pypdf from 6.14.2 to 6.16.1 in /envs/repl_env
dependabot[bot] Sep 1, 2026
10d3647
chore(deps): require patched nltk and pypdf
cursoragent Sep 9, 2026
4d2fc5b
chore(deps): defer nltk update with proxy regression
cursoragent Sep 9, 2026
4b4a5c2
Merge remote-tracking branch 'origin/main' into cursor/dependabot-env…
cursoragent Sep 9, 2026
ebd608a
Merge branch 'main' into cursor/dependabot-envs-safe-2026-09-09
burtenshaw Sep 9, 2026
287c715
Merge remote-tracking branch 'origin/main' into cursor/dependabot-env…
cursoragent Sep 15, 2026
0d68e5c
fix(discovery): reject control characters in paths
cursoragent Sep 15, 2026
b35fcea
fix(discovery): enforce schema control exclusion
cursoragent Sep 15, 2026
d6d61f4
docs(discovery): name rejected control ranges
cursoragent Sep 15, 2026
99cfaa0
Merge remote-tracking branch 'origin/main' into cursor/dependabot-env…
cursoragent Sep 15, 2026
5e3f9f9
Merge remote-tracking branch 'origin/main' into cursor/dependabot-env…
cursoragent Sep 15, 2026
440faf5
fix(client): drain dropped sockets before reconnect
cursoragent Sep 15, 2026
39e30dc
test(client): await mocked reconnect correctly
cursoragent Sep 15, 2026
94b95e3
Merge remote-tracking branch 'origin/main' into cursor/dependabot-env…
cursoragent Sep 15, 2026
2831ed5
fix(discovery): ignore relative XDG cache paths
cursoragent Sep 15, 2026
d140395
Merge remote-tracking branch 'origin/main' into cursor/fix-0.4.3-rele…
cursoragent Sep 16, 2026
7dc8031
fix(client): track cancelled disconnect handshakes
cursoragent Sep 16, 2026
210b678
Merge remote-tracking branch 'origin/cursor/fix-0.4.3-release-blocker…
cursoragent Sep 16, 2026
f336c19
fix: harden close teardown and relative HOME cache fallback
cursoragent Sep 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/source/guides/catalog-discovery.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,9 @@ JSON Schema validation is necessary but is not the whole profile contract.
The packaged schemas enforce object shape, required fields, relative-path
safety, supported literals, conditional artifact/license-evidence presence, and
exactly one orchestration interface. Other rules require semantic validation:
The relative-path profile permits printable UTF-8 (including spaces), but
rejects C0, DEL, and C1 control characters so untrusted metadata cannot forge
CLI or log lines.
Comment on lines +168 to +170

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor nit (non-blocking): this paragraph describes what the packaged schemas enforce (relative-path safety), but it's inserted immediately after "Other rules require semantic validation:" — whose trailing colon introduces the table of semantic-validation rules right below it. As written, the schema-enforced note interrupts that lead-in → table flow.

Consider either folding it into the preceding sentence ("The packaged schemas enforce object shape, required fields, relative-path safety, ...") or separating it from the "semantic validation:" clause (e.g. its own line before that sentence), so the colon flows directly into the table.


| Subject | Additional rule |
|---------|-----------------|
Expand Down
22 changes: 21 additions & 1 deletion src/openenv/auto/_discovery.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
import os
import re
import stat
import tempfile
from dataclasses import asdict, dataclass
from pathlib import Path
from typing import Any, Type
Expand Down Expand Up @@ -345,9 +346,28 @@ def _default_cache_file() -> Path:
shared, world-writable temporary directory. A fixed path under the shared temp dir
lets another local user pre-create the cache file and redirect discovery to
attacker-controlled import paths (`import_module` on a cached `client_module_path`).
Per the XDG Base Directory specification, relative `XDG_CACHE_HOME` values are
ignored so an untrusted working tree cannot supply a victim-owned cache file.
Relative `HOME` values are likewise rejected: `Path.home()` must not be
resolved against the current working directory.
"""
base = os.environ.get("XDG_CACHE_HOME")
root = Path(base) if base else Path.home() / ".cache"
if base and Path(base).is_absolute():
root = Path(base)
else:
home = Path.home()
if home.is_absolute():
root = home / ".cache"
else:
# Keep the fallback absolute and uid-scoped so a shared temp root
# cannot be turned into a fixed, cross-user planting target.
uid = os.getuid() if hasattr(os, "getuid") else os.getpid()
root = Path(tempfile.gettempdir()) / f"openenv-{uid}-cache"
if not root.is_absolute():

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Temp fallback can use checkout cache

High Severity

The new relative-HOME fallback builds the discovery cache path from tempfile.gettempdir(), which turns a relative TMPDIR into a cwd-absolute path and can fall back to the working directory. An untrusted checkout that sets both HOME and TMPDIR can still supply a victim-owned planted cache, and later import_module follows that cached client_module_path. The is_absolute() guard does not catch this because gettempdir() already returns an absolute path.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f336c19. Configure here.

raise RuntimeError(
"Cannot resolve an absolute discovery cache directory when "
"XDG_CACHE_HOME and HOME are both missing or relative"
)
return root / "openenv" / "discovery_cache.json"


Expand Down
117 changes: 79 additions & 38 deletions src/openenv/core/env_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,15 @@ async def _best_effort_close(ws: ClientConnection) -> None:
pass # Best effort


async def _best_effort_disconnect(ws: ClientConnection) -> None:
"""Notify the server, then close the socket without propagating failures."""
try:
await ws.send(json.dumps({"type": "close"}))
except (Exception, asyncio.CancelledError):
pass # Best effort
await _best_effort_close(ws)


class EnvClient(ABC, Generic[ActT, ObsT, StateT]):
"""
Async environment client for persistent sessions.
Expand Down Expand Up @@ -538,6 +547,13 @@ async def _connect_async(self) -> "EnvClient":
self._ws = None
self._ws_loop = None

# A timed-out request drops its socket immediately but closes it in the
# background so the timeout itself remains prompt. Wait for that close
# before opening a replacement: the old server-side session continues
# occupying a capacity slot until the close handshake finishes, and
# many environments allow only one session.
await self._drain_pending_close_tasks()

try:
self._start_provider_if_needed()
except Exception:
Expand Down Expand Up @@ -573,24 +589,49 @@ async def _connect_async(self) -> "EnvClient":
def disconnect(self) -> Any:
return self._dispatch(self._disconnect_async)

def _schedule_socket_close(
self, ws: ClientConnection, *, notify_server: bool = False
) -> asyncio.Task[None]:
"""Schedule and track a socket close on the current event loop."""
close = _best_effort_disconnect(ws) if notify_server else _best_effort_close(ws)
close_task = asyncio.create_task(close)
self._pending_close_tasks.add(close_task)
close_task.add_done_callback(self._pending_close_tasks.discard)
return close_task

async def _disconnect_async(self) -> None:
"""Close the WebSocket connection."""
if self._ws is not None:
ws = self._ws
ws_loop = self._ws_loop
same_loop = ws_loop is asyncio.get_running_loop()
try:
if same_loop:
await ws.send(json.dumps({"type": "close"}))
except Exception:
pass # Best effort
try:
if same_loop:
await ws.close()
except Exception:
pass
# Detach first so cancellation during the close handshake cannot
# leave a stale socket cached for a later operation.
self._ws = None
self._ws_loop = None
Comment thread
cursor[bot] marked this conversation as resolved.
same_loop = ws_loop is asyncio.get_running_loop()
if same_loop:
# Track the detached socket before awaiting anything. If this
# caller is cancelled, the shielded task keeps closing and a
# later reconnect drains it before opening a replacement.
close_task = self._schedule_socket_close(ws, notify_server=True)
await asyncio.shield(close_task)

async def _drain_pending_close_tasks(self) -> None:
"""Wait for background socket closes owned by the current event loop.

Shielding keeps cancellation of the caller from cancelling the close
tasks themselves. This matters both before reconnecting, when the old
server session must release its capacity slot, and during explicit
client shutdown.
"""
loop = asyncio.get_running_loop()
tasks = [
task
for task in tuple(self._pending_close_tasks)
if not task.done() and task.get_loop() is loop
]
if tasks:
await asyncio.shield(asyncio.gather(*tasks, return_exceptions=True))

async def _ensure_connected(self) -> None:
"""Ensure WebSocket connection is established on the current loop.
Expand Down Expand Up @@ -641,9 +682,7 @@ async def _receive(self) -> Dict[str, Any]:
# would actually block for up to 10s before its deadline was
# honored. Scheduling it lets the exception propagate
# immediately while the close still happens in the background.
close_task = asyncio.ensure_future(_best_effort_close(ws))
self._pending_close_tasks.add(close_task)
close_task.add_done_callback(self._pending_close_tasks.discard)
self._schedule_socket_close(ws)
raise
return json.loads(raw)

Expand Down Expand Up @@ -957,34 +996,36 @@ async def _close_async(self) -> None:
If this client was created via from_docker_image() or from_env(),
this will also stop and remove the associated container/process.
"""
for child in list(self._child_clients):
with suppress(Exception):
await child.close()
self._child_clients.clear()

try:
# Wait out any backgrounded closes from a dropped socket (see
# `_receive()` / `_best_effort_close`) so a real close() call still
# sees the handshake through. SyncEnvClient.close() waits for
# `_close_async()` before stopping its loop, so the relevant risk
# is async-context cancellation of close itself — not `_stop_loop()`.
# Keep this gather inside the provider-teardown try/finally so a
# cancelled close cannot skip container/process cleanup.
if self._pending_close_tasks:
await asyncio.gather(*self._pending_close_tasks, return_exceptions=True)
await self._disconnect_async()
for child in list(self._child_clients):
with suppress(Exception):
await child.close()
finally:
# Parent teardown must run even when a child close is cancelled.
self._child_clients.clear()

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cancel drops remaining child sessions

Medium Severity

Cancelling _close_async during one child.close() now clears _child_clients before later sessions are closed. Those children are forgotten, so a retry cannot finish their WebSocket teardown and they can keep occupying server capacity. The new test only covers a single child.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit f336c19. Configure here.

try:
if self._provider is not None:
# Handle both ContainerProvider and RuntimeProvider
if hasattr(self._provider, "stop_container"):
self._provider.stop_container()
elif hasattr(self._provider, "stop"):
self._provider.stop()
try:
# A real close waits out backgrounded closes, but shield them
# from cancellation so their socket handshakes aren't
# abandoned midway.
await self._drain_pending_close_tasks()
finally:
# Run even when pending-close draining is cancelled. A client
# may already have reconnected, and that current socket must
# not remain cached or open during teardown.
await self._disconnect_async()
finally:
if self._start_provider_on_connect:
self._base_url = None
self._ws_url = None
try:
if self._provider is not None:
# Handle both ContainerProvider and RuntimeProvider
if hasattr(self._provider, "stop_container"):
self._provider.stop_container()
elif hasattr(self._provider, "stop"):
self._provider.stop()
finally:
if self._start_provider_on_connect:
self._base_url = None
self._ws_url = None

def _stop_provider_best_effort(self) -> None:
"""Stop the underlying provider directly, ignoring any errors.
Expand Down
10 changes: 8 additions & 2 deletions src/openenv/discovery/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,10 @@ def relative_path(value: str) -> str:
return value
if (
not value
or "\x00" in value
or any(
ord(character) < 0x20 or 0x7F <= ord(character) <= 0x9F
for character in value
)
or "\\" in value
or PurePosixPath(value).is_absolute()
or any(part in ("", ".", "..") for part in value.split("/"))
Expand All @@ -64,8 +67,10 @@ def relative_path(value: str) -> str:

# Positive components exclude "." and ".." without lookaround, which some
# JSON Schema regex engines do not support.
_CONTROL_CHARACTER_PATTERN = r"[\x00-\x1f\x7f-\x9f]"
_PATH_COMPONENT_PATTERN = (
r"(?:[^./\\\x00]|\.[^./\\\x00]|\.\.[^./\\\x00]|\.\.\.)[^/\\\x00]*"
r"(?:[^./\\\x00-\x1f\x7f-\x9f]|\.[^./\\\x00-\x1f\x7f-\x9f]"
r"|\.\.[^./\\\x00-\x1f\x7f-\x9f]|\.\.\.)[^/\\\x00-\x1f\x7f-\x9f]*"
)
RelativePath = Annotated[
NonEmpty,
Expand All @@ -79,6 +84,7 @@ def relative_path(value: str) -> str:
rf"(?:/{_PATH_COMPONENT_PATTERN})*)$"
),
},
{"not": {"pattern": _CONTROL_CHARACTER_PATTERN}},
]
}
),
Expand Down
35 changes: 30 additions & 5 deletions src/openenv/discovery/schemas/0.1-draft/catalog.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,12 @@
"path": {
"allOf": [
{
"pattern": "^(?:\\.|(?:[^./\\\\\\x00]|\\.[^./\\\\\\x00]|\\.\\.[^./\\\\\\x00]|\\.\\.\\.)[^/\\\\\\x00]*(?:/(?:[^./\\\\\\x00]|\\.[^./\\\\\\x00]|\\.\\.[^./\\\\\\x00]|\\.\\.\\.)[^/\\\\\\x00]*)*)$"
"pattern": "^(?:\\.|(?:[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.\\.)[^/\\\\\\x00-\\x1f\\x7f-\\x9f]*(?:/(?:[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.\\.)[^/\\\\\\x00-\\x1f\\x7f-\\x9f]*)*)$"
},
{
"not": {
"pattern": "[\\x00-\\x1f\\x7f-\\x9f]"
}
}
],
"maxLength": 8192,
Expand Down Expand Up @@ -405,7 +410,12 @@
"path": {
"allOf": [
{
"pattern": "^(?:\\.|(?:[^./\\\\\\x00]|\\.[^./\\\\\\x00]|\\.\\.[^./\\\\\\x00]|\\.\\.\\.)[^/\\\\\\x00]*(?:/(?:[^./\\\\\\x00]|\\.[^./\\\\\\x00]|\\.\\.[^./\\\\\\x00]|\\.\\.\\.)[^/\\\\\\x00]*)*)$"
"pattern": "^(?:\\.|(?:[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.\\.)[^/\\\\\\x00-\\x1f\\x7f-\\x9f]*(?:/(?:[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.\\.)[^/\\\\\\x00-\\x1f\\x7f-\\x9f]*)*)$"
},
{
"not": {
"pattern": "[\\x00-\\x1f\\x7f-\\x9f]"
}
}
],
"maxLength": 8192,
Expand Down Expand Up @@ -472,7 +482,12 @@
"path": {
"allOf": [
{
"pattern": "^(?:\\.|(?:[^./\\\\\\x00]|\\.[^./\\\\\\x00]|\\.\\.[^./\\\\\\x00]|\\.\\.\\.)[^/\\\\\\x00]*(?:/(?:[^./\\\\\\x00]|\\.[^./\\\\\\x00]|\\.\\.[^./\\\\\\x00]|\\.\\.\\.)[^/\\\\\\x00]*)*)$"
"pattern": "^(?:\\.|(?:[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.\\.)[^/\\\\\\x00-\\x1f\\x7f-\\x9f]*(?:/(?:[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.\\.)[^/\\\\\\x00-\\x1f\\x7f-\\x9f]*)*)$"
},
{
"not": {
"pattern": "[\\x00-\\x1f\\x7f-\\x9f]"
}
}
],
"maxLength": 8192,
Expand Down Expand Up @@ -510,7 +525,12 @@
"items": {
"allOf": [
{
"pattern": "^(?:\\.|(?:[^./\\\\\\x00]|\\.[^./\\\\\\x00]|\\.\\.[^./\\\\\\x00]|\\.\\.\\.)[^/\\\\\\x00]*(?:/(?:[^./\\\\\\x00]|\\.[^./\\\\\\x00]|\\.\\.[^./\\\\\\x00]|\\.\\.\\.)[^/\\\\\\x00]*)*)$"
"pattern": "^(?:\\.|(?:[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.\\.)[^/\\\\\\x00-\\x1f\\x7f-\\x9f]*(?:/(?:[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.\\.)[^/\\\\\\x00-\\x1f\\x7f-\\x9f]*)*)$"
},
{
"not": {
"pattern": "[\\x00-\\x1f\\x7f-\\x9f]"
}
}
],
"maxLength": 8192,
Expand All @@ -524,7 +544,12 @@
"root": {
"allOf": [
{
"pattern": "^(?:\\.|(?:[^./\\\\\\x00]|\\.[^./\\\\\\x00]|\\.\\.[^./\\\\\\x00]|\\.\\.\\.)[^/\\\\\\x00]*(?:/(?:[^./\\\\\\x00]|\\.[^./\\\\\\x00]|\\.\\.[^./\\\\\\x00]|\\.\\.\\.)[^/\\\\\\x00]*)*)$"
"pattern": "^(?:\\.|(?:[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.\\.)[^/\\\\\\x00-\\x1f\\x7f-\\x9f]*(?:/(?:[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.\\.)[^/\\\\\\x00-\\x1f\\x7f-\\x9f]*)*)$"
},
{
"not": {
"pattern": "[\\x00-\\x1f\\x7f-\\x9f]"
}
}
],
"maxLength": 8192,
Expand Down
14 changes: 12 additions & 2 deletions src/openenv/discovery/schemas/0.1-draft/declaration.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@
"source": {
"allOf": [
{
"pattern": "^(?:\\.|(?:[^./\\\\\\x00]|\\.[^./\\\\\\x00]|\\.\\.[^./\\\\\\x00]|\\.\\.\\.)[^/\\\\\\x00]*(?:/(?:[^./\\\\\\x00]|\\.[^./\\\\\\x00]|\\.\\.[^./\\\\\\x00]|\\.\\.\\.)[^/\\\\\\x00]*)*)$"
"pattern": "^(?:\\.|(?:[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.\\.)[^/\\\\\\x00-\\x1f\\x7f-\\x9f]*(?:/(?:[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.\\.)[^/\\\\\\x00-\\x1f\\x7f-\\x9f]*)*)$"
},
{
"not": {
"pattern": "[\\x00-\\x1f\\x7f-\\x9f]"
}
}
],
"maxLength": 8192,
Expand Down Expand Up @@ -104,7 +109,12 @@
{
"allOf": [
{
"pattern": "^(?:\\.|(?:[^./\\\\\\x00]|\\.[^./\\\\\\x00]|\\.\\.[^./\\\\\\x00]|\\.\\.\\.)[^/\\\\\\x00]*(?:/(?:[^./\\\\\\x00]|\\.[^./\\\\\\x00]|\\.\\.[^./\\\\\\x00]|\\.\\.\\.)[^/\\\\\\x00]*)*)$"
"pattern": "^(?:\\.|(?:[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.\\.)[^/\\\\\\x00-\\x1f\\x7f-\\x9f]*(?:/(?:[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.\\.)[^/\\\\\\x00-\\x1f\\x7f-\\x9f]*)*)$"
},
{
"not": {
"pattern": "[\\x00-\\x1f\\x7f-\\x9f]"
}
}
],
"maxLength": 8192,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,12 @@
"path": {
"allOf": [
{
"pattern": "^(?:\\.|(?:[^./\\\\\\x00]|\\.[^./\\\\\\x00]|\\.\\.[^./\\\\\\x00]|\\.\\.\\.)[^/\\\\\\x00]*(?:/(?:[^./\\\\\\x00]|\\.[^./\\\\\\x00]|\\.\\.[^./\\\\\\x00]|\\.\\.\\.)[^/\\\\\\x00]*)*)$"
"pattern": "^(?:\\.|(?:[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.\\.)[^/\\\\\\x00-\\x1f\\x7f-\\x9f]*(?:/(?:[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.\\.)[^/\\\\\\x00-\\x1f\\x7f-\\x9f]*)*)$"
},
{
"not": {
"pattern": "[\\x00-\\x1f\\x7f-\\x9f]"
}
}
],
"maxLength": 8192,
Expand Down Expand Up @@ -97,7 +102,12 @@
"path": {
"allOf": [
{
"pattern": "^(?:\\.|(?:[^./\\\\\\x00]|\\.[^./\\\\\\x00]|\\.\\.[^./\\\\\\x00]|\\.\\.\\.)[^/\\\\\\x00]*(?:/(?:[^./\\\\\\x00]|\\.[^./\\\\\\x00]|\\.\\.[^./\\\\\\x00]|\\.\\.\\.)[^/\\\\\\x00]*)*)$"
"pattern": "^(?:\\.|(?:[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.\\.)[^/\\\\\\x00-\\x1f\\x7f-\\x9f]*(?:/(?:[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.[^./\\\\\\x00-\\x1f\\x7f-\\x9f]|\\.\\.\\.)[^/\\\\\\x00-\\x1f\\x7f-\\x9f]*)*)$"
},
{
"not": {
"pattern": "[\\x00-\\x1f\\x7f-\\x9f]"
}
}
],
"maxLength": 8192,
Expand Down
Loading
Loading