@agentsdock/server 1.0.8-beta.2 → 1.0.8-beta.5

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentsdock/server",
3
- "version": "1.0.8-beta.2",
3
+ "version": "1.0.8-beta.5",
4
4
  "description": "AgentsDock's self-hosted Python server and explicit installation CLI",
5
5
  "license": "Apache-2.0",
6
6
  "repository": {
package/server/VERSION CHANGED
@@ -1 +1 @@
1
- 1.0.8-beta.2
1
+ 1.0.8-beta.5
package/server/install.sh CHANGED
@@ -3055,21 +3055,37 @@ install_lock_control() {
3055
3055
  local action="$1"
3056
3056
  local expected_device="${2:-0}"
3057
3057
  local expected_inode="${3:-0}"
3058
+ local wait_for_prepared="false"
3059
+ # Old managed preparers do not pass a managed-update ID. The existing pinned
3060
+ # preparation/activation modes may serialize briefly; this grants no update
3061
+ # authority and leaves ordinary installation and recovery fail-fast.
3062
+ if [[ "$action" == "acquire" && "${RECOVER_ONLY:-false}" != "true" \
3063
+ && "${RECOVER_UNARMED_ONLY:-false}" != "true" \
3064
+ && ( "${PREPARE_ONLY:-false}" == "true" || -n "${ACTIVATE_PREPARED:-}" ) \
3065
+ && "${PREPARED_ARCHIVE_SHA256:-}" =~ ^[0-9a-f]{64}$ \
3066
+ && "${EXPECTED_API_CONTRACT:-}" =~ ^[1-9][0-9]*$ \
3067
+ && -n "${RELEASE_VERSION:-}" ]]; then
3068
+ wait_for_prepared="true"
3069
+ fi
3058
3070
  local python_path=""
3059
3071
  python_path="$(install_lock_python)" || return 1
3060
3072
  "$python_path" - \
3061
- "$action" "$INSTALL_ROOT" "$$" "$expected_device" "$expected_inode" <<'PY'
3073
+ "$action" "$INSTALL_ROOT" "$$" "$expected_device" "$expected_inode" "$wait_for_prepared" <<'PY'
3062
3074
  import errno
3063
3075
  import os
3064
3076
  from pathlib import Path
3065
3077
  import secrets
3078
+ import signal
3066
3079
  import stat
3067
3080
  import sys
3081
+ import time
3068
3082
 
3069
- action, raw_root, raw_pid, raw_device, raw_inode = sys.argv[1:]
3083
+ action, raw_root, raw_pid, raw_device, raw_inode, raw_wait = sys.argv[1:]
3070
3084
  root = Path(os.path.abspath(raw_root))
3071
3085
  pid = int(raw_pid)
3072
3086
  lock = root / ".install-lock"
3087
+ PREPARED_LOCK_WAIT_SECONDS = 30.0
3088
+ wait_for_prepared = action == "acquire" and raw_wait == "true"
3073
3089
 
3074
3090
  root_info = root.lstat()
3075
3091
  if (
@@ -3145,7 +3161,56 @@ def read_owner(directory_descriptor: int) -> tuple[int, tuple[int, int]]:
3145
3161
  return int(value), (int(info.st_dev), int(info.st_ino))
3146
3162
 
3147
3163
 
3164
+ def check_wait_root() -> None:
3165
+ current = root.lstat()
3166
+ if ((current.st_dev, current.st_ino) != (root_info.st_dev, root_info.st_ino)
3167
+ or not stat.S_ISDIR(current.st_mode) or current.st_uid != os.geteuid()
3168
+ or stat.S_IMODE(current.st_mode) & 0o022):
3169
+ raise PermissionError("AgentsServer install root changed while waiting")
3170
+
3171
+
3172
+ def same_wait_owner(directory_descriptor, directory_info, owner_pid, owner_identity) -> bool:
3173
+ check_wait_root()
3174
+ current = lock.lstat()
3175
+ if ((current.st_dev, current.st_ino) != (directory_info.st_dev, directory_info.st_ino)):
3176
+ return False
3177
+ if (not stat.S_ISDIR(current.st_mode) or current.st_uid != os.geteuid()
3178
+ or stat.S_IMODE(current.st_mode) & 0o077):
3179
+ raise PermissionError("AgentsServer install lock directory is unsafe")
3180
+ return read_owner(directory_descriptor) == (owner_pid, owner_identity)
3181
+
3182
+
3183
+ def cleanup_wait_candidate(path, expected) -> None:
3184
+ # Cancellation while waiting may remove only our still-unpublished draft.
3185
+ # An acquired-but-unreported lock is left for ordinary dead-owner reaping.
3186
+ try:
3187
+ check_wait_root()
3188
+ descriptor = os.open(path, os.O_RDONLY | os.O_NOFOLLOW | os.O_DIRECTORY)
3189
+ try:
3190
+ current = os.fstat(descriptor)
3191
+ linked = path.lstat()
3192
+ identity = (expected.st_dev, expected.st_ino)
3193
+ if ((current.st_dev, current.st_ino) != identity
3194
+ or (linked.st_dev, linked.st_ino) != identity):
3195
+ return
3196
+ owner, owner_identity = read_owner(descriptor)
3197
+ if owner != pid or read_owner(descriptor) != (owner, owner_identity):
3198
+ return
3199
+ os.unlink("pid", dir_fd=descriptor)
3200
+ os.fsync(descriptor)
3201
+ linked = path.lstat()
3202
+ if (linked.st_dev, linked.st_ino) == identity:
3203
+ path.rmdir()
3204
+ finally:
3205
+ os.close(descriptor)
3206
+ except (OSError, RuntimeError):
3207
+ pass # Preserve the original cancellation/failure; never guess ownership.
3208
+
3209
+
3148
3210
  if action == "acquire":
3211
+ # One budget includes owner/namespace changes. Waiting only observes an
3212
+ # existing safe lock; all authority checks still follow atomic acquisition.
3213
+ deadline = time.monotonic() + PREPARED_LOCK_WAIT_SECONDS
3149
3214
  candidate = root / f".install-lock.{pid}.{secrets.token_hex(12)}.tmp"
3150
3215
  os.mkdir(candidate, 0o700)
3151
3216
  os.chmod(candidate, 0o700)
@@ -3165,21 +3230,44 @@ if action == "acquire":
3165
3230
  finally:
3166
3231
  os.close(descriptor)
3167
3232
  fsync_directory(candidate)
3168
- for _attempt in range(32):
3233
+ if wait_for_prepared:
3234
+ def cancelled(signum, _frame):
3235
+ raise SystemExit(128 + signum)
3236
+ for signum in (signal.SIGINT, signal.SIGTERM, signal.SIGHUP):
3237
+ signal.signal(signum, cancelled)
3238
+ namespace_attempts = 0
3239
+ while namespace_attempts < 32:
3240
+ if wait_for_prepared:
3241
+ check_wait_root()
3242
+ if time.monotonic() >= deadline:
3243
+ raise RuntimeError("AgentsServer prepared update timed out waiting for the installation lock")
3169
3244
  try:
3170
3245
  os.rename(candidate, lock)
3171
3246
  except OSError as exc:
3172
3247
  if exc.errno not in {errno.EEXIST, errno.ENOTEMPTY}:
3173
3248
  raise
3174
- directory_descriptor, directory_info = open_lock_directory()
3175
3249
  try:
3250
+ directory_descriptor, directory_info = open_lock_directory()
3251
+ except FileNotFoundError:
3252
+ if not wait_for_prepared:
3253
+ raise
3254
+ # The incumbent retired after rename observed its lock.
3255
+ namespace_attempts += 1
3256
+ continue
3257
+ try:
3258
+ if wait_for_prepared and any(
3259
+ name != "pid" for name in os.listdir(directory_descriptor)
3260
+ ):
3261
+ raise PermissionError("AgentsServer install lock contains unknown files")
3176
3262
  try:
3177
3263
  owner_pid, owner_identity = read_owner(directory_descriptor)
3178
3264
  except FileNotFoundError:
3179
3265
  # An old installer may have crashed in mkdir->pid. The
3180
3266
  # next atomic directory rename replaces only that
3181
3267
  # exact empty namespace; a populated replacement wins.
3268
+ namespace_attempts += 1
3182
3269
  continue
3270
+ alive = False
3183
3271
  try:
3184
3272
  os.kill(owner_pid, 0)
3185
3273
  except ProcessLookupError:
@@ -3189,6 +3277,22 @@ if action == "acquire":
3189
3277
  f"another AgentsServer installation is active ({owner_pid})"
3190
3278
  )
3191
3279
  else:
3280
+ alive = True
3281
+ if alive:
3282
+ if wait_for_prepared:
3283
+ try:
3284
+ unchanged = same_wait_owner(directory_descriptor, directory_info,
3285
+ owner_pid, owner_identity)
3286
+ except FileNotFoundError:
3287
+ unchanged = False
3288
+ if not unchanged:
3289
+ namespace_attempts += 1
3290
+ continue
3291
+ remaining = deadline - time.monotonic()
3292
+ if remaining <= 0:
3293
+ raise RuntimeError("AgentsServer prepared update timed out waiting for the installation lock")
3294
+ time.sleep(min(0.1, remaining))
3295
+ continue
3192
3296
  raise RuntimeError(
3193
3297
  f"another AgentsServer installation is active ({owner_pid})"
3194
3298
  )
@@ -3200,19 +3304,25 @@ if action == "acquire":
3200
3304
  directory_descriptor
3201
3305
  )
3202
3306
  except FileNotFoundError:
3307
+ namespace_attempts += 1
3203
3308
  continue
3204
3309
  if current_owner != owner_pid or current_identity != owner_identity:
3310
+ namespace_attempts += 1
3205
3311
  continue
3206
3312
  try:
3207
3313
  os.unlink("pid", dir_fd=directory_descriptor)
3208
3314
  except FileNotFoundError:
3315
+ namespace_attempts += 1
3209
3316
  continue
3210
3317
  os.fsync(directory_descriptor)
3211
3318
  finally:
3212
3319
  os.close(directory_descriptor)
3320
+ namespace_attempts += 1
3213
3321
  continue
3214
3322
  else:
3215
3323
  owned = True
3324
+ if wait_for_prepared:
3325
+ check_wait_root()
3216
3326
  for _ in range(3):
3217
3327
  try:
3218
3328
  fsync_directory(root)
@@ -3232,7 +3342,9 @@ if action == "acquire":
3232
3342
  raise SystemExit(0)
3233
3343
  raise RuntimeError("AgentsServer install lock ownership kept changing")
3234
3344
  finally:
3235
- if not owned:
3345
+ if not owned and wait_for_prepared:
3346
+ cleanup_wait_candidate(candidate, candidate_info)
3347
+ elif not owned:
3236
3348
  try:
3237
3349
  (candidate / "pid").unlink()
3238
3350
  except FileNotFoundError: