forgexa-cli 1.59.0__tar.gz → 1.59.2__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 (29) hide show
  1. {forgexa_cli-1.59.0 → forgexa_cli-1.59.2}/PKG-INFO +1 -1
  2. {forgexa_cli-1.59.0 → forgexa_cli-1.59.2}/forgexa_cli/__init__.py +1 -1
  3. {forgexa_cli-1.59.0 → forgexa_cli-1.59.2}/forgexa_cli/_local_bind.py +22 -46
  4. {forgexa_cli-1.59.0 → forgexa_cli-1.59.2}/forgexa_cli/agent_core.py +41 -0
  5. {forgexa_cli-1.59.0 → forgexa_cli-1.59.2}/forgexa_cli/daemon.py +62 -8
  6. {forgexa_cli-1.59.0 → forgexa_cli-1.59.2}/forgexa_cli/main.py +2 -2
  7. {forgexa_cli-1.59.0 → forgexa_cli-1.59.2}/forgexa_cli.egg-info/PKG-INFO +1 -1
  8. {forgexa_cli-1.59.0 → forgexa_cli-1.59.2}/pyproject.toml +1 -1
  9. {forgexa_cli-1.59.0 → forgexa_cli-1.59.2}/tests/test_local_bind_commands.py +40 -3
  10. {forgexa_cli-1.59.0 → forgexa_cli-1.59.2}/README.md +0 -0
  11. {forgexa_cli-1.59.0 → forgexa_cli-1.59.2}/forgexa_cli/_build_config.py +0 -0
  12. {forgexa_cli-1.59.0 → forgexa_cli-1.59.2}/forgexa_cli/autoupgrade.py +0 -0
  13. {forgexa_cli-1.59.0 → forgexa_cli-1.59.2}/forgexa_cli/py.typed +0 -0
  14. {forgexa_cli-1.59.0 → forgexa_cli-1.59.2}/forgexa_cli/runtime_evidence.py +0 -0
  15. {forgexa_cli-1.59.0 → forgexa_cli-1.59.2}/forgexa_cli/zcode_protocol.py +0 -0
  16. {forgexa_cli-1.59.0 → forgexa_cli-1.59.2}/forgexa_cli.egg-info/SOURCES.txt +0 -0
  17. {forgexa_cli-1.59.0 → forgexa_cli-1.59.2}/forgexa_cli.egg-info/dependency_links.txt +0 -0
  18. {forgexa_cli-1.59.0 → forgexa_cli-1.59.2}/forgexa_cli.egg-info/entry_points.txt +0 -0
  19. {forgexa_cli-1.59.0 → forgexa_cli-1.59.2}/forgexa_cli.egg-info/requires.txt +0 -0
  20. {forgexa_cli-1.59.0 → forgexa_cli-1.59.2}/forgexa_cli.egg-info/top_level.txt +0 -0
  21. {forgexa_cli-1.59.0 → forgexa_cli-1.59.2}/setup.cfg +0 -0
  22. {forgexa_cli-1.59.0 → forgexa_cli-1.59.2}/tests/test_auth_and_runtime_commands.py +0 -0
  23. {forgexa_cli-1.59.0 → forgexa_cli-1.59.2}/tests/test_autoupgrade.py +0 -0
  24. {forgexa_cli-1.59.0 → forgexa_cli-1.59.2}/tests/test_check_command.py +0 -0
  25. {forgexa_cli-1.59.0 → forgexa_cli-1.59.2}/tests/test_expiry_warnings_and_revoke.py +0 -0
  26. {forgexa_cli-1.59.0 → forgexa_cli-1.59.2}/tests/test_runtime_credentials.py +0 -0
  27. {forgexa_cli-1.59.0 → forgexa_cli-1.59.2}/tests/test_session_credentials.py +0 -0
  28. {forgexa_cli-1.59.0 → forgexa_cli-1.59.2}/tests/test_silent_install.py +0 -0
  29. {forgexa_cli-1.59.0 → forgexa_cli-1.59.2}/tests/test_upgrade_observability.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.59.0
3
+ Version: 1.59.2
4
4
  Summary: Forgexa CLI — command-line client and AI agent runtime for the Forgexa platform
5
5
  Author-email: Jason Sun <dev.winds@gmail.com>
6
6
  License-Expression: MIT
@@ -1,2 +1,2 @@
1
1
  """forgexa-cli — Forgexa command-line client."""
2
- __version__ = "1.59.0"
2
+ __version__ = "1.59.2"
@@ -47,58 +47,34 @@ def save_local_workspaces_mapping(mapping: dict) -> None:
47
47
 
48
48
 
49
49
  @contextlib.contextmanager
50
- def local_workspaces_mapping_lock() -> Iterator[None]:
51
- """Advisory cross-process lock for read-modify-write cycles on the
52
- mapping file (multi-workspace parallel design §6.1/§6.3). Shares the
53
- `local-workspaces.json.lock` path with the Desktop Rust writer (fd-lock
54
- crate); without it, a CLI bind-local and a Desktop link running
55
- concurrently could each drop the other's just-written entry. Best-effort:
56
- platforms without fcntl/msvcrt degrade to lock-free (the atomic rename
57
- still prevents torn files).
58
- """
50
+ def local_workspaces_mapping_lock(workspace_path: str | Path | None = None) -> Iterator[None]:
51
+ """Serialize mapping writes; lock or parse failures never degrade to unlocked writes."""
52
+ from forgexa_cli.agent_core import acquire_file_lock, release_file_lock
53
+
59
54
  path = local_workspaces_mapping_path()
60
55
  lock_path = path.with_name(path.name + ".lock")
56
+ lock_file = acquire_file_lock(lock_path, blocking=True)
57
+ if lock_file is None:
58
+ raise RuntimeError("Could not acquire the local workspace mapping write lock")
59
+ workspace_lock = None
61
60
  try:
62
- path.parent.mkdir(parents=True, exist_ok=True)
63
- lock_file = open(lock_path, "a+")
64
- except OSError:
65
- yield
66
- return
67
-
68
- lock_kind = None
69
- try:
70
- try:
71
- import fcntl
72
-
73
- fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX)
74
- lock_kind = "fcntl"
75
- except ImportError:
61
+ if path.exists():
76
62
  try:
77
- import msvcrt
78
-
79
- lock_file.seek(0)
80
- msvcrt.locking(lock_file.fileno(), msvcrt.LK_LOCK, 1)
81
- lock_kind = "msvcrt"
82
- except (ImportError, OSError):
83
- pass
63
+ mapping = json.loads(path.read_text(encoding="utf-8"))
64
+ if not isinstance(mapping, dict):
65
+ raise ValueError("Mapping must be a JSON object")
66
+ except (OSError, ValueError) as error:
67
+ raise RuntimeError("Local workspace mapping is unreadable; recover it before modifying bindings") from error
68
+ if workspace_path is not None:
69
+ from forgexa_cli.agent_core import acquire_workspace_write_lock
70
+
71
+ workspace_lock = acquire_workspace_write_lock(workspace_path)
72
+ if workspace_lock is None:
73
+ raise RuntimeError("A task or agent is writing this workspace; wait before changing its mapping")
84
74
  yield
85
75
  finally:
86
- if lock_kind == "fcntl":
87
- try:
88
- import fcntl
89
-
90
- fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN)
91
- except (ImportError, OSError):
92
- pass
93
- elif lock_kind == "msvcrt":
94
- try:
95
- import msvcrt
96
-
97
- lock_file.seek(0)
98
- msvcrt.locking(lock_file.fileno(), msvcrt.LK_UNLCK, 1)
99
- except (ImportError, OSError):
100
- pass
101
- lock_file.close()
76
+ release_file_lock(workspace_lock)
77
+ release_file_lock(lock_file)
102
78
 
103
79
 
104
80
  _SSH_SCHEME_RE = re.compile(r"^ssh://(?:[^@/]+@)?([^/:]+)(?::\d+)?/(.+?)(?:\.git)?/?$")
@@ -1125,8 +1125,49 @@ def acquire_file_lock(
1125
1125
  return lock_file
1126
1126
 
1127
1127
 
1128
+ def workspace_write_lock_path(workspace_path: str | Path) -> Path:
1129
+ """Resolve one physical worktree's shared lock without uploading its path."""
1130
+ root = Path(workspace_path).expanduser().resolve()
1131
+ try:
1132
+ result = subprocess.run(
1133
+ ["git", "rev-parse", "--absolute-git-dir"],
1134
+ cwd=str(root), capture_output=True, text=True, timeout=10,
1135
+ )
1136
+ if result.returncode == 0 and result.stdout.strip():
1137
+ return Path(result.stdout.strip()).resolve() / "forgexa-workspace.lock"
1138
+ if (root / ".git").exists():
1139
+ raise RuntimeError("Could not resolve the local workspace Git metadata")
1140
+ except (OSError, subprocess.TimeoutExpired):
1141
+ if (root / ".git").exists():
1142
+ raise RuntimeError("Could not resolve the local workspace Git metadata")
1143
+ key = hashlib.sha256(str(root).encode("utf-8")).hexdigest()
1144
+ return Path.home() / ".forgexa" / "locks" / "workspaces" / f"{key}.lock"
1145
+
1146
+
1147
+ def acquire_workspace_write_lock(
1148
+ workspace_path: str | Path, *, companion_lock_path: str | Path | None = None,
1149
+ ):
1150
+ """Acquire a physical-directory lock and an optional legacy identity lock."""
1151
+ companion = acquire_file_lock(companion_lock_path) if companion_lock_path is not None else None
1152
+ if companion_lock_path is not None and companion is None:
1153
+ return None
1154
+ try:
1155
+ physical = acquire_file_lock(workspace_write_lock_path(workspace_path))
1156
+ except (OSError, RuntimeError):
1157
+ release_file_lock(companion)
1158
+ return None
1159
+ if physical is None:
1160
+ release_file_lock(companion)
1161
+ return None
1162
+ return (companion, physical) if companion is not None else physical
1163
+
1164
+
1128
1165
  def release_file_lock(lock_file) -> None:
1129
1166
  """Release a handle returned by :func:`acquire_file_lock` (None-safe)."""
1167
+ if isinstance(lock_file, tuple):
1168
+ for handle in reversed(lock_file):
1169
+ release_file_lock(handle)
1170
+ return
1130
1171
  if lock_file is None:
1131
1172
  return
1132
1173
  try:
@@ -1196,7 +1196,7 @@ except (ImportError, ModuleNotFoundError):
1196
1196
  # DAEMON_VERSION is the protocol/logic version of the daemon code.
1197
1197
  # Kept in sync with pyproject.toml version via bump-version.sh.
1198
1198
  # CLIENT_TYPE identifies which packaging/distribution this daemon runs in.
1199
- DAEMON_VERSION = "1.59.0"
1199
+ DAEMON_VERSION = "1.59.2"
1200
1200
 
1201
1201
 
1202
1202
  def _detect_client_type() -> str:
@@ -2797,7 +2797,7 @@ def _is_sensitive_local_path(path: str) -> bool:
2797
2797
  return any(pattern.search(normalized) for pattern in _LOCAL_BIND_SENSITIVE_PATH_PATTERNS)
2798
2798
 
2799
2799
 
2800
- def _try_acquire_local_workspace_file_lock(binding_id: str):
2800
+ def _try_acquire_local_workspace_file_lock(binding_id: str, workspace_path: Path | None = None):
2801
2801
  """Non-blocking OS-level file lock keyed by binding_id.
2802
2802
 
2803
2803
  See docs/designs/forgexa-ide-design.md §6.3: the DB-side
@@ -2814,6 +2814,14 @@ def _try_acquire_local_workspace_file_lock(binding_id: str):
2814
2814
  agent_core.acquire_file_lock so the IDE agent_host sidecar shares them.
2815
2815
  """
2816
2816
  lock_dir = Path.home() / ".forgexa" / "locks"
2817
+ if workspace_path is None:
2818
+ entry = _load_local_workspaces_mapping().get(binding_id)
2819
+ if isinstance(entry, dict) and isinstance(entry.get("local_path"), str) and entry["local_path"]:
2820
+ workspace_path = Path(entry["local_path"]).expanduser()
2821
+ if workspace_path is not None:
2822
+ return agent_core.acquire_workspace_write_lock(
2823
+ workspace_path, companion_lock_path=lock_dir / f"{binding_id}.lock",
2824
+ )
2817
2825
  return agent_core.acquire_file_lock(lock_dir / f"{binding_id}.lock")
2818
2826
 
2819
2827
 
@@ -8543,6 +8551,8 @@ class ProgressReporter:
8543
8551
  timeout=10,
8544
8552
  )
8545
8553
  if 200 <= response.status_code < 300:
8554
+ if response.json().get("status") == "ignored":
8555
+ return False
8546
8556
  self._pending_output_lines.pop(task_id, None)
8547
8557
  self._last_progress_sent[task_id] = {
8548
8558
  "progress_percent": progress_percent,
@@ -12120,7 +12130,17 @@ class RuntimeDaemon:
12120
12130
  return
12121
12131
 
12122
12132
  # 2. Prepare workspace
12123
- await reporter.report_progress(task.task_id, 5, "preparing_workspace", execution_attempt_id=task.execution_attempt_id)
12133
+ authorized = await reporter.report_progress(
12134
+ task.task_id, 5, "preparing_workspace", execution_attempt_id=task.execution_attempt_id,
12135
+ )
12136
+ if is_local_bind and not authorized:
12137
+ await report_task_complete(TaskResult(
12138
+ status="failed", exit_code=-1, stdout="", stderr="",
12139
+ error="Local execution authorization is no longer current or could not be verified",
12140
+ failure_code="local_binding_unavailable",
12141
+ execution_attempt_id=task.execution_attempt_id,
12142
+ ))
12143
+ return
12124
12144
  workspace_path = await self.workspace_manager.prepare_workspace(
12125
12145
  task.project, task, runtime_id=conn.runtime_id,
12126
12146
  )
@@ -12782,11 +12802,18 @@ class RuntimeDaemon:
12782
12802
  # directory: never `git add -A` sensitive paths, never
12783
12803
  # rebase/merge/reset, fast-forward push only. See
12784
12804
  # docs/designs/forgexa-ide-design.md §6.2 item 6.
12785
- commit_result = await self.local_commit_and_push(
12786
- workspace_path,
12787
- task,
12788
- before_sha=node_before_sha,
12805
+ authorized = await reporter.report_progress(
12806
+ task.task_id, 95, "verifying_local_commit_authorization",
12807
+ execution_attempt_id=task.execution_attempt_id,
12789
12808
  )
12809
+ if not authorized:
12810
+ commit_result = {"commit_error": "Local execution authorization changed; preserving work without committing or pushing"}
12811
+ else:
12812
+ commit_result = await self.local_commit_and_push(
12813
+ workspace_path,
12814
+ task,
12815
+ before_sha=node_before_sha,
12816
+ )
12790
12817
  else:
12791
12818
  commit_result = await self._auto_commit(
12792
12819
  workspace_path,
@@ -15811,6 +15838,17 @@ class RuntimeDaemon:
15811
15838
  "then retry."
15812
15839
  )}
15813
15840
 
15841
+ if before_sha:
15842
+ try:
15843
+ await git(
15844
+ "merge-base", "--is-ancestor", before_sha, "HEAD", cwd=workspace_path,
15845
+ )
15846
+ except RuntimeError:
15847
+ return {"commit_error": (
15848
+ "Refusing to commit: the task baseline is no longer reachable "
15849
+ "from HEAD. Review the branch history yourself before retrying."
15850
+ )}
15851
+
15814
15852
  await self._discard_transient_untracked_runtime_files(workspace_path)
15815
15853
  status_output = await git("status", "--porcelain", cwd=workspace_path)
15816
15854
  dirty_paths = [line[3:] for line in status_output.splitlines() if line.strip()]
@@ -15856,6 +15894,16 @@ class RuntimeDaemon:
15856
15894
  logger.warning("Failed to build rich commit message: %s — using fallback", msg_err)
15857
15895
  commit_msg = f"{task.node_type}({task.requirement_key or task.task_id}): {display_title}"
15858
15896
 
15897
+ commit_branch = (
15898
+ await git("rev-parse", "--abbrev-ref", "HEAD", cwd=workspace_path)
15899
+ ).strip()
15900
+ if commit_branch != target_branch:
15901
+ return {"commit_error": "Refusing to commit: the task branch changed while preparing the commit"}
15902
+ if before_sha:
15903
+ try:
15904
+ await git("merge-base", "--is-ancestor", before_sha, "HEAD", cwd=workspace_path)
15905
+ except RuntimeError:
15906
+ return {"commit_error": "Refusing to commit: the task baseline changed while preparing the commit"}
15859
15907
  _git_name, _git_email = _resolve_git_author(task.project)
15860
15908
  proc = await asyncio.create_subprocess_exec(
15861
15909
  "git", "commit", "-m", commit_msg,
@@ -15938,9 +15986,15 @@ class RuntimeDaemon:
15938
15986
  f"'{target_branch}' and retry."
15939
15987
  )}
15940
15988
 
15989
+ verified_head = (await git("rev-parse", "HEAD", cwd=workspace_path)).strip()
15990
+ if before_sha:
15991
+ try:
15992
+ await git("merge-base", "--is-ancestor", before_sha, verified_head, cwd=workspace_path)
15993
+ except RuntimeError:
15994
+ return {"push_error": "Refusing to push: the task baseline is no longer reachable"}
15941
15995
  try:
15942
15996
  await git(
15943
- "push", "origin", current_branch,
15997
+ "push", "origin", f"{verified_head}:refs/heads/{target_branch}",
15944
15998
  cwd=workspace_path, timeout=settings.GIT_PUSH_TIMEOUT,
15945
15999
  )
15946
16000
  except RuntimeError as exc:
@@ -3297,7 +3297,7 @@ def cmd_project_bind_local(args: argparse.Namespace) -> None:
3297
3297
  # with no tie-breaking) can resolve to the wrong, orphaned binding_id
3298
3298
  # instead of the one just verified. The whole read-modify-write runs
3299
3299
  # under the cross-process mapping lock shared with the Desktop writer.
3300
- with local_workspaces_mapping_lock():
3300
+ with local_workspaces_mapping_lock(local_path):
3301
3301
  mapping = load_local_workspaces_mapping()
3302
3302
  mapping = {
3303
3303
  key: entry
@@ -3370,7 +3370,7 @@ def cmd_project_unbind_local(args: argparse.Namespace) -> None:
3370
3370
  _delete(f"/projects/{args.project}/local-workspace-bindings/{binding_id}")
3371
3371
  from forgexa_cli._local_bind import local_workspaces_mapping_lock
3372
3372
 
3373
- with local_workspaces_mapping_lock(): # re-read under the cross-process lock
3373
+ with local_workspaces_mapping_lock(entry.get("local_path")): # re-read under the cross-process lock
3374
3374
  mapping = load_local_workspaces_mapping()
3375
3375
  mapping.pop(binding_id, None)
3376
3376
  save_local_workspaces_mapping(mapping)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: forgexa-cli
3
- Version: 1.59.0
3
+ Version: 1.59.2
4
4
  Summary: Forgexa CLI — command-line client and AI agent runtime for the Forgexa platform
5
5
  Author-email: Jason Sun <dev.winds@gmail.com>
6
6
  License-Expression: MIT
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "forgexa-cli"
3
- version = "1.59.0"
3
+ version = "1.59.2"
4
4
  description = "Forgexa CLI — command-line client and AI agent runtime for the Forgexa platform"
5
5
  requires-python = ">=3.9"
6
6
  license = "MIT"
@@ -24,6 +24,41 @@ def _init_git_repo_with_remote(path: Path, remote_url: str = "https://github.com
24
24
  subprocess.run(["git", "remote", "add", "origin", remote_url], cwd=path, check=True)
25
25
 
26
26
 
27
+ def test_mapping_write_lock_rejects_corrupt_mapping(tmp_path, monkeypatch):
28
+ from forgexa_cli._local_bind import local_workspaces_mapping_lock
29
+
30
+ monkeypatch.setenv("HOME", str(tmp_path))
31
+ path = tmp_path / ".forgexa" / "local-workspaces.json"
32
+ path.parent.mkdir()
33
+ path.write_text("not valid json", encoding="utf-8")
34
+
35
+ with pytest.raises(RuntimeError):
36
+ with local_workspaces_mapping_lock():
37
+ pytest.fail("Corrupt mapping must not enter a write transaction")
38
+ assert path.read_text(encoding="utf-8") == "not valid json"
39
+
40
+
41
+ def test_mapping_other_workspace_remains_mutable_while_one_is_busy(tmp_path, monkeypatch):
42
+ from forgexa_cli._local_bind import local_workspaces_mapping_lock
43
+ from forgexa_cli.agent_core import acquire_workspace_write_lock, release_file_lock
44
+
45
+ monkeypatch.setenv("HOME", str(tmp_path))
46
+ busy_repo = tmp_path / "busy"
47
+ free_repo = tmp_path / "free"
48
+ _init_git_repo_with_remote(busy_repo)
49
+ _init_git_repo_with_remote(free_repo)
50
+ path = tmp_path / ".forgexa" / "local-workspaces.json"
51
+ path.parent.mkdir()
52
+ path.write_text(json.dumps({"old": {"local_path": str(busy_repo)}}), encoding="utf-8")
53
+ writer = acquire_workspace_write_lock(busy_repo)
54
+ try:
55
+ assert writer is not None
56
+ with local_workspaces_mapping_lock(free_repo):
57
+ assert path.exists()
58
+ finally:
59
+ release_file_lock(writer)
60
+
61
+
27
62
  def test_bind_local_happy_path(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
28
63
  monkeypatch.setenv("HOME", str(tmp_path))
29
64
  repo = tmp_path / "my-real-project"
@@ -55,7 +90,7 @@ def test_bind_local_happy_path(tmp_path: Path, monkeypatch: pytest.MonkeyPatch)
55
90
  args = SimpleNamespace(project="project-1", path=str(repo), runtime=None)
56
91
  main.cmd_project_bind_local(args)
57
92
 
58
- assert posts[0] == ("/projects/project-1/local-workspace-bindings", {"runtime_id": "runtime-1"})
93
+ assert posts[0] == ("/projects/project-1/local-workspace-bindings", {"runtime_id": "runtime-1", "multi_binding_capable": True})
59
94
  assert posts[1] == ("/projects/project-1/local-workspace-bindings/binding-1/verify", {})
60
95
 
61
96
  mapping = json.loads((tmp_path / ".forgexa" / "local-workspaces.json").read_text())
@@ -66,6 +101,7 @@ def test_bind_local_happy_path(tmp_path: Path, monkeypatch: pytest.MonkeyPatch)
66
101
  "runtime_id": "runtime-1",
67
102
  "repo_identity": "github.com/acme/widgets",
68
103
  "local_path": str(repo.resolve()),
104
+ "share_agent_output": True,
69
105
  },
70
106
  }
71
107
 
@@ -124,6 +160,7 @@ def test_bind_local_replaces_stale_entry_for_same_local_path(tmp_path: Path, mon
124
160
  "runtime_id": "runtime-new",
125
161
  "repo_identity": "github.com/acme/widgets",
126
162
  "local_path": str(repo.resolve()),
163
+ "share_agent_output": True,
127
164
  },
128
165
  }
129
166
 
@@ -184,7 +221,7 @@ def test_bind_local_rejects_project_repository_mismatch_before_writing_mapping(
184
221
  main.cmd_project_bind_local(args)
185
222
 
186
223
  assert posts == [
187
- ("/projects/project-1/local-workspace-bindings", {"runtime_id": "runtime-1"}),
224
+ ("/projects/project-1/local-workspace-bindings", {"runtime_id": "runtime-1", "multi_binding_capable": True}),
188
225
  ]
189
226
  assert deletes == ["/projects/project-1/local-workspace-bindings/binding-1"]
190
227
  assert not (tmp_path / ".forgexa" / "local-workspaces.json").exists()
@@ -228,7 +265,7 @@ def test_bind_local_uses_runtime_prefix_to_disambiguate(
228
265
 
229
266
  def fake_post(path: str, data=None):
230
267
  if path.endswith("/local-workspace-bindings"):
231
- assert data == {"runtime_id": "runtime-2"}
268
+ assert data == {"runtime_id": "runtime-2", "multi_binding_capable": True}
232
269
  return {"id": "binding-1", "status": "pending"}
233
270
  if path.endswith("/verify"):
234
271
  return {"id": "binding-1", "status": "pending"}
File without changes
File without changes