From a698f98785cfd5c16eb5521f022c81ab7e733cf6 Mon Sep 17 00:00:00 2001 From: Szymon Chmal Date: Thu, 15 Oct 2026 14:28:04 +0200 Subject: [PATCH 1/2] chore(agent-device): require agent-device 0.21.28 and take its proxy from npm agent-device 0.21.28 is the first release with every change simlock's edge relies on, so the vendored integration-branch proxy goes and the version check moves from 0.21.25 to 0.21.28. A prerelease of the minimum now counts as older than it. The released proxy refuses install_from_source and release_materialized_paths from a tenant, so simlock's front refuses them too instead of forwarding them. --- docs/CLI.md | 2 +- docs/CONFIGURATION.md | 2 +- docs/HTTP-API.md | 2 +- docs/internal/agent-rules/toolchain.md | 10 ++-- e2e/helpers/agent-device.ts | 15 +++--- e2e/slow-agent-device.test.ts | 4 +- package.json | 2 +- pnpm-lock.yaml | 11 ++--- pnpm-workspace.yaml | 3 ++ scripts/slow-e2e.sh | 20 ++++---- src/agent-device/AGENTS.md | 5 +- src/agent-device/edge.test.ts | 40 ++++++++------- src/agent-device/front/request-rules.ts | 7 +-- src/agent-device/gateway-front.test.ts | 26 +++++----- .../host/agent-device-supervisor.test.ts | 31 ++++++------ .../host/availability-findings.test.ts | 2 +- src/agent-device/host/upstream.test.ts | 6 +-- src/agent-device/shared/compatibility.ts | 26 +++++++--- src/agent-device/shared/routes.ts | 2 - src/cli/index.test.ts | 8 +-- src/contract/schemas.test.ts | 6 +-- src/core/doctor.test.ts | 4 +- src/daemon/main.test.ts | 4 +- src/gateway/service.test.ts | 2 +- src/slow-e2e.test.ts | 46 ++++++++++++------ src/source-hygiene.test.ts | 12 ++--- ui/src/views/worker-detail.test.tsx | 4 +- vendor/README.md | 13 ----- ...agent-device-proxy-0.21.28-dev-253dca0.tgz | Bin 29907 -> 0 bytes 29 files changed, 163 insertions(+), 152 deletions(-) delete mode 100644 vendor/README.md delete mode 100644 vendor/agent-device-proxy-0.21.28-dev-253dca0.tgz diff --git a/docs/CLI.md b/docs/CLI.md index 45661d951..5957b4398 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -2204,7 +2204,7 @@ is needed, and what to do, and is printed to stderr as one line, cut to 500 characters for each of the two texts: ```text -agent-device fail found 0.20.3, need 0.21.25 or later; install agent-device 0.21.25 or later, then restart the daemon (`simlock daemon stop`, then `simlock daemon start`) +agent-device fail found 0.20.3, need 0.21.28 or later; install agent-device 0.21.28 or later, then restart the daemon (`simlock daemon stop`, then `simlock daemon start`) ``` While agent-device is still starting, or running, or switched off, there is diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 5c5ec67bf..df5889247 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -35,7 +35,7 @@ edit yourself keeps the mode you give it until the next `config set`; run | `gateway.url` | **Worker side.** Base URL of the gateway this worker joins, e.g. `wss://gw.example:4700`; the worker dials `/v1/uplink` and upgrades it to a WebSocket. Joining grants that gateway the `admin` role on this daemon, and `gateway.token` rides on the upgrade request as a bearer credential, so use `wss://` — or plain `ws://` only over loopback or inside your own tunnel. `http://` and `https://` are rejected at load. Unset means "do not join a fleet": the default, and the only thing that changes about a joined worker. | unset | | `gateway.token` | **Worker side.** The join token (`simlock token create --role worker`, minted on the gateway) this worker presents when it opens its uplink. Required whenever `gateway.url` is set. `simlock config` shows it; the daemon log and the `daemon.started` event show `[redacted]`. It is stored in plain text in `config.json`, which `simlock config set` writes owner-only. | unset | | `gateway.label` | **Worker side.** Display name for this worker in `simlock worker list`, `status`, the console, and on the lease's `worker` block. Display-only: nothing routes on it and it need not be unique. | the worker's own id | -| `agentDevice.enabled` | **Both sides.** On a worker: run one agent-device daemon on this worker, started and watched by Simlock. It sees Simlock's devices and no others, refuses `boot` and `shutdown`, and keeps its files in `~/.simlock/agent-device`. If agent-device is missing, older than 0.21.25, speaks another RPC protocol or needs a newer Node than Simlock runs on, it is not started and Simlock runs on without it. A daemon a killed Simlock left behind is stopped at the next start. With `http.enabled` also `true`, the agent-device CLI reaches it at `/v1/agent-device` (see [HTTP-API.md](HTTP-API.md#v1agent-device)). On a gateway: serve `/v1/agent-device` and send each agent to a worker whose agent-device is available; the gateway runs no daemon of its own. | `false` | +| `agentDevice.enabled` | **Both sides.** On a worker: run one agent-device daemon on this worker, started and watched by Simlock. It sees Simlock's devices and no others, refuses `boot` and `shutdown`, and keeps its files in `~/.simlock/agent-device`. If agent-device is missing, older than 0.21.28, speaks another RPC protocol or needs a newer Node than Simlock runs on, it is not started and Simlock runs on without it. A daemon a killed Simlock left behind is stopped at the next start. With `http.enabled` also `true`, the agent-device CLI reaches it at `/v1/agent-device` (see [HTTP-API.md](HTTP-API.md#v1agent-device)). On a gateway: serve `/v1/agent-device` and send each agent to a worker whose agent-device is available; the gateway runs no daemon of its own. | `false` | | `agentDevice.leaseTtlCapMs` | **Both sides.** Caps the TTL of the leases the agent-device CLI makes and renews through Simlock: a longer `ttlMs` is lowered to it, and a lease that sends none gets it. A whole number of milliseconds from `5000` to `lease.maxTtlMs`; a value outside that range stops the daemon at start, on a gateway too. Used only when `agentDevice.enabled` is `true`. | unset (no cap) | | `agentDevice.command` | **Worker side.** The agent-device executable: a name looked up on `PATH`, or a path. A gateway ignores it and logs a warning. Used only when `agentDevice.enabled` is `true`. | `agent-device` | | `exec.timeoutMs` | **Worker side.** How long one `device.exec` command (`simlock simctl` / `simlock adb` against a gateway or over HTTP) may run before the worker kills it and the operation fails with `EXEC_TIMEOUT`. Authoritative: it bounds the process that actually runs. | `10 minutes` | diff --git a/docs/HTTP-API.md b/docs/HTTP-API.md index d3e07a561..d7ca2aae6 100644 --- a/docs/HTTP-API.md +++ b/docs/HTTP-API.md @@ -269,7 +269,7 @@ sends it, empty when nothing waits; an older daemon leaves it out. state, in the order it sends them (at most 16, no name twice): ```json -[{ "name": "agent-device", "state": "unavailable", "reason": "found 0.20.1, need 0.21.25 or later" }] +[{ "name": "agent-device", "state": "unavailable", "reason": "found 0.20.1, need 0.21.28 or later" }] ``` `name` is lower-case letters, digits and `-`, starting with a letter, up to 32 diff --git a/docs/internal/agent-rules/toolchain.md b/docs/internal/agent-rules/toolchain.md index 2634db0de..c5a6a6a72 100644 --- a/docs/internal/agent-rules/toolchain.md +++ b/docs/internal/agent-rules/toolchain.md @@ -68,8 +68,8 @@ Tests that start real simulators or emulators. Run them only through - It holds a machine-wide lock, runs detached, prints its log path at once, and writes `.exit` when the run ends. - Exit 75: another run holds the lock; the script prints the holder. -- Exit 69: the machine is unfit (low disk, another tool's build at the - wrong commit or not built); the script prints why. +- Exit 69: the machine is unfit (low disk, another tool at the + wrong version or not built); the script prints why. - Vitest arguments pass through: `-t ""` runs one test, `--bail=1` ends the run at the first failure. - It first stops what earlier runs left behind. By hand: `scripts/slow-e2e.sh @@ -89,7 +89,7 @@ The machine can run a line only if: | iOS | `uname -s` is `Darwin`; `xcrun simctl list runtimes` lists one | | Android | `command -v emulator adb` finds both | | Physical iOS | `uname -s` is `Darwin`; `SIMLOCK_PHYSICAL_IOS` is the UDID of an iPhone on USB (trusted, Developer Mode on, unlocked, Auto-Lock Never); `SIMLOCK_PHYSICAL_IOS_APP` is a `.app` signed for it. Run `scripts/slow-e2e.sh --physical e2e/slow-ios-physical.test.ts` | -| agent-device through simlock | `node -v` is 22.12.0 or later (agent-device's own minimum); the iOS check and `uname -m` is `arm64` (the fixture app is arm64 only), or the Android check; `SIMLOCK_E2E_AGENT_DEVICE` (in `.slow-lane.env`) is the absolute path of `bin/agent-device.mjs` in a checkout of agent-device at the commit `vendor/README.md` names, built with `pnpm install && pnpm build && pnpm build:android` (the last builds the Android snapshot helper); room for two devices: `node -p "os.totalmem() / 2**30"` prints at least 7 for iOS or 12 for Android (simlock's 4 GiB OS reserve plus two of its default 1.5 GiB iOS or 4 GiB Android per-device budgets). Run `scripts/slow-e2e.sh e2e/slow-agent-device.test.ts` or `scripts/slow-e2e.sh e2e/slow-agent-device-gateway.test.ts` | +| agent-device through simlock | `node -v` is 22.12.0 or later (agent-device's own minimum); the iOS check and `uname -m` is `arm64` (the fixture app is arm64 only), or the Android check; `SIMLOCK_E2E_AGENT_DEVICE` (in `.slow-lane.env`) is the absolute path of `bin/agent-device.mjs` in an install of the agent-device version `package.json` pins for `@agent-device/proxy` (`npm install -g agent-device@<version>`, or a checkout at tag `v<version>` built with `pnpm install && pnpm build && pnpm build:android`, the last building the Android snapshot helper); room for two devices: `node -p "os.totalmem() / 2**30"` prints at least 7 for iOS or 12 for Android (simlock's 4 GiB OS reserve plus two of its default 1.5 GiB iOS or 4 GiB Android per-device budgets). Run `scripts/slow-e2e.sh e2e/slow-agent-device.test.ts` or `scripts/slow-e2e.sh e2e/slow-agent-device-gateway.test.ts` | | Physical Android | `command -v adb` finds it; `adb devices -l` lists `$SIMLOCK_PHYSICAL_ANDROID` on a USB transport (a `usb:` field) in state `device`; `SIMLOCK_PHYSICAL_ANDROID_APK` names an APK. Run `scripts/slow-e2e.sh --physical e2e/slow-android-physical.test.ts` | | Physical Android, a person at the phone | the Physical Android checks above, and `SIMLOCK_PHYSICAL_PERSON` is `1`. Run `scripts/slow-e2e.sh --physical-manual e2e/slow-android-physical-manual.test.ts` | | Physical iOS, a person at the phone | the Physical iOS checks above, a passcode set on the phone (locking it then needs the passcode), and `SIMLOCK_PHYSICAL_PERSON` is `1`. Run `scripts/slow-e2e.sh --physical-manual e2e/slow-ios-physical-manual.test.ts` | @@ -107,12 +107,12 @@ and names the slow-lane test that proves it. ## Other tools' source A spec's Facts row about another tool cites that tool's source at the -commit this repo uses, or a command run on this machine; never a summary or +version this repo uses, or a command run on this machine; never a summary or the tool's docs alone. | Tool | Version | Read it | | ---- | ------- | ------- | -| agent-device | the commit `vendor/README.md` names | `git -C <checkout> show <commit>:<path>` in a clone of `callstackincubator/agent-device` (on a machine that runs the slow lane, the one `.slow-lane.env` names). No clone: `gh api 'repos/callstackincubator/agent-device/contents/<path>?ref=<commit>' -H 'Accept: application/vnd.github.raw'`. The proxy as simlock bundles it: `node_modules/@agent-device/proxy/dist` | +| agent-device | the version `package.json` pins for `@agent-device/proxy` (tag `v<version>`) | `git -C <checkout> show v<version>:<path>` in a clone of `callstackincubator/agent-device` (on a machine that runs the slow lane, the one `.slow-lane.env` names). No clone: `gh api 'repos/callstackincubator/agent-device/contents/<path>?ref=v<version>' -H 'Accept: application/vnd.github.raw'`. The proxy as simlock bundles it: `node_modules/@agent-device/proxy/dist` | | Android SDK tools, Xcode tools | the installed ones | run them: `<tool> --help`, or the command itself | | simlock's own CLI | `main` | `pnpm build`, then `node dist/cli/main.js <command> --help` or the command itself | diff --git a/e2e/helpers/agent-device.ts b/e2e/helpers/agent-device.ts index fbadf3965..e85c6ab64 100644 --- a/e2e/helpers/agent-device.ts +++ b/e2e/helpers/agent-device.ts @@ -47,7 +47,7 @@ function agentDeviceEntry(): string { return entry; } -// why: the slow lane must run the integration build the repo pins, never an agent-device installed on the machine; the pinned version is in the vendored tarball's name. +// why: the slow lane must run the agent-device release whose proxy simlock bundles, so the version package.json pins for @agent-device/proxy is the one it accepts. export async function assertPinnedAgentDevice( platform: "ios" | "android" = "ios", ): Promise<string> { @@ -60,21 +60,20 @@ export async function assertPinnedAgentDevice( const manifest = JSON.parse(await readFile(join(REPO_ROOT, "package.json"), "utf8")) as { dependencies?: Record<string, string>; }; - const pinned = /agent-device-proxy-(.+)-[0-9a-f]{7,}\.tgz$/.exec( - manifest.dependencies?.["@agent-device/proxy"] ?? "", - )?.[1]; - if (pinned === undefined) throw new Error("package.json does not pin @agent-device/proxy"); + const pinned = manifest.dependencies?.["@agent-device/proxy"]; + if (pinned === undefined || !/^\d+\.\d+\.\d+$/.test(pinned)) + throw new Error("package.json does not pin @agent-device/proxy"); const actual = execFileSync(process.execPath, [entry, "--version"], { encoding: "utf8", timeout: 60_000, }).trim(); if (actual !== pinned) { throw new Error( - `SIMLOCK_E2E_AGENT_DEVICE ${entry} is version ${actual}, the pinned build is ${pinned}`, + `SIMLOCK_E2E_AGENT_DEVICE ${entry} is version ${actual}, package.json pins ${pinned}`, ); } if (platform === "android") { - // why: an Android snapshot needs the helper APK `pnpm build:android` packages into the checkout; without it every snapshot fails mid-test. + // why: an Android snapshot needs the helper APK the npm package ships and a checkout builds with `pnpm build:android`; without it every snapshot fails mid-test. const manifest = join( dirname(entry), "..", @@ -85,7 +84,7 @@ export async function assertPinnedAgentDevice( ); if (!existsSync(manifest)) { throw new Error( - `the Android snapshot helper is not built (${manifest} is missing); run \`pnpm build:android\` with ANDROID_HOME set in the agent-device checkout`, + `the Android snapshot helper is missing (${manifest}); install agent-device ${actual} from npm, or run \`pnpm build:android\` with ANDROID_HOME set in the agent-device checkout`, ); } } diff --git a/e2e/slow-agent-device.test.ts b/e2e/slow-agent-device.test.ts index 61e47ddb8..ea14d5b90 100644 --- a/e2e/slow-agent-device.test.ts +++ b/e2e/slow-agent-device.test.ts @@ -503,7 +503,7 @@ function defineSuite(target: Target): void { ); it( - "with agentDevice.command pointing at an agent-device that reports 0.9.2, the daemon starts and grants a plain simlock lease --detach; an agent's open fails with agent-device is not available on this simlock: found 0.9.2, need 0.21.25 or later; simlock doctor reports agent-device-unavailable with found 0.9.2, need 0.21.25 or later", + "with agentDevice.command pointing at an agent-device that reports 0.9.2, the daemon starts and grants a plain simlock lease --detach; an agent's open fails with agent-device is not available on this simlock: found 0.9.2, need 0.21.28 or later; simlock doctor reports agent-device-unavailable with found 0.9.2, need 0.21.28 or later", { timeout: 900_000 }, async () => { const root = await mkdtemp(join(tmpdir(), "simlock-ad-old-")); @@ -515,7 +515,7 @@ function defineSuite(target: Target): void { await writeFile(join(root, "dist/src/daemon.js"), ""); await writeFile(entry, '#!/usr/bin/env node\nconsole.log("0.9.2");\n'); await chmod(entry, 0o755); - const message = "found 0.9.2, need 0.21.25 or later"; + const message = "found 0.9.2, need 0.21.28 or later"; await env.withConfig({ agentDevice: { command: entry } }, async () => { const a = await agent("a"); diff --git a/package.json b/package.json index c6a7573a2..900b80875 100644 --- a/package.json +++ b/package.json @@ -65,7 +65,7 @@ "mutate": "node scripts/mutate.mjs" }, "dependencies": { - "@agent-device/proxy": "file:vendor/agent-device-proxy-0.21.28-dev-253dca0.tgz", + "@agent-device/proxy": "0.21.28", "@hono/node-server": "2.0.10", "@hono/zod-validator": "^0.9.1", "@modelcontextprotocol/sdk": "1.29.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0452e41cc..efce0096d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -214,8 +214,8 @@ importers: .: dependencies: '@agent-device/proxy': - specifier: file:vendor/agent-device-proxy-0.21.28-dev-253dca0.tgz - version: file:vendor/agent-device-proxy-0.21.28-dev-253dca0.tgz + specifier: 0.21.28 + version: 0.21.28 '@hono/node-server': specifier: 2.0.10 version: 2.0.10(hono@4.13.5) @@ -319,9 +319,8 @@ importers: packages: - '@agent-device/proxy@file:vendor/agent-device-proxy-0.21.28-dev-253dca0.tgz': - resolution: {integrity: sha512-8vQHEJTDvsQvJchkbX/yZe/BZ2JkF68rsnQ/Ii16kuFCRgv2vXO6IYml2NrEOum7jjGpIXuuZl/WFP8ywl5p5w==, tarball: file:vendor/agent-device-proxy-0.21.28-dev-253dca0.tgz} - version: 0.21.28-dev + '@agent-device/proxy@0.21.28': + resolution: {integrity: sha512-uwepF+PU2cZN27UB6itCWIjY8tyevW/fFMHEWIuUHsyv3lDJyFoqwJdlyx4DRoyPbvDUmHkwMtUO2nZRlQdWyQ==} engines: {node: '>=22.12'} '@axe-core/playwright@4.13.0': @@ -3254,7 +3253,7 @@ packages: snapshots: - '@agent-device/proxy@file:vendor/agent-device-proxy-0.21.28-dev-253dca0.tgz': {} + '@agent-device/proxy@0.21.28': {} '@axe-core/playwright@4.13.0(playwright-core@1.63.0)': dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index cc0537d22..dc32803dd 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,5 +1,8 @@ allowBuilds: lefthook: true +# why: agent-device 0.21.28 is the first release with the edge's changes, published the day simlock moved to it. +minimumReleaseAgeExclude: + - "@agent-device/proxy@0.21.28" overrides: "@hono/node-server": 2.0.10 ip-address: ">=10.3.1" diff --git a/scripts/slow-e2e.sh b/scripts/slow-e2e.sh index c54b4289e..cfd3f77cd 100755 --- a/scripts/slow-e2e.sh +++ b/scripts/slow-e2e.sh @@ -15,8 +15,8 @@ # when it ends. Exits 75 without starting when another worktree's run holds the lock: real # simulators and emulators share one machine, and two lanes at once make timeouts that look # like bugs. Exits 69 without starting when the machine is not fit to run it: too little free -# disk, or SIMLOCK_E2E_AGENT_DEVICE naming a checkout at another commit than vendor/README.md -# or one never built. The lock is a directory holding the runner's pid; a lock whose pid is +# disk, or SIMLOCK_E2E_AGENT_DEVICE naming an agent-device of another version than package.json +# pins for @agent-device/proxy, or a checkout never built. The lock is a directory holding the runner's pid; a lock whose pid is # gone is stale and is taken over. # # Before a run it sources `.slow-lane.env` from the main checkout's root, if there is one: this @@ -36,7 +36,7 @@ root=$(git rev-parse --show-toplevel) helper() { python3 - "$lock" "$lane_tmp" "$root" "$min_free" "$@" <<'EOF' -import os, re, shutil, signal, subprocess, sys, time +import json, os, re, shutil, signal, subprocess, sys, time lock, lane_tmp, root, min_free, command = sys.argv[1:6] def read(path, default=""): @@ -112,13 +112,13 @@ elif command == "check": problems.append(f"{free_gib(path):.1f} GiB free on the volume of {path}; the lane needs {min_free}") agent_device = os.environ.get("SIMLOCK_E2E_AGENT_DEVICE") if agent_device: - checkout = os.path.dirname(os.path.dirname(agent_device)) - pinned = re.search(r"\b[0-9a-f]{40}\b", read(os.path.join(root, "vendor", "README.md"))) - head = subprocess.run(["git", "-C", checkout, "rev-parse", "HEAD"], capture_output=True, text=True).stdout.strip() - if pinned and head != pinned.group(0): - problems.append(f"SIMLOCK_E2E_AGENT_DEVICE is a checkout at {head[:9] or 'no commit'}; vendor/README.md pins {pinned.group(0)[:9]}") - if not os.path.isdir(os.path.join(checkout, "dist")): - problems.append(f"SIMLOCK_E2E_AGENT_DEVICE's checkout {checkout} has no dist/: build it first") + install = os.path.dirname(os.path.dirname(agent_device)) + pinned = json.loads(read(os.path.join(root, "package.json"), "{}")).get("dependencies", {}).get("@agent-device/proxy") + found = json.loads(read(os.path.join(install, "package.json"), "{}")).get("version") + if pinned and found != pinned: + problems.append(f"SIMLOCK_E2E_AGENT_DEVICE is agent-device {found or 'of no version'}; package.json pins {pinned}") + if not os.path.isdir(os.path.join(install, "dist")): + problems.append(f"SIMLOCK_E2E_AGENT_DEVICE's install {install} has no dist/: build it first") for problem in problems: print(f"unfit: {problem}", file=sys.stderr) sys.exit(69 if problems else 0) diff --git a/src/agent-device/AGENTS.md b/src/agent-device/AGENTS.md index 038c3a253..d08a55ff8 100644 --- a/src/agent-device/AGENTS.md +++ b/src/agent-device/AGENTS.md @@ -36,7 +36,7 @@ Supervisor start (`AgentDeviceSupervisor.start`, at daemon start): Signal only if `ps` args end in `--simlock-home=<data dir>`; SIGTERM, check again, SIGKILL. 2. Find — `agentDevice.command` (`agent-device`) on `PATH` or as a path; `package.json` names `agent-device`; a daemon entry under `dist/src`. Never downloads. -3. Check — `--version` ≥ 0.21.25; Node ≥ 22.12.0 (`shared/compatibility.ts`). +3. Check — `--version` ≥ 0.21.28 (a prerelease of it is older); Node ≥ 22.12.0 (`shared/compatibility.ts`). 4. Prepare — state dir `<data dir>/agent-device` (0700, ours), policy file denying `boot`, `shutdown`, `device-shutdown`; env from `agentDeviceDaemonEnvironment` plus every driver's lease env (iOS device set mapped; no-iOS and no-Android sentinels). @@ -57,7 +57,8 @@ Front (`createFront`, prefix `agent-device`): rebuilt into the map first. 2. Non-RPC: a fixed route list (`shared/routes.ts`), else 404. `/health` answers 200 even when unavailable. -3. `/rpc`: body ≤ 1 MiB, one JSON-RPC request; `classifyCall` → allocate, heartbeat, +3. `/rpc`: body ≤ 1 MiB, one JSON-RPC request; only `command` and `lease.*` methods, as + agent-device's proxy takes no other from a tenant; `classifyCall` → allocate, heartbeat, release, command, or refused `wrapped-lease`. 4. `checkRequest`: iOS/Android only; no device selector but the leased one; no device set or allowlist; `install` only from an upload; `devices` alone, not streamed. diff --git a/src/agent-device/edge.test.ts b/src/agent-device/edge.test.ts index 49738f570..c3be0e5b1 100644 --- a/src/agent-device/edge.test.ts +++ b/src/agent-device/edge.test.ts @@ -561,7 +561,7 @@ describe("the agent-device front, through the whole worker daemon", () => { it("with agentDevice.enabled true and agent-device unavailable, /health, /rpc and an upload each answer `agent-device is not available on this simlock: <reason>`, nothing is leased, and POST /v1/lease-requests still grants a device", async () => { const rig = await setUp(); - const reason = "found 0.20.3, need 0.21.25 or later"; + const reason = "found 0.20.3, need 0.21.28 or later"; rig.fake.setUnavailable(reason); const health = await send(rig, "/health"); @@ -2888,7 +2888,7 @@ describe("the agent-device front's request rules", () => { expect((await send(rig, "/admin/leases")).status).toBe(404); const missing = await send(rig, "/admin/leases", { token: null }); expect(missing.status).toBe(401); - rig.fake.setUnavailable("found 0.20.3, need 0.21.25 or later"); + rig.fake.setUnavailable("found 0.20.3, need 0.21.28 or later"); const unavailable = await send(rig, "/admin/leases"); @@ -2908,6 +2908,25 @@ describe("the agent-device front's request rules", () => { "refused: unknown method agent_device.admin", ); }); + + it("`install_from_source` and `release_materialized_paths`, which agent-device's proxy refuses for a tenant, are refused `refused: unknown method <method>` and never forwarded", async () => { + const rig = await setUp(); + + await refused( + rig, + () => + rpc(rig, "agent_device.install_from_source", { + platform: "ios", + source: { kind: "url", url: "https://example.com/App.zip" }, + }), + "refused: unknown method agent_device.install_from_source", + ); + await refused( + rig, + () => rpc(rig, "agent_device.release_materialized_paths", { materializationId: "m-1" }), + "refused: unknown method agent_device.release_materialized_paths", + ); + }); }); describe("rule 4, the platform", () => { @@ -2970,21 +2989,6 @@ describe("the agent-device front's request rules", () => { ); expect(commandsReceived(rig)).toHaveLength(1); }); - - it("`install_from_source` with a URL source and platform `ios` is forwarded, as today; with platform `web` it is refused", async () => { - const rig = await setUp(); - const params = (platform: string) => ({ - platform, - source: { kind: "url", url: "https://example.com/App.zip" }, - }); - await forwarded(rig, () => rpc(rig, "agent_device.install_from_source", params("ios"))); - - await refused( - rig, - () => rpc(rig, "agent_device.install_from_source", params("web")), - onlyIosAndAndroid, - ); - }); }); describe("rule 6, device selectors", () => { @@ -3664,7 +3668,7 @@ describe("a worker's capabilities in status.get, through the whole worker daemon it("a worker whose agent-device is unavailable answers state unavailable with the same reason its front's notAvailable reply names", async () => { const rig = await setUp(); - const reason = "found 0.20.1, need 0.21.25 or later"; + const reason = "found 0.20.1, need 0.21.28 or later"; rig.fake.setUnavailable(reason); const health = await (await send(rig, "/health")).text(); diff --git a/src/agent-device/front/request-rules.ts b/src/agent-device/front/request-rules.ts index 69ec89105..971bc6f3a 100644 --- a/src/agent-device/front/request-rules.ts +++ b/src/agent-device/front/request-rules.ts @@ -40,7 +40,7 @@ class Refused extends Error { const refuse = (message: string): Refused => new Refused(refusal(message)); interface Unit { - readonly kind: "command" | "allocate" | "install"; + readonly kind: "command" | "allocate"; readonly owner: Fields; readonly command: string; readonly isStep: boolean; @@ -215,7 +215,6 @@ function ruleAddresses(check: Check, unit: Unit): void { } async function ruleSelectors(check: Check, unit: Unit): Promise<void> { - if (unit.kind === "install") return; ruleAddresses(check, unit); if (unit.kind === "allocate") return; if (unit.command === "test") return ruleShardList(check, unit); @@ -291,7 +290,6 @@ async function applyRules(check: Check, unit: Unit): Promise<void> { } } rulePlatform(check, unit); - if (unit.kind === "install") return; ruleDeviceSet(unit); await ruleSelectors(check, unit); await ruleSharding(check, unit); @@ -312,9 +310,6 @@ function unitFor(name: string, params: Fields): Unit | undefined { if (name === "lease.allocate") { return { command: "", isStep: false, kind: "allocate", owner: params }; } - if (name === "install_from_source") { - return { command: "", isStep: false, kind: "install", owner: params }; - } return undefined; } diff --git a/src/agent-device/gateway-front.test.ts b/src/agent-device/gateway-front.test.ts index d458a4f8f..ac684dba9 100644 --- a/src/agent-device/gateway-front.test.ts +++ b/src/agent-device/gateway-front.test.ts @@ -1702,9 +1702,8 @@ describe("the agent-device front, through a gateway: the lease header", { timeou .toEqual([["devices"], ["devices"]]); }); - it("through the gateway, `release_materialized_paths` is refused `refused: this agent-device is too old to use through a gateway; update it`, and an `install_from_source` naming the token's lease reaches its worker", async () => { + it("through the gateway, `release_materialized_paths` and an `install_from_source` naming the token's lease are refused `refused: unknown method <method>`, and no worker sees either", async () => { const rig = await twoLeasesOnTwoWorkers(); - const target = holderOf(rig, 2); const before = rig.members.map((member) => member.fake.requests.length); const released = await envelopeOf( @@ -1712,18 +1711,21 @@ describe("the agent-device front, through a gateway: the lease header", { timeou materializationId: "m-1", }), ); + const installed = await envelopeOf( + await rpc(rig, "agent_device.install_from_source", { + meta: { leaseId: hex(2) }, + platform: "ios", + source: { kind: "url", url: "https://example.com/App.zip" }, + }), + ); - expect(released.error?.message).toBe(tooOld); + expect(released.error?.message).toBe( + "refused: unknown method agent_device.release_materialized_paths", + ); + expect(installed.error?.message).toBe( + "refused: unknown method agent_device.install_from_source", + ); expect(rig.members.map((member) => member.fake.requests.length)).toEqual(before); - - await rpc(rig, "agent_device.install_from_source", { - meta: { leaseId: hex(2) }, - platform: "ios", - source: { kind: "url", url: "https://example.com/App.zip" }, - }); - - expect(target.fake.rpcRequests("agent_device.install_from_source")).toHaveLength(1); - expect(holderOf(rig, 1).fake.rpcRequests("agent_device.install_from_source")).toHaveLength(0); }); }); diff --git a/src/agent-device/host/agent-device-supervisor.test.ts b/src/agent-device/host/agent-device-supervisor.test.ts index 95fec996c..7c2b2b32d 100644 --- a/src/agent-device/host/agent-device-supervisor.test.ts +++ b/src/agent-device/host/agent-device-supervisor.test.ts @@ -139,7 +139,7 @@ async function arrange(options: Options = {}) { httpPort, pid: mode === "ready" ? pid : pid + 1_000, token, - version: "0.21.25-dev", + version: "0.21.28", }), ); } @@ -232,7 +232,7 @@ async function installAgentDevice( await options.layout?.(filesystem); } -function versionRun(output = "0.21.25-dev\n", extra: Partial<ScriptedProcessExpectation> = {}) { +function versionRun(output = "0.21.28\n", extra: Partial<ScriptedProcessExpectation> = {}) { return { match: { args: [realCli, "--version"], command: nodePath }, result: { code: 0, stderr: "", stdout: output }, @@ -419,7 +419,7 @@ describe("AgentDeviceSupervisor finding agent-device", () => { reason: "not-found", }); expect(unavailable(harness).remedy).toContain("agentDevice.command"); - expect(unavailable(harness).remedy).toContain("0.21.25 or later"); + expect(unavailable(harness).remedy).toContain("0.21.28 or later"); for (const phrase of restartCommand) expect(unavailable(harness).remedy).toContain(phrase); expect(harness.runner.calls).toEqual([]); }); @@ -510,21 +510,24 @@ describe("AgentDeviceSupervisor checking the install", () => { const harness = await run({ expectations: [versionRun("0.20.3\n")] }); expect(unavailable(harness)).toMatchObject({ - message: "found 0.20.3, need 0.21.25 or later", + message: "found 0.20.3, need 0.21.28 or later", reason: "wrong-version", }); - expect(unavailable(harness).remedy).toContain("0.21.25 or later"); + expect(unavailable(harness).remedy).toContain("0.21.28 or later"); for (const phrase of restartCommand) expect(unavailable(harness).remedy).toContain(phrase); }); - it.each(["abc", "0.21", "0.9.100"])("reports %s as wrong-version", async (printed) => { - const harness = await run({ expectations: [versionRun(`${printed}\n`)] }); + it.each(["abc", "0.21", "0.9.100", "0.21.27", "0.21.28-nightly.20261013.49"])( + "reports %s as wrong-version", + async (printed) => { + const harness = await run({ expectations: [versionRun(`${printed}\n`)] }); - expect(unavailable(harness).reason).toBe("wrong-version"); - expect(daemonSpawns(harness)).toEqual([]); - }); + expect(unavailable(harness).reason).toBe("wrong-version"); + expect(daemonSpawns(harness)).toEqual([]); + }, + ); - it.each(["0.21.25-dev", "0.21.25", "0.22.0", "0.100.0", "1.0.0+build.7"])( + it.each(["0.21.28", "0.21.28+build.7", "0.21.29-dev", "0.22.0", "0.100.0", "1.0.0+build.7"])( "starts a daemon for the version %s", async (printed) => { const harness = await run({ expectations: [versionRun(`${printed}\n`), daemonSpawn()] }); @@ -797,7 +800,7 @@ describe("AgentDeviceSupervisor readiness", () => { platforms: ["ios", "android"], state: "running", token, - version: "0.21.25-dev", + version: "0.21.28", }); expect(harness.health.reads).toContain(`http://127.0.0.1:${httpPort}`); }); @@ -811,7 +814,7 @@ describe("AgentDeviceSupervisor readiness", () => { const pid = harness.children[0]?.pid; await harness.filesystem.writeFileAtomic( daemonJsonPath, - JSON.stringify({ httpPort, pid, token, version: "0.21.25-dev" }), + JSON.stringify({ httpPort, pid, token, version: "0.21.28" }), ); harness.probe.startListening(httpPort); await advanceUntil(harness, () => stateOf(harness) === "running", 10_000); @@ -851,7 +854,7 @@ describe("AgentDeviceSupervisor readiness", () => { await advancing(harness, harness.begin()); expect(unavailable(harness)).toMatchObject({ - message: "agent-device 0.21.25-dev speaks RPC protocol 3, need 2", + message: "agent-device 0.21.28 speaks RPC protocol 3, need 2", reason: "wrong-version", }); expect(unavailable(harness).remedy).toContain("RPC protocol 2"); diff --git a/src/agent-device/host/availability-findings.test.ts b/src/agent-device/host/availability-findings.test.ts index 5a7dab551..89f8dc6f4 100644 --- a/src/agent-device/host/availability-findings.test.ts +++ b/src/agent-device/host/availability-findings.test.ts @@ -42,7 +42,7 @@ describe("agentDeviceFindings", () => { platforms: ["ios"], state: "running", token: "t", - version: "0.21.25", + version: "0.21.28", }, ] as const satisfies readonly AgentDeviceAvailability[])( "returns no finding for the $state state", diff --git a/src/agent-device/host/upstream.test.ts b/src/agent-device/host/upstream.test.ts index f258bc63c..a3662b05c 100644 --- a/src/agent-device/host/upstream.test.ts +++ b/src/agent-device/host/upstream.test.ts @@ -103,7 +103,7 @@ async function started(harness: Awaited<ReturnType<typeof arrange>>): Promise<vo describe("the agent-device upstream adapter over the supervisor", () => { it("reports running with the supervisor's base URL, token, generation and the ceiling", async () => { - const harness = await arrange("0.21.25-dev"); + const harness = await arrange("0.21.28"); await started(harness); const availability = harness.supervisor.availability(); if (availability.state !== "running") throw new Error(`got ${availability.state}`); @@ -123,13 +123,13 @@ describe("the agent-device upstream adapter over the supervisor", () => { await started(harness); expect(harness.upstream.current()).toEqual({ - reason: "found 0.20.3, need 0.21.25 or later", + reason: "found 0.20.3, need 0.21.28 or later", state: "unavailable", }); }); it("reports unavailable with starting while the supervisor starts", async () => { - const harness = await arrange("0.21.25-dev"); + const harness = await arrange("0.21.28"); const starting = harness.supervisor.start(); starting.catch(() => undefined); diff --git a/src/agent-device/shared/compatibility.ts b/src/agent-device/shared/compatibility.ts index 9bc589a8d..137019516 100644 --- a/src/agent-device/shared/compatibility.ts +++ b/src/agent-device/shared/compatibility.ts @@ -1,20 +1,30 @@ -export const MIN_AGENT_DEVICE_VERSION = "0.21.25"; +// why: 0.21.28 is the first agent-device release with every change the edge relies on (tenant authenticate, caller-chosen lease ids, allocate by spec, the iOS device set, session end with the lease, release after close, the lease on uploads). +export const MIN_AGENT_DEVICE_VERSION = "0.21.28"; export const AGENT_DEVICE_RPC_PROTOCOL_VERSION = 2; export const MIN_NODE_VERSION = "22.12.0"; -function numbersOf(version: string): readonly [number, number, number] | undefined { - const match = /^(\d+)\.(\d+)\.(\d+)/.exec(version.trim()); +interface ParsedVersion { + readonly numbers: readonly [number, number, number]; + readonly prerelease: boolean; +} + +function parse(version: string): ParsedVersion | undefined { + const match = /^(\d+)\.(\d+)\.(\d+)(-[^+]*)?/.exec(version.trim()); if (match === null) return undefined; - return [Number(match[1]), Number(match[2]), Number(match[3])]; + return { + numbers: [Number(match[1]), Number(match[2]), Number(match[3])], + prerelease: match[4] !== undefined, + }; } +// why: a prerelease of the minimum sorts below it, as in semver: agent-device nightlies of 0.21.28 predate most of what 0.21.28 ships. export function isVersionAtLeast(version: string, minimum: string): boolean { - const found = numbersOf(version); - const needed = numbersOf(minimum); + const found = parse(version); + const needed = parse(minimum); if (found === undefined || needed === undefined) return false; for (let index = 0; index < 3; index += 1) { - const difference = (found[index] ?? 0) - (needed[index] ?? 0); + const difference = (found.numbers[index] ?? 0) - (needed.numbers[index] ?? 0); if (difference !== 0) return difference > 0; } - return true; + return !found.prerelease || needed.prerelease; } diff --git a/src/agent-device/shared/routes.ts b/src/agent-device/shared/routes.ts index 53ad22b82..2ca453702 100644 --- a/src/agent-device/shared/routes.ts +++ b/src/agent-device/shared/routes.ts @@ -5,8 +5,6 @@ const RPC_METHODS: ReadonlySet<string> = new Set([ "lease.allocate", "lease.heartbeat", "lease.release", - "install_from_source", - "release_materialized_paths", ]); export function isKnownRpcMethod(method: string): boolean { diff --git a/src/cli/index.test.ts b/src/cli/index.test.ts index f153d65e2..5cda1648e 100644 --- a/src/cli/index.test.ts +++ b/src/cli/index.test.ts @@ -1427,7 +1427,7 @@ describe("CLI: daemon status (ADR 0003 §11)", () => { { kind: "agent-device-unavailable" as const, message: `found 0.20.3,\n need ${"x".repeat(600)}`, - remedy: `install agent-device 0.21.25 or later,\nthen ${"y".repeat(600)}`, + remedy: `install agent-device 0.21.28 or later,\nthen ${"y".repeat(600)}`, }, { kind: "warm-pool-target-unreachable" as const, @@ -1466,7 +1466,7 @@ describe("CLI: daemon status (ADR 0003 §11)", () => { const index = lines.findIndex((line) => line.startsWith("agent-device fail ")); expect(index).toBeGreaterThan(-1); expect(lines[index]).toBe( - `agent-device fail found 0.20.3, need ${"x".repeat(500 - "found 0.20.3, need ".length)}; ${("install agent-device 0.21.25 or later, then " + "y".repeat(600)).slice(0, 500)}`, + `agent-device fail found 0.20.3, need ${"x".repeat(500 - "found 0.20.3, need ".length)}; ${("install agent-device 0.21.28 or later, then " + "y".repeat(600)).slice(0, 500)}`, ); expect(lines.filter((line) => line.startsWith("agent-device"))).toHaveLength(1); for (const earlier of ["Warning [ios]", "Missing [ios]", "warm-pool-target-unreachable"]) { @@ -6573,7 +6573,7 @@ describe("CLI: a worker's capabilities", () => { capabilities: [ { name: "agent-device", - reason: "found 0.20.1, need 0.21.25 or later", + reason: "found 0.20.1, need 0.21.28 or later", state: "unavailable", }, ], @@ -6585,7 +6585,7 @@ describe("CLI: a worker's capabilities", () => { expect(shown).toMatch(/^wrk_1 .* -- agent-device available$/m); expect(shown).toMatch(/^wrk_2 .* -- agent-device off$/m); expect(shown).toMatch( - /^wrk_3 .* -- agent-device unavailable: found 0\.20\.1, need 0\.21\.25 or later$/m, + /^wrk_3 .* -- agent-device unavailable: found 0\.20\.1, need 0\.21\.28 or later$/m, ); }); diff --git a/src/contract/schemas.test.ts b/src/contract/schemas.test.ts index 085ab41c1..3ecc36261 100644 --- a/src/contract/schemas.test.ts +++ b/src/contract/schemas.test.ts @@ -659,8 +659,8 @@ describe("the status device entry's absent state (ADR 0022 §7)", () => { describe("doctorReportSchema's agent-device-unavailable finding", () => { const finding = { kind: "agent-device-unavailable", - message: "found 0.20.3, need 0.21.25 or later", - remedy: "install agent-device 0.21.25 or later", + message: "found 0.20.3, need 0.21.28 or later", + remedy: "install agent-device 0.21.28 or later", }; it("parses a report with the finding, and refuses one without message or without remedy", () => { @@ -685,7 +685,7 @@ describe("a worker status's capabilities list", () => { const entry = (overrides: Record<string, unknown> = {}) => ({ name: "agent-device", state: "unavailable", - reason: "found 0.20.1, need 0.21.25 or later", + reason: "found 0.20.1, need 0.21.28 or later", ...overrides, }); diff --git a/src/core/doctor.test.ts b/src/core/doctor.test.ts index fcd82abe1..0baf2fa73 100644 --- a/src/core/doctor.test.ts +++ b/src/core/doctor.test.ts @@ -2651,8 +2651,8 @@ describe("Doctor with a platform it cannot observe", () => { describe("Doctor: optional module findings", () => { const unavailable: OptionalModuleFinding = { kind: "agent-device-unavailable", - message: "found 0.20.3, need 0.21.25 or later", - remedy: "install agent-device 0.21.25 or later, then restart the daemon", + message: "found 0.20.3, need 0.21.28 or later", + remedy: "install agent-device 0.21.28 or later, then restart the daemon", }; async function setup( diff --git a/src/daemon/main.test.ts b/src/daemon/main.test.ts index 3a1e44bcc..85733a841 100644 --- a/src/daemon/main.test.ts +++ b/src/daemon/main.test.ts @@ -2407,7 +2407,7 @@ describe("startDaemon with the agent-device edge", () => { return new ScriptedProcessRunner([ { match: { args: [realCli, "--version"], command: process.execPath }, - result: { code: 0, stderr: "", stdout: "0.21.25-dev\n" }, + result: { code: 0, stderr: "", stdout: "0.21.28\n" }, }, { hangs: true, @@ -2618,7 +2618,7 @@ describe("startDaemon with the agent-device edge", () => { ) .toMatchObject({ kind: "agent-device-unavailable", - message: expect.stringContaining("found 0.20.3, need 0.21.25 or later"), + message: expect.stringContaining("found 0.20.3, need 0.21.28 or later"), remedy: expect.any(String), }); await expect(daemon.dispatch("lease.request", lease, agent)).resolves.toMatchObject({ diff --git a/src/gateway/service.test.ts b/src/gateway/service.test.ts index 723b60798..9e7fe99d2 100644 --- a/src/gateway/service.test.ts +++ b/src/gateway/service.test.ts @@ -1868,7 +1868,7 @@ describe("GatewayService", () => { describe("GatewayService: a worker's capabilities in its view", () => { const unavailable = { name: "agent-device", - reason: "found 0.20.1, need 0.21.25 or later", + reason: "found 0.20.1, need 0.21.28 or later", state: "unavailable" as const, }; const available = { name: "agent-device", state: "available" as const }; diff --git a/src/slow-e2e.test.ts b/src/slow-e2e.test.ts index b81fac8ec..67a9720b5 100644 --- a/src/slow-e2e.test.ts +++ b/src/slow-e2e.test.ts @@ -7,7 +7,7 @@ import { fileURLToPath } from "node:url"; import { afterEach, describe, expect, it } from "vitest"; const SCRIPT = join(dirname(fileURLToPath(import.meta.url)), "..", "scripts", "slow-e2e.sh"); -const PINNED = "54e4e4db971d75ba1ebeb75c0ab7bce45c7d9b28"; +const PINNED = "0.21.28"; const started: number[] = []; afterEach(() => { @@ -23,8 +23,11 @@ afterEach(() => { function machine() { const dir = mkdtempSync(join(tmpdir(), "slow-e2e-")); const repo = join(dir, "repo"); - mkdirSync(join(repo, "vendor"), { recursive: true }); - writeFileSync(join(repo, "vendor", "README.md"), `packed at commit \`${PINNED}\`\n`); + mkdirSync(repo, { recursive: true }); + writeFileSync( + join(repo, "package.json"), + JSON.stringify({ dependencies: { "@agent-device/proxy": PINNED } }), + ); execFileSync("git", ["init", "-q"], { cwd: repo }); const env = { ...process.env, @@ -145,28 +148,39 @@ describe("scripts/slow-e2e.sh", () => { expect(existsSync(lock)).toBe(false); }); - it("check refuses an agent-device checkout at another commit than vendor/README.md pins", () => { + function agentDeviceInstall(dir: string, version: string): string { + const install = join(dir, "agent-device"); + mkdirSync(join(install, "bin"), { recursive: true }); + mkdirSync(join(install, "dist")); + writeFileSync(join(install, "package.json"), JSON.stringify({ name: "agent-device", version })); + return join(install, "bin", "agent-device.mjs"); + } + + it("check refuses an agent-device install of another version than package.json pins for @agent-device/proxy", () => { const { dir, env, repo } = machine(); - const checkout = join(dir, "agent-device"); - mkdirSync(join(checkout, "bin"), { recursive: true }); - mkdirSync(join(checkout, "dist")); - execFileSync("git", ["init", "-q"], { cwd: checkout }); - execFileSync( - "git", - ["-c", "user.name=t", "-c", "user.email=t@t", "commit", "-q", "--allow-empty", "-m", "x"], - { cwd: checkout }, - ); - const head = execFileSync("git", ["rev-parse", "HEAD"], { cwd: checkout, encoding: "utf8" }); const result = spawnSync("sh", [SCRIPT, "check"], { cwd: repo, - env: { ...env, SIMLOCK_E2E_AGENT_DEVICE: join(checkout, "bin", "agent-device.mjs") }, + env: { ...env, SIMLOCK_E2E_AGENT_DEVICE: agentDeviceInstall(dir, "0.21.27") }, encoding: "utf8", }); expect(result.status).toBe(69); expect(result.stderr).toBe( - `unfit: SIMLOCK_E2E_AGENT_DEVICE is a checkout at ${head.slice(0, 9)}; vendor/README.md pins ${PINNED.slice(0, 9)}\n`, + `unfit: SIMLOCK_E2E_AGENT_DEVICE is agent-device 0.21.27; package.json pins ${PINNED}\n`, ); }); + + it("check passes an agent-device install of the version package.json pins", () => { + const { dir, env, repo } = machine(); + + const result = spawnSync("sh", [SCRIPT, "check"], { + cwd: repo, + env: { ...env, SIMLOCK_E2E_AGENT_DEVICE: agentDeviceInstall(dir, PINNED) }, + encoding: "utf8", + }); + + expect(result.stderr).toBe(""); + expect(result.status).toBe(0); + }); }); diff --git a/src/source-hygiene.test.ts b/src/source-hygiene.test.ts index 2a2f52b3b..13befc4d3 100644 --- a/src/source-hygiene.test.ts +++ b/src/source-hygiene.test.ts @@ -9,9 +9,8 @@ const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), ".."); const NUL = 0x00; -/** why: built binaries committed for tests or vendored packages are never searched as text. */ -const isCommittedBinary = (name: string): boolean => - name.startsWith("e2e/fixtures/") || (name.startsWith("vendor/") && name.endsWith(".tgz")); +/** why: built binaries committed for tests are never searched as text. */ +const isCommittedBinary = (name: string): boolean => name.startsWith("e2e/fixtures/"); /** * why: a raw NUL byte in a text file makes `grep` and `ripgrep` classify the whole file as @@ -21,8 +20,8 @@ const isCommittedBinary = (name: string): boolean => * * Enumerated from `git ls-files` rather than a directory walk so the check sees every * tracked file in the repository (testing rule 4), not just the directories this test - * happened to name. The exceptions are `e2e/fixtures/` and the vendored `vendor/*.tgz` tarballs: built binaries - * committed for tests, never searched as text, so they are skipped. Untracked and ignored files are out of scope: they are not part of + * happened to name. The exception is `e2e/fixtures/`: built binaries committed for tests, + * never searched as text, so they are skipped. Untracked and ignored files are out of scope: they are not part of * what anyone clones or searches. */ describe("tracked files", () => { @@ -53,9 +52,6 @@ describe("tracked files", () => { it("hold no committed binary and still hold e2e/appduct-fixtures.test.ts", () => { expect(trackedFiles.filter(isCommittedBinary)).toEqual([]); - expect(trackedFiles.some((name) => name.startsWith("vendor/") && name.endsWith(".tgz"))).toBe( - false, - ); expect(trackedFiles).toContain("e2e/appduct-fixtures.test.ts"); }); diff --git a/ui/src/views/worker-detail.test.tsx b/ui/src/views/worker-detail.test.tsx index 5ed3bc051..7390f944a 100644 --- a/ui/src/views/worker-detail.test.tsx +++ b/ui/src/views/worker-detail.test.tsx @@ -317,7 +317,7 @@ describe("the Capabilities row of a worker's page", () => { capabilities: [ { name: "agent-device", state: "available" }, { name: "second", state: "off" }, - { name: "third", reason: "found 0.20.1, need 0.21.25 or later", state: "unavailable" }, + { name: "third", reason: "found 0.20.1, need 0.21.28 or later", state: "unavailable" }, ], }); @@ -325,7 +325,7 @@ describe("the Capabilities row of a worker's page", () => { expect(html).toContain(status("ok", "available")); expect(html).toContain(status("idle", "off")); expect(html).toContain(status("warn", "unavailable")); - expect(text(html)).toContain("third unavailable found 0.20.1, need 0.21.25 or later"); + expect(text(html)).toContain("third unavailable found 0.20.1, need 0.21.28 or later"); }); it("says Not reported for an absent list on a running worker", () => { diff --git a/vendor/README.md b/vendor/README.md deleted file mode 100644 index 0c80f1e79..000000000 --- a/vendor/README.md +++ /dev/null @@ -1,13 +0,0 @@ -# vendor - -`agent-device-proxy-0.21.28-dev-253dca0.tgz` is `@agent-device/proxy`, packed -from the `simlock-integration` branch of agent-device at commit -`253dca023edf13410f7928c76e9e604e5e5871a0`: - -```bash -pnpm --filter @agent-device/proxy pack --pack-destination <dir> -``` - -Its `prepack` runs the package's own build, which bundles agent-device's -internal packages into `dist`, so the tarball has no `workspace:` dependency. -It is swapped for the npm release later. diff --git a/vendor/agent-device-proxy-0.21.28-dev-253dca0.tgz b/vendor/agent-device-proxy-0.21.28-dev-253dca0.tgz deleted file mode 100644 index 4aa419927175b55b10010cedcc53c842e508053a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 29907 zcmV)9K*hfwiwFP!00000|Lnc%b{j{MAl!e6r%0o9EPw`zq%6BVP}3ZUge0taTL84$ zBhf?=l^|+Zs6tm3NTErdvk$RPxKHw(h{$VQKuUIdXZCCNIW3~{7MU3t85!4wKe_g= z_|{>6zjr+B{pp|a-}d(Q_V+JemVV#f-hQ$D-HShMfBW6{+dDhof4}__zW-(C<;y>9 z|LG6-Zz(cA{nPdzGS+{=|Bm`2b{I~0EO@Q9pDb2scs0vdW71?VwqJbD_WdXlnLoL% z)lPXj4}}Pmn1zDPc*-wV>?-x+j0Y_?O*v=Dluc%Sdc|8TOPC+8*n+1biCJ=)`C%N! zSIlRVWU;CxQ<lv_!9+66ZvB)qKMt5LL^28ej0Y@8Cd)aGGe3jzreVYdYh*Ld>O<M0 z-o!xy?nkvSW>BVz*ln22l4ZtHF0wS7Kp!m@#*=6n01T=)3g@950h;5~MJ-F%Qt%eS z*<$k~2&eEb$7wB=mr*EYEf$2(_2n|-EhgY2u3-!2v6ZAu@F=QHl10b`tcndC)&bBL zu%MY-A^~4+XUW{1NhoU5Wg3TK#<5M1Fp=Ph|H&sAe1YoIB#M$-nA#+XgAisUI<?wp z#+iSa+;E%=?QEQ6;e?hOH(+6QRThbvA4Tkv%LU^B3uES6(@9}?k@;~J`Vm_sDUO$) zrdO+t-u2k<<jv@PchF<~Av+zM{M<k29k6<L$oj*2i@ooU-kqF{*!%8a&^;dg!cN|> z?(r||r~dIli}n6?I_M3D>|{{uADtfddj~DnKi)q)JLn(3Wv|ah?D%BF4*N&_QSX3_ zP8bX*yXyCb(AiOMu>Y=mJnFvgANEJTv}$kqqhsjv&B=gu*=ct$>hGT&b_eYAY;bxq z?6L0g0Xsf9?jOGy^pD^6j(W!<kM)n)@d@kw+&dnz;k)kPAq-XPo{ip}3;;g1e{%ZE zp#S#Wh`l>GJm?LE>~)VF_PehSdo<MXFKqv?+dpctgYHrHEkHM5C!=@0K@F-AbnN}R z9(;oFy2q@$KkA<x!!-6!jz@#;{;0)9Cxel0`o2HxwODu1AHssX8JrxoYOtIqZ=g#5 z7}_28s25mj=IqGH0Dqqid%8n*(CZ%dkKYcVF-%ca_G<sPDb>IJ`)B;OkoUj**ZWVA z#DDPpzw_Ppce(q2XZz)gzx?a|{{#NJt1(uO{W-69SpDDqD;{U<fZv1@zO_h`Ppf(h zD%|iCZV0He?Y-FXUi<)+s4QD8u<<+zmJz3K0T+`rT)@2mMWfV@1>6(um@l)`kC?yA zW<1WqiJ!r(g+swLGfk5?gPX*UnGFmJd_GTNkGhwaEp&a<AJI4HF-hT*W?86UmdyFW zzrxzHEL(`q)|TD17x0@cs+#geBEl?5SM?5qm9eIG6=qn8vHCKN<d<($hgV@XTV8sT zWWF^~!@Hwc?SnAo6FEv<9y}qJyT;f9!ouULFy_Jl5+?xy^5gD{7v9bbtL7(ITC1j~ z|CI1US>4+LmSro91OCYiym=-hJdD-DIdoB|HUCcm!^nZ4XNOKu{e}HS1BD!0y~94J zDT@pW&n0@?9PkB?10GL8dyXywPLeoF{RvL3zU^&$+iC^fdb31i?dvdmq}nx4V;-4G zH3ci-ss+a7GK>PKn28{{jaj=rN#bdE#pt(3zq|}r=b`rSr~m%+f6(h59QC|;@J|?H zJ^kN#@$Gi;{r7U`yMNLDKj6PVvl60DF9#yZmwXn+0UJBi7>mX&<_qStD7;Mlbmg($ z{1P4u84&VSvP>DAohv`%xBd!VDp%nRXDOd28Lv70joBm$d7KI6PbOU8Q^=PjpCvK# z<0PB$l+FFgER1=JB{w`}x3h3E<M3Ce?iU!AO=d|V@cE%8(W%wi_Lakh9uS^_%x770 z&10)|3tl-`rYAC)@yRv4-C`US=&s-b9z8WCp?xl}E_U~-)@IWrz4g;Tuq2LF^vFz> zFupGrLIT2K&I8e5<E<I@qii;AvGG>An2cL&xrh=!5H04XSvUo9h(OdBOZnfITx2W= z{i`?;SvV0eX3D2afGdne1}{00EYk_X>1PbCdgkMU8deq{lIsjql$zhBA%I8&vn-rk z^9)|<deKzLEKQbIGwcM{xdl&1^(K|w(q$K|yjpF*|Go@UF4%Drz<t5+B7>EV{EVkw zt@h_XvpzvxtBuFwOJB@t@nWv0QiiMJ@wis|iSq>uGr=UM!X{B7cn%ba%3<cQeK`|a zE9OgNznwuVSzRo+e=V3_3&QD?r?5+d(|xF!C6gplL$%l?pC%~5Sg}bmp96-Z%UIBS zhCIFDW{C)O-;GA6IKxcT=<a2AY?5+6;|DnSDb&TLDSH5}XwIIOO#b<+T5XcVB4dK; z?pF2EfU9k}5LwFo`D<VBvoz|k=fJ;owzhU&`~|K=Z>RI}+nw#_EvxPbP`LxZ!^!k` zeB*WB_Krsv2fd&B`@M^UZtv*i_+oVOQ}6gIXh=v>((G+_)O+9k<zoM^5A9Wphvusq zLGMMO$aviNVf%;e<}2D*wNaBKjzQpoXVN7`s)*LN-KOnpU;2W#mubW_k4BWp<DdNb zBI3xbIaH_yJ2x(y9X#|2;4u>%QG9I8a}2~LVe@1eXVzAO*d%8BQz($h9Rml~0cgg6 z^X8|3PGJFBAb6^c3tI>Whl}hGmxrf~LWM?xytZgI0hB-gnY~V7L`}OifvlDEK4vpt zusBgTluYp?0i(nE8S`Ut%TvKX;1l?1z{V1sF-uSgC5fmiuZ;&>ERtAoU7-cU1CED0 z`0YtDU-(&g8F4r%SvcpgBGc5L^V=l7CQ>EOAFSsao=s+~d)j9cKdu3#Vqw;K`~*Gi z?9gtN9G%~KXb2PWNA6@0+Sj(WSeH#<a&3{VBCfW8$630<>vbu3z%Ezp^%9T?|9qFk z3D9o2m^I@6S-_|MGJ<8m4)N@LT6r@+4kFGT<av6Nq|9gU`Q<PHvJT5G?^Z5^f5i=| zv{<%GW5)f-3?zZc9571N3S+#Y4JrU^g2zD(w#&kROMi0hvC#~q!`Up1uV_PQ$x|Mo zKrzM(dMxiVZ<$785HUl=v%w+$E3Zjd4v(F=H`5{s<IJ!SfEysC46nl2W8JvM*!WXh z-o!j;BLN??8TSLg_8Xq2VL<md6O$RAbI*oGgjca2jX{u_0<O<^Y(sRFz-<n6g&C#^ zI1ND636H2>d8Noc;XKqBlc=t{5?QiX@POU=Arg#flCl80hg<a;Zn$(AgAg6yQ%lS) zlK>Wx5YJ;>$t5s&sUYV7fPgqRBEh^!6jy>{Unao{5G~A@pQhmrhkI*GSTmNKPRj8Z zd#snHNeaYx&T(g<;r|$(9D|@8q0tQ0%TJ#Gx4!%I2?#^B6fMR-P52@M&Iec^L=;*r zJx9d;7BP103bxi~i^vaSHa<==HcggsFlH~eUm}tFGaDdsTI-9KYZ=vhz0&8yjEgUI zyE(!RzptX?(vR2}Ptw?9r@jzutWGylqp`sRYF!H@mzM%mKF~G+>qN)v)?b0teljDT zIa|gtkEk`>Vo21I1&>M2kCTu$mL_z+iCob*ZG0^6{KX67jHj_5eJ<THm2nw5tRa!Q z$##E~lX;hjY=krPfF|e~MD>sk<rg#M6AsAd5|toGxc~|X7jAB5M04g#foh&4x*@d> zvg4W=H<ZX@T{ytF<owfQ=Eql@wy@%)OC0N3&juof^0dMZ2_8*rwUd~;>;MoiG4oR* z-Ef3fvX#e1@*QM%C9hl9(E!HsqZV8ELe$3gqh*Y2F!uwzmi2<!9jO;V16KmN#nXvr zJc;F#qKONaav<*lk734RUve(8BY0*oU*I!n951723|k<%<xsL=K6|;n^ZOv@XWymv z!gQD~R`JBeL=xo?Q2u0stl2IQ>QMg{-ZYGZ*Q@R#{E4qrYmY7l;a%~p@tnkp;XhDI z_`KPCh27HP>3;Xvom|6y&{1E#Fkla?!(jEG)q{ooxtjtGiI8}rw$OdL`11KiqX3EY zGkW?Kwhmr+K?`}=P$clO0^%;1;Hc#j#1RoyqSoTMF7#Mebs0i?w$t8u@dJx^mhn`y zSiGEH;vYehiB@eH!%9sOc+@2+o2?cz4xQvK=!u`ED+@;XN*+6l3ruls{HbkoYHfT- ztGgQSF6c$RKSSZ50o`W;9=LEFBOiQ1d~vOIFaKos>_A=w_w0!KsFS|0-LrPP&7A+Q z-ItJ@d)%K8PWC;6ugIWC&8Mb1@Vt|(oac5V>4VeF$ZiS~BJqK2>ChFcozh|FNW?wE zs+t=@`Uv<%D8$25qf~yRd`EE-b9V0#K$qH}D8!#j{35h@#ZtbAe2_P|FC=#zB-Ovq z-L&kU4e>0%(4{9q_A~cC4HlEvNw9jo%D8}rJM1WY&F<MDusNxub&5N>;=p*k+}_rl zN`7O)IZu|^5q9+*n};!qY3|u!5?>({_(+NKtQ?zzY|FpqwtWFC2hdy?1F-NN%5)^z z1$&cB)N13my%AI59@g+9Azxs)Rb%IAs;V_fP6UrNxpRFW`Y8W5jLmZ+2Ei9CN>Abm z?*|~Vi-Vd}?AyGLJd!jH!*gvG2QE(*^IKN-cmys=F>5iut+~U2)QH~aJkUrf4GmBn zM(ZKhrj0^2q7>8$CL!f3fW={aRhz>PDQFS@(o6q7@_*F&aAfa6?enKUPX7Pxw>t&- z|IW_Nw>$rm|NjC1ZEgKI<yUaZJWp!KwnRGFN_pVp>GL#eH|!k``=kEPy^EiEzg!G@ zY?o~{kPO`mK1q3Y4?OE_k_PwA#eP3YlIxJ)`-||yMf-g3ny>Cj&aZzi((uO5xc<Eq zJjEZAB;fZ%1I_Q0Pg5>t^g*-C&8_g2hID_>lM+S==*P>(UgwhgDNjF&Z$9|#e{8q^ z&AVuye{=5#lZ(b)2kGs76yE3o?k7>$ys+ywKVEKKS;MFW=?;hex5r>AWQLRNF52gt z(7QGH@-_RSIa`A?8z05yhuzNkN3q##)Ia=MKi{n1KmYLS^YhK;_aBGfG%e8228R+- z6hQ8EK5l*7`ta*VQGfo|AJ4zBY7Tk_-ThJT;9@ued#H<}?%ysBd&h4_@7OL}s3r9d zdV_&m?}wd#d!Z_h24~0nT`)<y7@oa()BhXWWp&T<{+gxB82Pv#>#u6lWlY4M5b;2( zI1c>GZ-nt;nKjv6Z9{V6RO@FyT5>9Ev8_$^?-v)RXM^6w1>4-h`|CaTuZKKq03ZKQ z+uGb@Lq86)@E`E-jM*~I(xrf#N@VGBk}ZMJn)vYMk#|s-u}PW;5Oko<0JT*~syL!c zkWip5Y;(&@LC7&sda@7QHLz2hKsaTMuV{?>dj{HiFrviYU+wO)I*Cy0P4(uuNy30_ zzp8D}YEJw(2m^SV?oz8))TQo77JEu^hn<4yVtiGH5eqfEh{spijP363vhCuq&VuK* z<*v4|A$#c*J8soBHayQOKnD1YqiL|d{yJBzVEU15<6{fHb@E-nW9#mrwgIoKhdcri zYTUs8x0v8OZn0~=dKbooMzCa9h=9P84c5kdq3N5|fC6`=O<gq&U7H{n;#UQN8oaEN z@_o041o*v~FQ~Jou2cW8AEuLK<frHLSG5hS<_CeSCL4Ft)L(g_z<={S;7@PvFB%P$ zLE}@}UO_3DhOXfdNi0m3*<uOr>$`_nwGEJ4HmKmkHD9$zoP7?@q!XN#2OmOi;0MG3 z*z#A5mvNCbuK7yV*`R@-*K;5n9~9NW`ECF-Ss3FwZ9LTS)hp(Kn*noq(AZ7wO3<i$ z69G1wZ<dDhIS<&bTuJ!dP%HnHEY+)x<YIsw^I$E0$e(aBD%S|>Te#q?O~kETqPd)< zk!UE8Ue)Acv0YiulMlH@?lx^XU9WFgdOx;UWA0~@8BP+mVBx2JmZU9~`nR|{EtX8D zf@koiLPC%7972&$4E-q|@@%;{OQWs;p&b~CBk|dQ9^qfpDg4s?A_`I0@F(^TsA854 zA$z6AI*?}QJiuq{Da>n@L;+75s?(CT^7r`J-8El5JiF7-JeZXm1H!@Tt-%FaPa7F5 zgnR<>D9Wa=M^dMNr)k|BI>J;*VgI_j<|Z!?BZ>8MKfOlf^Y~-@?5=RZKYWbGrgF-` z6@Uka@|s!4IkaWh5<1ff2!wP8M}9W*COnK9xxTm9i>A9DG*<xzgpKW%)lKu^*&U7b z;MQvAJ8j#onh&^-wZ|4R$2t`jvJ%2?fA}6iE3WCdggW7gCYoyTM%1G^l*IY9SYw$+ z7Ii;MBl@zp$KYS5_qFGsFy7X$k8so<J^0_Vt&m8Ux}BT>D-OBRb1Tf2zrAa|^0MSG zx#j7;FL<L#mnd@D<_?|uk!Fy-Qcd;Y^psenQSa27g+^Jr(nkh($+N+s9$wP|Z{cS% zkcsK_-uk%p&9ki*tJft=TmB+!U-OkH!lhTRvDf+a{^Mn{vDX2SP&nZqx0-wYBK#=6 z(d(uLM=EA8MtvqahnMsNn*eD-@N+{xlwy&!scPC%yzi%=j@e~u1vG1c>q5nz;C?!p zHJS4h?(02uB<i^q@c2Q~r2Bi-N<EC^&7ZHh%j!;N8@XnM3J+%TvTBw{vRSc4B)P{W z+S>Xvj{|xs*Q(ykQhbsLcv;joQLuX*gIEnqkZT343iIhqqI3*&e59h~@p8^nVCAzJ zpLdkrC<%CN6Wy&`WtY!pK+}~G)>!H{fzZE6()}dhB<*wi8K*o;L(opXNm6={KutD@ zd?CUq$VzdPnU6!Hd;&Vv75n=#{0A7cfM{;UqeX3#UGjJ`1DeSi(t>vD;sP*JTwH8H z9kiNF(m4nN+qeX6)n6O`f!78!@G{Z#0JxuEwYrOi1z0Fpz(Fq^km3TJmVDq{$*Ke~ z5&8d#AA=J4l7rrm2enNlIR@rh%gcd?p>Yf<0_Bo}oTIk6C10pN9iO~EzUZEwUi1co zlflLQ$w6<(cG-v8M!kRhbN8@+aM2yS9oAd5jk+`xIzAa)yg4~LKCnObk1x)KJ^V5{ zIXS#I>JPzD2R}gf(5Qbr9CZ&5d(`mkczAYtdNP1p<MgmQdUG;3DioZY_6B$tU<r0| zbb5%MO0t{%lcS^V@xjHLZeR60><x!-eYk+09g`1J|3754?$PW1+q09i;l)vRwEqsj z4SJ`C-Cr&a`akyuZ+pl4G^0W9wA&wCsD68YJM9l-C$b2j=#Ok5&W>?y>$P(w4KS<x zdH|kRyKK0cUnY?^P0~i)v7lFY0X*l8pEC6uUcP|6<^%tfp%#ID-PJb01R<QT53=u> zFVM^|p73*v)X_@}@CyjF>P=JWH<C<xXg~r;Ro0&e@^I{>MW@{6K#Ffg`OE{x-aP*B z{i(iu$Su)1Hq27w8y?`{5x`#oa4C_1Cz%uj-~xjfBS{-j349poA<ksw7KsdK@?yE* zX+u^)pygvN;L|YXr)jd_X||G6%K_D5cLZ$*A!EqdZa`J}yvHM|zsd30u7bqWM#+bf zWjxd#UP*E~mUX?9`vUcaQ>0L&hXI|7{F|rDzx3lE0b7dy^S}N#S>Y|Ba022Rw8{Zp z<0kl`lH4W;h155A47B+;WptxNB1053@>ifT1vM;^NO)5DwN2S$r2I&U953T6Sx&%d z6Zl7Hog@p<0-4cG2+9>d@E4?01*0Ag;Andl(FBMVa37(0lu>K~0)e6g7QmudgWmr< z>kUU2``zRH-eK?HV$kb?;(r%>T5wG+2udE*Usbg^>J5k8w>_&7d%V%R{_zM}z+I<W z-TFee1WPfHRVJ|?MJv`Q?n7}8nz(5(*c-sgg#xu+JYdT>LhEo)57~<j11@76Dd2%u zR~Q38DL~o^I=qNHjZI_J;Q0aElOZ^yaMl3rI(C}!i2DN6>6_9jNg4P;DVorv%S8rH z`ox%$(IpvzGL%$Yuv>tjJeq<-?<N{M$=Yxg%bN@+2T%1q22LobaU?(8lm}#l0`9tC zrDcm4NiQc2XZGckk$g2u&bs1n_^qaLjl!K#i6AW`7cjq0IX*~1pBfm+A1pzq3Qubr z4+=cz8Q=>I9#7L2Fs%`oodIV6NT;q7{T$$FO19$h1n6vmg!43gRa(=n<}8doXbXDK z@=Pfe#E;fXeogE8S!^+cb)cF6NKRrI1J4vGfiX=TBwPFwiP$W8laJ$oVT-d=Mlxq8 zEmhT|cduN8>(*+rPF~<_oDKTGW0S1&0IM5RtJ}5e_^6Z<@az)ZX_Q=@`uLWw=ucK~ zyOJOCy`07VjUPs6f%jO4R$UZj<>x2{d_s?jl>5O!{`-JWRiAs-%w%cByk)5#4cYig zK%tbTU1adpZo%DZpw}Ee+hvzY5^+B+^s4Jn*QJkw^4`UA0ap|cPU32ytxDFiP7EkQ zwq2MdJ>r?Kx-e~&+F}>T^d;}bX*ikf6A^ynhY=5ssD=$~OCfun;jv(md`5|GMs+0V zkvi7&2nM}_J(AIQNUCR4H*|1FUx6P<K>_t(Q{i9xFu-q#m9nc6e9rY-Qo{@aB)EXc zxekgIoc0Ela_vlkQ{P68TnoF%w2}MLad4xy@c{IYHtKEfxHstUU-bUgA6YEQ)~_Fj z-+*rgXzS1#5&6va-pAmZXIqy3@G-QRlyb)UM!{ZDZj-wojntQ#%6mmq1F0PBeF`&r zhCnC48<#YsnSy<Ex&Trz4O5W~QBHdp#$0KKbl)Vxb#-PyABa7cr5zACDPfoFy8`I4 zH6|I8dJdQylsb|PmC8^{&eS+10~ryg{Pxgc&+cRc5@$-aI*EaYKlM*9PP?N~Z*VMM z-&?-|Cy%QM{1=bW(2|3eRxY=C*h@BTD{Mh4G-rxdP>8KRUdjRy&X*C${*)E=6mbIt z2jHR-aT3PpheZGu+9WU73e6){LVSGEANDRz2R%}1Z~fZX>wp+ExV--~Ntf}x2<MY5 zYIZ<Fzt`C7ATaa`j2AS{=J%g=6minht)>1gl)^KJDtKZMg;}HiF$VFa5!yviYgg4b zIw?X8C~Y!ZbB3C@l1=cyp1sjn_+ShgqwrY%O~`^hHQm1Qs10?Df~dG|C@9T?sIk}X z*+odBA%3w91ZS*K%Sc9FY;QLoJkJ}mj)liY>qqiFixDfo-9TGtI57E^H-{6ojRxN< z)x&I0el#o=nk3XV*|DFd$!*RgVO*fqV|*tiQ?fq24O8@@0wEW^j3q=Msq?R}s+5OC z$pmp8KqCupI3V!I561NVO^v;+_QPHHE3mqSXERunYR(cm2k|Ajih%TO0)8HFR>1pH zxlU$0o#!4cl`NGJ$>8?WG8yZLby{+lS-n$+%`J6=`*6ca?X0VNmo*Cp60+E|w<{C} zCjC5i<ycZcSGtd<*;t@dM#7H>FI&s|(b5E#GI_rhPi4335cLiD38<)8JY^C0!6^aA zluvHGQm!6aQVdC~TM}oeGQEB~;FE#2={`%B5s0kQ<d%%u=^+dxda0slsml1`PK2^7 z54ay7)kA@ReIwXTz^{{RR@-D0tmKk2aJi8#=3t2L%btDd?}hL^hnIMA8S%Lc<E2aR zO7bH>>J<b3DH^!83C6fV2*JA0yBJ-#p_5=ak+!}{poP}Dz#EQP<IgX?|2ME!z6tql z(-QUckAW6<4=+v+&yHS$TJfMa>h=%88UU}!IvUh27lUQQk5GgZ)PW_Zui%<<AXn#I znAOj%YiAupt`DRBP=4Z!B?vPj0PsGvZ?0>;0ur@$Doqw}ynihl4is}*A3YSZ<Ui(K zm~NqYGnRh0IO#S0nirf~LvzK25{vsaM^4IDK^G`h{Y}@<NiJsb-0gz}dwSz%6j1?3 z4Wlqyu|*PvlNBB*K+V$`_$a&}xgqIbAY|V(1B-6tZBZVDj@Ll2qm^-MQ)nAPv;^*_ ztl>+E9FTjloJ_dohhPo@h(y7?3ZwnSEA;xrYbMN;V3kMMNt|TOS0L4+;YmLxGJj>? zNVQGn_8~bxxaY*oUjXcE8IvoQ77pHWrMto?Enp}qU6{QZEZt9hMQ&Lo$|iZ?sfinf z5_oJuR0r&p1bjf4_!R^Jv>X_X3J<Sa6eA|fC{XIPmFfUxi|Pq=I}4G}aWU~1ih~8c zAoU<!K14ws2b&ybju?0}P_g7LUjKrMD?EyhK1zw`t&<;$!eZdd$CehBYQ+raFQo(u zLrETNA*NbXN$l~}<+2_c^Dyioxu5)>|MkCXo0ew<v?gghaV1+MJa3b?FFFC?`X<Xv zgrx8+g?p^LOywSvhmzTPKZA&ed3L2tNRnTs>ZRN(-JcIoc<!8WJz|N(M-L}%L%3GW zhm8pwbyq>z(bi!5=vbjHK|j4ha}HoJF<1p_w?Siz>$^mOi;R*WK$@=lF6t1?0E1<W z?yK~8fvp8iyrX)NZ!Ezh41Z<!?5Iagr!p9b@p@WBeg-OKmL*9<1SViS7E7|t1=X;Y zgoH6W?T+5r!xWUYW~iTe3g;8`#o$4t!41KeC&_rQWgJ<^+nuH%$nMJ?rl{L>*U4Wj zFvbn4JkW~|ol?PTTShv8sgh>~tmNeqefYzG$f#TtXyh*z$}|}k-v**wV{8B!0W+l# z0?%7!EuOkSXqMK>OF!bOk+jk}H;~>qIg-8*^++Xe>{p1IG$1Zm7>fmj`Y>?PUO8Br zS794Sj+RjtE+TF$hvnu$Pyu>$At--e0)=r<821LP?x!JlP}kM<+tcPY3N6J-dd<pU zMotV9IeGA&g9{}AHB^{kP+bVOV~<zV##_k6{Xolsg75}n)}lsBOj6F{EwQ`;XErjS zf`w1{!Vgm=q(<~gIV?JY+HONoH@!nsP)n1ySq`8wLjZRr+HK+A@HQEWsA1KUi&Sks zs5XtAJ$#&oF)Bg=SeQ1zg`$HM`zv+yW3FFmA;N))C<W3rNpL}wK%o|vd|$K>C}^?E zWrn9E%rG_ssGGrA4txdV*d#haCtW|qxN(g*X-}f$@)BbxOqaq(g(rny5-XAMIWaZO zR~oe~0tB{XTP8lQXpy(@lU4$~wX<|(uR-2Yq?#5iz$H(5wJst*21PnVUx7uC2RMY4 zA(KG1;?7#<B&9Zb0&+{#3i#!c=(n1>#bASEo8n(cWOm4nWwT;;jwXgmTruaFANZNC zoZXcJGELI$kF<!UtFxGY0*Ivg8p0$fwQ<E1QalypPnnxV2;p=v$a5$Zp^_K-@3t}p z6EztV=B`x9hv7b2Yjnm?A#UaLkjS&PjFAoa69W_ZM|i3I>%Tw5|5Hc({L>%k|M}ha z_P5)G_`iR7@$J9-KmUOLbnGlT^v;QIu?#-y6EA~l)ji8mxc*9o-<qVWMV5d`LkhN6 z^Ru)5fo_7a&ZvfI0j`I3Is7)I^bPJnu;;N&ZdJTjG`dv;dP`eR83WvX2r~vR=j;}I z>%rer%Jji_hk}D(^o(%MJ@(EQAgW-O3B*nTqiSG4Qjyg*G5V4;Ji<Oh;EPCB(}q(+ z!E7<G6I;go?@K>wOHmHmpo0lh2y7pidCgFw31JVpe=U1qH=z%3V00(@)k)vb93CO> zLEjC(#V#%3?#vgnE=FB}750d-!+aWbG4o%1_kF$TEf>J>HvFa+gun!%e#JC@{j}zn zPIGyE$#w=*0urHWQST%gsI;Qip?+~sWduLK2jhM$L-9dujf>Y^P#AU5x3um=O3;>S zcJ)#3xO+Uhc-QM5^aju*A7unXSClqA?v37`41T)k?(g@8!|E<#o<TUTHaRwzc6EC8 z`mn#B>k+y_>G`A$(l^`l6%D{B)WW+g14oDk#56ohqZWnGvJgNY+yH};>@r!?13TFF zVK!^jx4_gxeP<oekjWnpTVvOH2@Lyj1mnf<>&LBc$cv$s8?5Qr(LYqe4{=1^QD`0V z8ZQN(qL=K_soBf<&*iFV88mtwT1peg{k+4*uhH+DJ-f?w@Gu6wN&y?tX{3_}%CSnY z_Z46H$NRl%<WPoYrD$<>Ivfpp-J>V;qvCO_?@;%1)F0vt_;JA6^HAVJxB~x_b@P#| zHdstlV66s7C5-umpZT^l40Z$Fkm+L5Vf7anntNj-L}M4yev@|x8P>GFeX+g0gRXg& zQOyQ)N)8+kl?aA$OHN5e*^oXnEfb<uI>i|CISanznk1qymTFns*idaj^R}3r%BZhD z)7K7rAx&P47mkX0lDB*@B^_2$+cBU~<#5Gy!D%Mfm-=pc(ltx_LG#tu{^T0%;q%r* zcA@FfZByE<Ld1NJ>bvPo*VNwGYc2x1&r!P7rl`ejKaj6qxCo57D}*ZK;w%Q!N*;hx z3$!;<TsXw0jg$feS!4zrz2<-fprjitCa3sYS5#z%sujGmltEhNs~%7b1TIYeTzF8R zx(EfXU+{WGk+686!ZJ3nTA^<(JT9M#9D>%Q3996(n$Z0op_lsRQsr4$vjGNn3-p;z z7n43YUP=26>1W|u47<KCAg#UeqYxyQ0Br{ZXKoB?5Gh=P_Vr^O__~jE89u5q9yPON zlgg~15Ox~vXvrSAt|E_tY*a2eO0_7;^GC?t@UadbK0^M6+48bYq8`y!)j!rHv|gRu z806ll#lb`ejVi*{VYqgc^zHj?BpfD80?8Hf<j%T%v{7%8HYF8=`BGHP4#u=9L_8?N zZmq%9AMzSp<?5%y7HAZ={)B*n9y-wJo|}7~JTj$zba^ks#}0=<yJfB%EshDpi$w)f zhO*YVC=<mhlH;)SusO%n@?)n?rX7()FPSXd^APtD2KlVXZyMDv_uvJ1*ZL2ARTkNy z+<2p!{7TlFVh0=EyZ5Ghb~r*0=E3Ry#p{y;P+*PFEpBJ~<qzNe<@;B;x|**U^^bZd zXQPWFsQfMeF2}Q+4o?2|i?Ny;Kf5dC^pQ!WhHp*=@4JHobRQsp0%CBKj~+_1<E)LT zTUyqK6g0*7<&!?{rk3>$$Pp&R$$f{XOxd@#A6+GBn9b%;TKV)a7K8Q_hNmaT!`@Tp z0FFVBqy|#!5F}D}&<=zA;8mPp860P)hbP^Gi~V<J$3K+<RhO#=OeCF~Hn|wrOL~K} zYHzyxql<%+_s7_AX_snJblMzal<FAgWzPn<o74}iEdxL5M*9A?IImqxQei)5(G^fv zLwjuuhg=b7bYnwPPE9*0&L}mrL%6G^_9F_ST&Tg&=26p7N%h^X@o3_ups#DGL%^nG zjB-^@0DDDc@A&4X`m)k*iAHb2EJ0r~X40n>34I_S{Npk4U*+>qP_9D?X-d($onT9k zf{PH?Q-(bO`#@phjGzRnQTMh09+E?VtgGpP0;bUbv%>Vo*;w{~tqb32I5a<`F`6Fk zZA0y?LLs`EIrv73EKODvCPx3H>IIi2wkLTrMcxq$kZnN}AcQr{-x`FJo91AnIcg{l zK)x5fRero-k^)Cio1`ZVX+ERi?f+ujIoDcKQ5LZE1q#)y(m4XzkH=nO-;4obzh zhhnI-%C!FyK=^-c04jkpEI9DAdK~yM;-9i!3>;h9XnJ5|9`Ht82OtLth|PYzBrXxW zK_x?^R>7KSx|qN;sa(e)raK?Of<X(j^m1WNTdYoL4U{h<26l!>`AI_I-96jghRhOm z&|@o(E2Kz+5H^KX%|C^x54~2)Scz8b$AVg4KlmRai3h;{8s1R$L>+l5N;2{*p}-9S z@l+U(utas1t8R9y$c4l69iLGa&s`r%%`(JfCsQCY9+i?ls7frkt4%EM);qMh()B6x zBd$atcoyK*G=#6XEvm?@>)NF$!(nF#J!$8wUeM2Y0_&ch_WG;bN#~r(ECLQSA+Dch z4}aqPVUr|+kuU}N!!00tK^d_vN2@*yU-jcgogBNho_#2~8hwk0%?eixK5_NB)%_m3 zGqJq2pPhg((&~Jbjg0gpM`?jjk_#<SqE3!7A-_R#Qd1;9poj{i)SDvutsJ%_g~`cf zfKK4c6bntDJblh-E`}!B^VDZ{*Vw{CZF87&ZVkC&`J_UJBpnG&9c-lyS?`K0$)3Hm z5>y^wFl5<X-R9y9;18}{qRLFAtTw^g0U2sY%pilGgU%hg22((AZjtWWIt01QAOR+( zvCILs>8R<6`b&=s9edr9Z;(m*C676HzK;J&BmdX|7YsBIl9|H52FX<kt)ZJ{UDbgI zg~UJG)56?~hBU~C5;-A2z=2Ml4f>!<OJaz@M4AEVYqV8wet2<C=cVaUY-`jvjGx^p zU>-W-CqyC29skef0t~P6K?xmE8Oi-X3T@eg`T*2&f3YOF>A60_r)e?|1@|C=#|Ls- z56)y?1WEyfX3OChrO=qTMYl+$!os53#(AY^sZy-%ghQbxI?yOH!3MA<wT%ru|KdYN z%a08QVl76aDXBtXJD#!>7PqL9)8vVx6aU?zjwRZzd?Oi!ADp5k<LJ^+<S`xciJe+t zqw)zxRev`P`FBF}w=C||Cc3K95Puh!KoM0%IniTZEw$Wa_=?ZX+)=~Yd(0BB!55&L z6#DYIWBN$0Da@f{RP=ZH08fDO1NWsp6%nHB_E&;)jR$brTvjN5b1PO4y|SpiUOwb* z^^-$wBeW`VSgN;*-OTfodJI)>@P!zwpjwNsxfck$l%C81l%jB+EvTAy1vmXU7%dqK z+>1TJANZRj)yqZ1KS5q)KbqD7R~*j5gldHRaS#G%v6A>gi>;=U-vqr>O3|&=DkL#J zP5CqmuVz{4MW&7$q_NC+oNuBolN_sBfWd46{7KRWONVV#Tn<+P+>akn)<Rq3o!c=u z)ob+QB`hhD7FSKLr;>*qcZ)Dgo$EpsnU%}TQnBI0Cv{`p#H34tP>zC(p(9Kh7A0VU zFAZZTj1O7L*eT4ytYfFi+QhtaAPWjo%6Fy*(y^Q%yPOl@2XYd|ZQ4N@FbUJAxs1_c zxNHf1Fc}op^Za<#_>g0#oeE$dlq^le#X{1MCfw$>!0n4M-LmP?=f=6PTu(X-g_um) z2ha1WWy&q^#l!yd@X(;=wzs%)6fIzkofiql)&AL1)1b<QEVtsH$pEQesVWe=+Lc^4 z-S#=Lwz>l(>4jIR6v0|YLbg{T$FVD19;0KK(C*R;MGz#Xe#t3Fhn<~JCc;H8EXwEN z_ybihsN%jQt8BR9`W8tuw<s;{mKA68iK6&dl|AeHz~#L@E|ND(`Qf~j1$=~}uDkw= zh_COi0%<Md1sqnYfSQ`6a&XAip{z$}w=xFBsrw6XT%u?XnBBgL{z^=#S{HJy&`VL6 zZ}Uq@b#7}a0if$(4FE7c2rwxys>Pb>c&`>$3cXdl=b*?s?y|LUqL&{^UH4^M^?;CJ zmo!9DOaY&z^4D?Bx4r{^Rqmvp8$zLT8X=cyE#U#4PT=VZJ?nL}WGAMh@}UeVce_*H zjb^vJ+|l%cJO@Mbl+`2#>{U(0ZUnE>tGH2p)-069Ev6+4r;x7?89&+p;*ek{`5MVX zfc)SRQu^7;fnPP!qGC#`ghu&vaCY`Xk)VRdQT^mHxVvme5C%Ewyp9dC@jkyr`4k5F zimf|lg`@~AF&r$tOx<xSu23EOQ{ROjpwhV&ui~NXwkszC28R_vp|^<AT|Ro`l_jXv z{INjE_OUmiu*>d0>x#+=RS`ToliC*5RWaGdA$@RJFbfPOuX^0b7a5kHOwGXh-nTR* zOHDUJ=*#Z;M+n`qSK7UfD`VByQcmhvCmlxfK?A0H^%Rm5*PDAvzSgQYKWv{{hOjx} zWv$Zq*<Eft6LJIrmnMwHsJ9~nlRtE}ww~QtRY6SjfI>Sxke)Z$XxK~k@a!(P<ro#; zG}QsXoq*tWG8t8~!jyQuZa-(wSx7RfTgxwN1WUOuB>c%`BGujD??#pU$oWkjS<ku3 z=cAO`R}NF6rKnrqXdpK`YS~B_xX#rsnu@ss7^m6-$XOxvASxPw>LR<h5*s1AJDFj) zR1^c<G#pC|Od)9=26-ch0v5;^?2&)Tqc0A1uMQP49GJ}()!dcEqEok(9#Z46jEzII zYJ&u-GGu*Of&q&<+omIBDwdq>=2+QoKD)#9co>7I`~&`d-jUbX14cR}OB2VUFNdmy zg_SJ4H0>|Vj6?SPPl1$FK?+XL@T%wlo}^eLO@$tFw_}Cr0`hH^k+7z7x%&s|X%Z(P zH??;Qt|%rK&z4ermEDOYc5vo$9B%<}b#9!J`qEsHvXN+vQGmLMHP)C&NYe=C_39ch zsGxqvuH*D%I8w8-dM6&FtoOXaDL;~a#$xoX(vq!mP(b|una|^j=1*3dm9%$NTS}fr z*<O_%aO<z~uOGK0Zi%hG>iAwiZdr;%2nDtC!}d;-glO_%<V$yRh_xf#w$YRt!uq6q zo@AOahG1`SG$~2%SjIUTlL=Nx9&0)dx(tp*+DwvykUUSx{4fjv1>j;lzTr`_;HG(r z0XGI`$ZIdxuB|zVf?rXPl3kY9Ihu7=mF$pe-vb3!1s`@OVt!pMe%&ekR+g%La7(|F zrPhYH6<(YZX?MTAR9~wOU-j`QJAA}!J>~p8MLu5Dx;WyNiofvq(4Qmea{4$cJv7#u zt(lT-Ze0Z4aQa2BAzN+ph`PqT9jwQYbwQLUFUxyHd8O5dVU5e~nmnIuHLRYJvJ*N5 zQV|De*zoA{SH(NXcT)azeTw^<bwn#W*SRi0rtx8oPu+Q^x)UYxs9KNmxf^kyt15F0 zD84`tUum2E#Zm$F_Irwe-9T^Gt+YqCrdwgNh<198)ZUeo=A`yHiEFt8wVbuUoB|a< zmpb9L9(S(Ag|xF#TB}x;j`H#g&(WiM^SLmTYXu#QePqzJQlih$r{_*d!M>={)Yr%! zoHE$EdS3Limf4g-t%5_7di_>GXp0vJ4wA{1nzoc$;m-X}mg#MlB!^&PD+7mH{!~`? zhUGUxAPS~y34c(Pbj34>)&w@j#$tE2jIV*vfc^!gbC>!JzKbqeh)Azq*~H|IL82SE z^{6^Arae4`WI2Her3_V@>@wy4HGDGNf1}=Im#d6ZazoX|RQl0b66u%Vj7CaZ8t*CZ z&itaPL~XPZ-eM_c=G|?CpRbn?R@j>)kTAMIl`TK?`ZAmT0JO$oB+6_*)vcl~fr87Z ztf}16T|Fc{cg@)`#?vn5VLdyvCkXsY<2u+4a?s^9K2HF|$`hi=NR<%M6>gIPOfh$+ zwVPzo;rA%HI<xpZs~Npq|6-X&0mSPeDwVG5gvnNWBR03+{#2-_i?t$_b&=UtZSgFZ z(rl$Z18&R#{l!7d9LHavU6+p%)tvCDMDM0$t<ss<QPil<dtkAQhASj(bcRIHSdL*3 znLl5^{K=lw>8+Hi=F~;hz_K72g0l`rGb58)4BL5&(LJq4BCI?-O)+5V7<@a&Xfz^U zwW7twYHTV%?{v(%6-5GDVbm-+h>9o|ii9jjT9{`Eb^**$Ah8pMoVGMag2xm?)ry0h zUkY~(V8x|Jwrv|N|D+vK9^qzxd{N>rjyoh9Qv3mgzpudWu)1_vpgogng~-3cls>sG z@w+-9_(K9{*@HKkQ{cdDn&sgr>7Y;r@~x^sDozrof390=j`uGdWxAHD#k(aZEayn< zoP|A4$}wG=cc+f5B2_^p3N!j%0d_wyFe8KEFcy+!%Gr+QC*g$wkN~BaTbtXo;!d%Q zZ1QiRYKm=J^hkuF8s-(_aHh)LTI<qwUi@I+pN@Z~1L{*<Z_pb*ZzpaT&aa9Lr=yQl zS8(5P8!egqn%ApZiL$=b3;6!Nbg1232Xq%gEdh&ze1H}i!NJXDQC-hEl$EyV>tevj zRhDiV3StTaj^XEX(F~sE?orYYn(t#OxjKnCFI1%q9m0k^G@Ch-nwQ)E<`~shNgHy0 z(&s>a(<QGnZMk4<4cEEC=|x@pviW@?O5`HubgVcWxQfI#RY#U^_rgsGTi#c*JPw8Q zmHRwLy((Bg!#mH~luF;|$9%@^ARDhfa}K6QJB$ZSz$(K8%<c?wrGz8S3yJMvfR7IQ zPKWs@kMmh@du~1I30+cyT)zJm)${+S@jo4J_dhEB=l08;moM}2KX<<SSNzXEHvT6F zqhvxqD<mg98ck5nJGKZH98>F*)B_(kaG<lIg5I&w;c%Fs{hrezi=dVpe8%8kCh}6c z$0I9PB1uQ&I2v^Kds4#8;{c=;LnOeB6y$m@w_jR3zsjkNx*lm5suVKNv$j|OTeRvf zw=23VIL1++9g54)$ddQ`QcX|Un#!;dcq>72KNv<Pz|A>py;?75*0r?{3U<h)7&|XP zR(`?8qlD%Qjd(0&iuMfUgd~166he8h>F@bv!|jWv<=oySD@hZR{Aq6XdeRm|()1q~ zWRp*Xfrq1dx`3)2Oe0v3pnESSCGbJ@Mt1&WTtvafMBb#-!30ZDjd4f_S3E(;Zbj-a zCty$70M+4bD8t=4$aXe3RKd0Q-_ej6<ZxSH_wwwHhSA1)`uTy1cBr@W;xFE|x9#oJ z$#_p8D!eY1Y2+;hgvy<B^&4D^Z!uAA>BC}~Q!}nXPqP;4<l$P#9{`)Qj?e*TWgrY$ z<10Hvi2V)q1$nc33j*Us=P>vPs->-QT;53V^EQdOaTKCyNM|<%p5^b>(FD@NzICVT zoZyP9XM^;a8pg*IM;1oN?2pE<c-NBN*f|uf?n6jM06DYQOItqB_(u2SFCg}@Bp%R- zE2%G2Kvl25SrSu6g&hQctbGU*oM0>nBTDw6q(4>6dOy~wJ9k;b#Pq?NL3khv#3Vjp z3>oXti@*#f-M>8SlS1%F0=HpR!qUdpCj0k`i_^0~@8W`OZrSF1^nNWQw&yy(1)1{5 zy8x5a)se(gAltutN!+MQ80xL+8LC?Ws!yUs@W(2vRlv$Yv%0bq-Z{TK)z6TUxJduX z5b)oI|F=RE{GMa1<^OlS{eJt~?+X0?xA5<u{(%3Mpu_sp_8&6Vf5HDWn;ue5x?;AF z76TdnnrtCok|tnXS^(tEta2GEQ$yN?hruD3H4d{Rg%nxN!vfz87qcX}wnm)_yUNsg zpooQk8@K46A-p-<qy@7u3NGZyu$%qf52h!iIIye`O>9o<qgoGnW9HWhXu9Pq4e|Uf z8_eM(S@4n|ivqqw;@HACAoqqp>9Rm(6DwP{rt5}=rN$lGVG6FeZ4ipNM}HR}$e&A_ zN?5r;t(?FulP6dz9w8I_<?^b<rqNQ&PU0b?yA<S_iv7FcBAtHiT)mc3kEDXsJLUj_ z{(%W|M##G@@Ofx0OoQL>IQzYJM`^+^yo}UTW}?GNS(^ihXVnBNi~jILc_9G&5x?P4 z2lTr!q~X5d^k@m=X@a8M1>_)A<@m!|ln%{6X%n5bQ)SL#$`w>)_JS@cYaDo=&|u~Z zmZS7rmOvV$JxT8n^MTYU1T}TQpNkl0;>~DKESJ_Pa`l?J5NEg#iRsBO%bqtRV$0XL za_gYw#dec@!*<+#^C&G8Xhi8ZB+TH4WY1mLRES`U+2PS!tDvhVm>)jIV@sEa0qESJ z=%+LqR|Y@G@f)0^6!o-nPW_gKg6x50VR*yele<?Brj5&>PL*0(vXxQRKn5^(&W}T4 z*Uy1dGTW~l@GF;Eu}LBec$(sif;Yw=Y-^SC4{#vQ*|R$}CcVhDCKfYxZEmu|B!+A% zZOWPnw)inxEJXeSQ^ATEJOWY{g&9x%NaZWd(j}Naf$crS;NU3(?%a`92z)&tZV!13 z5rc2|&+vE?G;PWvN{5fCy0zHe-Y&+KPzTg0J;z3kM#QI}Yy~rGd}`re%G9aNpjbJl zSQ^XfQFB_}(&LG_dE8jr=n9peW*IlrpvJAomnW}qE*(O;I^`Beiz17L4SARJTuU$I zkQTfl^FdoZ<Lc@>(+EsAiW<7S#p)jhy@T%lsCRG<k+B>>UfHYO1b|1wzq!9?G(ecJ zqKzz{Fjgo|Sd%K6s`1P>j?D#!f92CGe@a#Xc%+^bY2+jv1U3N*`M_HYp*cr*>BHwi zeV*Iob@W8jG~nf|3pfbD90GV$F9=b()==cqCEt{#R=Q;;Fd;Ok4#HH5Yo@}RUk70d zJSco7ZTDoEitvW3q|HdWDm(Op06Uac$4WZ$CX6`t>P!NBRJEOCb!w(-p2j>vMpWkH zkPdu`0eyXPa@gx0UyKH4y^Ei_hi4cSzKn$!@(kkY)psy*IcnD7j};gAZxYvQ5J<=E z_D%P2_{4ruWnM@9EdCWE15!_>Q|j30i9y8rI!Pk#$A?5&E%T%jMX~L(=-NQaO)gmk z++Nd5spDJOBrDq)m$J&uDt|ZdPkdtSS9j36P&O9>ND#jD>xXXpKm7JTw%h-9ZvONx zzG<ItKHE||()(L)|KgzEeS3T|9QF4>Lw#}BKkARzF4N)vB}3Nx#3#!vNl$KgnuY;K zLc8u^l2iA{wDSAqy)~wkJ<5srwQgiDUB(S!<XUV>wzQ6cuNo-6c@$E(wtLelp8zGn zMAVa+AI36OGNvuIvR+3k#^;O7#B3&tNR<T}e*(>an|_XSq09-U17N8kC-Om<+HWG+ zVK2ThTB_VMif;g`(br!qnmhBw$*r~@?!`APOl#abguKxg2mOI%)N3kvP8Fc}I+KVA zRga7kUst(kfHs7ecrU(b@c8Dp&R1FI_n<4~j}+gyuQ$&F5P|WwAXX7|b^G#=>gh7x zp9hV}JZM2$by1RQioj1KJ`Pma;I;T_CbH+1bgri7cB*GMNq9Qp&@&Y@WK2W1c0QnU zx{Pt@c_5dyd|+kZd#p$lp*x?bh{%gH0VXDJLvPQMU>R|bjglmSDmNj;*4xCkumT!B z(Ly!d@ZhI5xWz{y<W&wXbw$Da6cZCe5=TFZAOyBxx3dH-g9!?dSS6Ibdl^q=9D-!C zO9<fZr>j@ALcm~Ss%^ANj=*3U@;1w8bZD@%3Dg?w)n}ZU^+fIla!85{GH#=CwSAjP zo_!kfC=e{*Q{XplIK;}6_RqCV$V3EQx^WV><K-MuL-S8zhB{5!Hq3OsgQm?GI~Wvp z%IEen_2bEmi6!VL^<0@Bf&J);MLwt!Ah)F&kd1Zkz=Um&+11id1J~fWfG#Q=v=K98 zL9)yW5unxQ()iD^u*0OV!2L`SgRR)=@RlkY06$j^0EHz3z~|fmdKvOVm;xAtw!|7l z7kAi>4$`h??am1v%m!c=oUgyOKrp51n?+yYdx1XDXB3>^Sn@BPrf7*Kd&vuro^qn| zbczlcN;Yi+IsMh@$zEMP8NnkNvxtJExTn5=&Q67Av$VJ})-r_fK%x)4J6uLuVW(W8 z!Uz~Jl+HE`Y-$o@3wdj$)|=c?Fe)D3bP6|$ssuNSjC&h|Ne5EyY;0`QK~_tqk|lq` zpXa3V<(WU3acfZZ1&eRPI7n{AyAUh@Rc65rG2>Artp`<(4iy*QOK6{f*29Fc4E(?J zxkHO_=P>RoXED{sWf=SEYABBv`*3dMeA2zqA*0^lQxFC+G7H<);kcELjt<#kA%c$M zqSsPa(@@JV@6f6R8k1Q8!N9XDLY`WWW?~AXPL_BaDWIX+g?xD+KUDolcA6SELpRwZ z;(jVY&Xr*k)PH)EQ;C=c*j>!HBJq5Pmw~YMWkVyo2g2DmNy@KMxL6=Wdc<KS=9p0n zm+?oLmYK94Moyw1^umxU(E*t>X3LbP!b<VuG^SN(z*S|n{>pBy^w(N282eGr5JNOP z-P=RrCR?jT2~g(_l*_^ra$S=ft}_!MjCAm2MQKrM9axL>=pI@wJYevH)J>u)$`$nb z=>$h=D)Lg)tkgbewcpU!2F?O}=t?2T4XZ;WL2e=0Ljf4QLUQ2T^wrmLkR>HTD`K07 z+hp7j@=gvws}cD=c3dj3mvf1<3vC&;qC4ngDT8bP20_>gO}ZuK{}e{iP?r+m*I^W$ z5|2DeIqygVi`-{AEN%>Gd0;i=vuromF1rP(s2mruPEi*S+9(hdFAjiVA9*@s2-Y4n zQ|I0ofowSBP7{vuA&4A2Sl9|Hsc4y#ZNw(MO}eurPXH<aOeW;`rGkU2tOF9{iaID* z-iM0AEC}P=szT#1UYAZPcaHDxt#j;928tchgImrvgm>y7nUaaYVBodqTeioBkKl&R zSGD0R8(6e}0;b+#<lpN~t$g9_vqiYSM@kxbI6^^xs2=yo<C=BU=>g%REL=-@WHegI zcTxkwB)WaS0k^tOz{NRNya8;_qu~A}vj>D&GyBH@fGfTd0G_660f3Ql_h4usdU{|) z0A#}kNbkZ#;=p=BtEsp<%7i!SK<w(d0}h^oK=lfhm3b4@MhRz5LXQt|6m$NQm|UIk zN1F;%Fr;Pt=#$t{n;W$l(88ZJ$%$fevM|2tR55`%njn!CN(i!8W*wJ`P=tFV44tR3 zdeEYj&$ZdyT-Im^9)Ra8pPon;I3WmSUE7>e*{|Wt$*@dH@~DiWbuqftC~9q7Y<duG z@ZjWZ<b>i|<B&Ffc4v0_0WSoUZZLnExryc1t=Yp|Kv+FI#dCTpAkJW{1QR*&Nqy1H z<Sa$WJdaO$$7Yg5y^kQDc6<e5m<k$R=+$m6PX_Qv(*CFF63BO>jo@uQruzMor`gU^ zyICS;uxWUitigT`<)j)FAk@K5`%5GqrQ<unO@9xuN2$Q0Y!plOp__p1yyE-GIn@>u zq3*kkDRyxQDD`?qpo+0~1sv;S8b;<p8(f~;v?y+{RdkMB5!)>_HhNW3{QxWjdNE83 zUYfa%q~QSUl4K?2a>q9aifLG;Z?c8s-M78t(ZxaU=l*{00@W<&w4xB#8UfkPDhOfI za)TR@F!}n00za2ZXAli?u;b&;opsJ4>MIFcv&y9GE@wk!t1@+BMNr0fNz?#_MqZrU zB1|AMkT%J{aLmDO#1<Y{NL2M0?=b5*?r@QXj&twj;;{P30E59IN^)pfXb}^t2)v>2 z8`dnXhZv&zJj@^`Izf)yK{?Q0Fe+I+-H}aGht-jHL5JowAe1kjG>rN$vW6wob7obM z1iA76<f2Hg!6sUOH9J>Xp{$!SgTL}sDdg9Hb<t7FW#|d`X2K~{s%&A6a;AlM#+I*+ zl+Cu@s{?_*kaCuK-rP=(?VMwq#nWu)5!gx0{Z9a!Ip?fh(gU)Xa(g8zk>Q`LYO_$Z z2>6pC;pmmnL6=N5^$j@Bgs`%kUZEM9?%ZzME@?|lX>LF=Dulp0cCLeBl;rYfhUNR> z$3Y0BwES&ym?YQBg?gIEG`+IL(?TdKr+e!aq*u3KEC}OZIA~zBY0YOLl9Dq4_Gg!% zi?f5!l9}^qck~WYdiyc+FGUhT3<PUDG?)R8e%sGP&7$Po(v#^CnS-{wf;V&t8|++# zkMqGUpYplvFSRaA<nduK50Ood+|3-FoE$=+iedlwZSiSBE{)3=m4L7Z7|44v4<LFs z^;`nf(`WSvaK)Ds^$9A?bQv=TSQn|Fe~gb%3FYH*r{4e_3fq7?4KcUj(;+R($xa1U zPWHlFK2u6w$wz<2TpxSG>fy{g)tc~Qsz*zN0Q^mk_Wjo9!WT~ydgKAc2t$eh#WV=) zDpZv(n**NtVI=Sxd_>1uh7Dv&NCf<E`-8LN3p}XDC!>ouCuhe87sJs(Z!iFBt51`3 z8FxrX+JQhaKoG$v24Zh7voH#?6{|Ns{8~T%rdi*z?AEX)kdEUddjqFj0c|alim#V| zKk*t!Jea#`xaWP?IXA)hlc>ynBCrgRjMBw5Pcl3`+6uy>$!@jB#;zgIvpc)|VGN=w z3=gNiqJbruM}YyyirEHJpdjS#m-eD*`3sZ7x{MfvtJj9vwc{jfBbg*t#sZ;}1DJwn znOkp+y|78Lj3_@e?%K0E==K4K?z20Z`h&-M<QD17cz3I7g5rv;zX9xErn!ZTXq<^H zy`Zd-q->QeQ--ykLoR5rKbJnFkOmRVO>DdK7F@XipI8P-Zeawg7}^_49+d2Q;FPwN zZVeRMVI+_wy3umL4to+ULHtM|tl)s)8I!(N<SPo~RX%ljE)smn;ge<#jDN~fKhKiW zsYEHN5w4g+Y8T|rtS+*tm4r$YB_qQGdoTb%{#|+Mobd_?N4`u}Zksi_`kTEqK6s51 zYHbDq^29oy#YyGTS$?j1)e!Qv8db%wxiif%YF{?rYO$6p&T@wOJ>*%WAY9M)TP%Dz z+dr8HxeO_$6dcyo50B0u7mfPI?P5+Xm2w0u|0NUNFmDAjdBb?qo4%xb$hlE9^2g%H zDp0R$YwTqZxWRg@0IQ_trF5&MiDk4X+?Z+OvK*tmmKO@}=g)Y-wZRqmN(P1P(nE9_ zB}r=H3tRRh?614vgq80_*>O;%YDPMzIyVk0AkG4XsKjYUlm%syT`7K);#(&+A*sUp zQy7g0-8hip65OB%?r_o!4a77El_xDov>lobLSWsWS|zTf7D};e&%@Z^V8?mAuhdVL z!K^gxIGEA^g5(L!TD0&>t&I^NRhhA@8zoPs*O(q8F`62vjnkCaY>zbUjn#LvJXU1| zFLj4tsQ9~D3x{{*o{2jlyTr;o5}Xxk1_8Rysc3F8UPQg6n!>veRHwGM)!Nl7YeLH6 zN*1fndQ+r*l(YMJcpx0(4POtEvjN0xyawktKAocDH(^T~WMvD*K7{dAi;0<^68EPZ z9Ar*&;js}I7gJ{HOCBY+wN0v|<m(WHIAK1XVLxWd4Sh8MW8{-KTCqh!UO|v|nu9Oh zA`HmHn8E<@pi|pq|KMrDG6+_Vag9Tv;C(I_LQF=>F~9P$GbAel=$mZCmT}4>KdWYl zXv*@?yoEktd3v}yjYSA<%2=a>{PhWRsD*TT5e6;|Qy8n$Jj&&PW7=rjo!bF$33E&0 z$Exm}0J4vmdT6K>?m&G)J!j@6ZH<8?s}K8cf9f9|+Ue0qyZ9NM*bqn*w$x=};_H~k z1v{cI0yQ_{6-b`RVA2fc0e(#B301U`purt`M$<Is$5siV0WF6}ssQc>8XOP^^4Li< zSwwz@p|a4G?>3CTeIY}ttwmbhR|$5Rv?Wx_VbxSnTxk}AZJk2f;~DE9BI`kP9(zPe z3hgC*XH)Kj^?c02c*a5M6STpog3Xc$0))-M+~3bI&^V~3l7(~HYn$}Cf!gr&fyZi+ z&A^O5Mo)kY+z>G8gJ8*m$5Or$V$>^T{_39lbrz?{A1d3RFy8ZIWeZP;6|E@YY3h^| z99Sx>%*eV-A6W|oYvrXid>A_srjR$cLH_J;ag7hy^1RtH#&lsW144<xn)P<V4@J@3 z*I%<bunKi}sZ$-`h_tPU6d0fOPkZ$*@w7{Mmaf3n&YZ=F^F;&V$6H5kjUBwhyA{}i zJKL1hwk=jhofRh5+V(dTkZj7j$4jiN*V$LANys=^PrmhCpn?6dSn|}Rvjk>~&B;1Y zFQw**q(Uff+bu10mGQF^J-`L~i<qa9@VigIh#W5>C4{F#E9EKMWjp*^<v{uFWH`Dw z9jaFoSnF?HhVj+{g2Ygj_x<C8llQ}md@nCdFS^R$1al^V2oSnjfafaOXo38%Zpjt_ z&|Z8Krb&#C78_A5TZKZf{daD=pZzq1b4;16tUe0;Q*vvU7YAAd9g?M@_1G(oxu9Nd zRu3@B{xVH@oV`!d>oC4j{?V(_8BK0w+(@T~<0P<N>)<k7z8s(U1PV*r|IAZ}Xj0nn zhRO=<PUS6I+DKi&#a2|IACxx>gB;zI^}E0{6T|pBvy*t=Z?9n_ZGQ)wU^_IpH#jP! zNzlL}qw&w1LHl=~0<j}-%b}+tWV{#52e4W!nF8Cur4d7vB#RblEPQsk4B+X2`AYD` zqw;)?AhXIW<y^WO);96Q0w)qeJWl8gL@Ubg3E^INKq)yF{z^c&5Y;z`cG1z0XDs!R zmC0rhh9u@}!Bb#(V1!qY-t#?~d}7&t|A_s42|k8?EP$$DFSoNK!oEBP`e9_9>|SxC zRXmxcNt`SJ>_wVfa$%yiQg1Ba6L1BE@7UT`7!4_ZiH?H@7H|>3hhZLIJXfGi@ncAh zO0^W1cAF;gmEB34P~w-8d|>)WLB8y4^#=0B5?b3%Q4ampI2a1KO7EN#ZPFs?gjurQ z69zMnlMiSj0oR@ts|w8d__+=$EO`nr+=I9X8`g~8z*eGLJB?6}u)kf%#pM|Hu4^O5 z{k%p9quZpm0Z|!T5x<(88!YP|$^0_p;Pis>QIj_k|B>IFbX5OQF9_r<=!R+>O+B>x zr8|S%D&>R1=*r5H<7wUL1<bRwvhIK2bREj}7*^#B;wlD~o6y|LuNBD7aw&`7!<z{X z4pI#PRKvdn*(koBfSv*=3R?6YLTVueIH-ISvA+<rB=cAovt{6^2H8g#un^AuoGc-z zIgB&+f$q)1$qY{CGQN(J+n8~16bC15n~M0C0={)1_b`z~eBsf72l51t+lH3ln@(i@ z3Vp`f6#bM4)ZM>FVSKGQncP`CxrB(Oq%cEdmO=c$_Zs6A;;^C69@vr`y&}SgR5nV$ zJKd%4n&?V$sn*JQ7Cr$c*j6W4pHh_WvSl3peaTOyZ0j@(WM%bilQr$U$>wq_i?VX$ zFKj2?3h0J%DtdrcRn0+iE6|BT)g&nfA>Y%t<)Qle)b8_1%-d>>Yn$i-@5dR(d%|_H zm$)Gj2ORZvqB08KVRk(M*<t>c$`Ztk6(A-Y8^k=2I*7~%9Vz<cle|EuD#Eu5Ne{^% zj>5sGDjhc<+gSUYj7i8KkEOcK;K)^7W>3NE{}zZux7#D3Vd!nU2IpwL2I-14eH}C~ zb;vGt`}wOSl?B$)SHv??3skwhm|Zow73FJgkD!M|S}nucK)fV9e>ciF7!QIUf|?(3 z)z}#05~ay4yeOJ(9C3R@40<T5NpBatbvn=~Ivz-Wcy$Hq0{W?3#Y_xwcRml!J6S$& z`v<+_QGfIcJc+@|5o8>M(GVJ}woz{{z|sRgdH<P&aiiX<1IJQtQ;4WV80@y_a%`!) zzIJZCjvu38`uIHMu9l8dC4DuDY$1LotSf!@qj#N~+fF;GSw3+&u|jM8`q6yk_Sp(^ zE1-epvg81yef^w>7Or@ATLmaU&xneJ&AI%PD<UveE<CWFM)(W?_$LuOgy%ORLDdVP zNWH!QCv>bI2wU`-SfPo=e=E#z!@gB@sT|8pyeW9-%3RDAg~V9S$y6GEcw!NSS)=|D z(xO?*jj8A`wAx3p+4vZI)BGsDX?z^gzuq@{&5vU9*%le$VMsykRE|wu`YHqUhJc+? z!hv(?$5{ibf7m&1u^p0EJXH>24}g~mx=Q1|0_nC0Hx2RPJWsdj`?u#sk`4X2lD#z~ zPu(?ETS#S^!8Hh_bfMdKg#7|UWch#+988ucZyuTb4_p@pXyjLY-#naGKI^Qv6AD4T zM9^f};ygwZibuWxKr#2{!m5dZEgS2|KKT~Yb-1x1T?GMlaxbR)5QOMF3ee5?%TIdq z(bS{Wl9#;r0O4^$F?{hxJi2`TS_=73TEHi+n4Rh{zw0Jwm+dC)5ZCe~r!qdRYglRQ z&zvx^Ru*%U6-F6uNx^|1U2A=5oBTbbCoB@r%ax2jdd)coOhZbG;oBI+LC$XB88z`2 z8Q7qrb%iv@Fvc3Nz4&B`rEcAitinYYc&sNwuDPJVne#K&I6WEm|Hj4(Fm_jR&HesS zlTA6toYVq@w8#-J{0!7NAY+XK{DO&5QS6W=%PV*hqEs@zia5LVqigpikHlK(;{gck z#=*zLDGkt~en0yXl3{NfTfcr>zUjSrv(-XZ;79HzGwy$tYe){TZiE+R1aoQOs!@d& ziPVb&<H%sq0KF6X;-A_}<B=OI&pd$c&}JGGIa#~aId&k(u>&pUOoCwE{gL6VEN?aV z=mf$yx3F|Vu;huQ*G*0-XBF%k;|}xC%BvCwC+{y_pB(%G>O^+W-Xv+ypUm16OO)NQ zXP03LY%v22cy{6KgJoox#NhAOgmU+jH%iXp5G410gc8}NH&QO_&sVF}>gXs4-o2a8 zg_zQR&s*%na3wN6_ufXyB^Zd30nPq00)o+xr^$Jz172=v=*OA-+++_7P3i8~^XKg0 z6?=ZS{cwM`^KgIn;^F@8+lS|@Jq74AlhVRpJ}6S`Z#chJoqZ$YuXw3{JEp19o?zhp zwmg*V`G=3`$N2o4=PmY}J#Vsy@vADn=zoGbrJG(^ay`Y&*5GbAPEOMVBFf_(1HWUy z@6^XwwDO|_0)LG^>?g5EB7WZKoM7;96=-NzE#ZiN$~^V;ywf=wz4_r4dv=F!Wry02 z-}0=zAI?=KXx843d?AiOcJ>~S=0Z+`wcmsh7`;4~-C)RWh0J5Umb+N|FmB2>IXfA! zdh_8qdVxH3u=>NZyDAiY7}qvj4Kp6kk_YzZA7nbc_v)Vyzuup3!XMsd^JBXAw6hGp zqd<dl@lf!aaWO67xZRCWU*%0)LLFX~ws@&p6iLTgIfToFJuZ7FGe&5wNBz&?n$&Hx zp5wtQ)Y;sU`TbUjL)ClzGuF-FKK~zGxixE6`r<OO$=xc<{ICdv^D1$O2TocI+wlNw zDYYvD@*BIma8OO$Xn1!PUvLN$-IH1;s9$gPv$9y<DE;gP@~dI9>jS)d^Ebq6RMsDf zhKoe9k@l;%d`8QcqAY~<=c%H<;N|70c1wtq`k>Ujk|w?SXB-kll&1@@s+a#WPLPs< z=NF*30=XryE#(OPpLYH}58;a#{~H0W=*2l8Zg<vEaC+6@a?2^oZ-cXhh)1EwY^GTm z>X#%&%ORuo<p8Xd;Sl!rB#t&e7fBU{mLupI{45I<RtTN8hR%N$nJ30hiRc<-LO6(B z4ZkYb99h?8c*?MXVj*cprDWTx_v`6aIgf7tt1sLAvQk#3M9`%Yrh41|B>8y~D3OW^ z7HYSpZ<3l4FFlGT1+@5btjNEC^Eba8%jVO<j|1BgAx)ADG%79Q15>VqQF_V}WsK^h zsRKmK{P@bYRlc2t5eL5l{H`q0RV5R6+6Yu6H1Ko;xrz%M5lE{FA*>bR=Kh|2rS(Ff zC(T#RDNJ2OQlUYyzOD-?e)4rG)KY#C(9<e!4$6f$#kX0*?u8s%)`H{gYssdNyHiQP zO@&6D=czrVy3KO~ihLWDa}*%7kun?kCt#?Ko%sp<P)~6kicT<LAY3OZF-j=yv}2L4 zMO&g;0caZDF*9n%wzMTnaWul07K&^aJx6U+3(-%6Uij7avzEV%?(3Ufba1q?$mx`7 zvPzkQhmVo0<S7K#%1Vx|#5OpewaALtmXFmYPn{Fji7NgzA2XbYCizBuXUPkn0Dzfu zS?!Yfg9{JrA=tY0w98b<g<ie;3fJfv_sH%et+0eAj(!-@L{=dtIgE|KXc+n_+Hz`H z3e8X-8)+9)vCo?H^K`ELsL|}D8kEAxEZdlzFW&DBFB}CIUL}zn{xaMj9aef!bWcwY zdl$q0VSoSR_<|~f487it>n&EF-O7lx;0Th=zkgXje^rAc3#rn=_zLc~{l&7IPG<C7 zy23c$Nz(yF*mHj}3uB1R5tHNR{^=R>)5$E%_yqMAVGIFlA`Uh<pzdglaoBN-jX(YH z{l)h$$4$%b@pk5ejhYDO%gD!MH;|c*9r+Us>~RS;XHy?;Ty}zXWc#OQ0#zvt+zy@# zHdZJJP>j*?$72KXzX{N-1#}|d=PK)!S{=%%lDOFN60*uHhEAVSwg1ZBr`QT`rMT3X zZc;X~htGRIXvP6&*YvU(kL4h_G1SFZva1zVhO+zttutNAQt3O0nCr2AMlQ+17!NLZ zs@!}?Ee4s5Xg+A3o@tAIum^=tW3dvGEE<y`A)3F(Nj8Hb@D!A55kS)wuP_aj5TlgM zzHLkPY7b-LyUWy=nZH4@T3Bt8Tm*i48^(3GeM5n$JPf07lEg>;goe)TO712u42zWj z+)_Aq@%UKjLQBqHi4dcRIvQml<u;;4PW9nTy=uxttqK!|N$Y-#K1f#2zSS@X3kE;2 ziuEz76J@f(ipXXJ-ly+hLPX#?(GPe<sPdAfDFOYd^2hqtQlwh~{Lctm>#b7q`eboR zfpkA+6GZloUw>VyoTkOtsegu#dIbWrCke9upIMI2^<qpQhm@jk?Uefqo*Hk)gWj9& z+2QB{SeM+b{N48Ut6VK$7)tK$qEK>i0;4MF0|o{W-3(nfmWzgf^mvs4{>5@W_tTZm zX)@scVo2YtAZ3dr3MX!*Q~YGV_bt{P^T?9kJxSq?^8*keXgk?Z9B}ae3J6-6PPa_Q zok9qy7@r5)+HWBnL;Bm5%2`Vww$y|yfg^R0<1z^_bd9ARD19?FL*-{#Te!xlGKF7J ztgGrynHN|cf>l3t;;PMc7GKW)!5ao>=LD85lI^=q!%&EFqQ=y~6^xf`tkntbNh}ul zr0uZnmi-$z-VXbK25rC=hp}&JnGbd~>8&E`$X|8^I3lT-g^R;PWU`v`&8{r2!^MIJ zZ<6$6!Q=P*a>xaEgld6jS?uBx^vq2-$#te5E^YzbN*Mh8!+F!v3!-(wGKEktmc&SX z0cR`ue$N+~(E4iqMkSIb`!ft)K{cQYhxIpIIG^n<awMf9Q|A*;m@VPehcs`1Lk9&& zfpc&SEf6z^5IDdd;N_Vtvp%68JU*?H<Xwm0!bcq_Xe3eD&?gnmT&NC-dq}3~V$bk8 zTs);~Ia99+3vH)=n{<IUy=tq^k`hW3O90_*z_qN3vQh&&pDeRUGS`H{z-ox1eF-#j zQ>mgt>Z{9!uH6w$)tmvW^Yv9IhEhUyN_d}#2Bd(cTT<S4jwn9Xxn-GvU(CUD<&e2j z^4gH=Pv7z*KN({Cfk9c4iB$#%DP)!1aj1Ci$Nm)$+PD0&eF;`okX{qycm$_QnM%^J zuSJuD3M@QOT(vAA7z{~QL2D$gnt}ovyVpP|9_QoOIAh8yA~&FNdd!bzT_}$ZrP?^R zi|Vnou&Ub%@={7V*PTN{G9e0I=tJmP`D~&SR?=9B%2bOIS%NImf+|Z$W#-XR)krw| zCP~!*mJon|kpYwVx~Y`CpQ`6zjBOP-q*t|Mf}k46NzbfJFb_V3FPb@>g7V$w56ivM z-5sU$gp*8or0*>4Uw%R(ATI1~S~P0YNgbt8sU0OEbD6NJwRe1=i$p<$^GO!9#U$lC zZc`1J<pyMm{o@h5X|*`CWK*89mj*52FiRq$S5(R$?G8`~>2{J6o2oF8c7HG-3QlND zJ8Y6u)h4&Drfp%t*W<q<g>?}AiBohXt<HpCL#(M(phLOFp>kMP8Bi$=w&&<XLMT%e zl%6R$iGS9SGTlGvMB&N>Z6DyRT1mZz>$);$i{NtoVkGqD<JVuydP<dU)zaoi1<l!- z(1TA=T3I@(&lflNSuzhNZLo9&DFX>>-(rkMp8+urF)D1pAPEVklUQCxp`b7!6F*Mk zaN<WS<MRbMY{RVKQ5-K9wN0ki9i;9N2MOzfry%?@X8*vKY)t{Lw}e0C*$RkymQ19Z zwPl`_&AJHgDZwz}^92M2LV*zkUW20(g5*yE>5wIeM(L+3XrRRqrQO+Gl{o=X?S|7M z?E_|#ji|P2wJ{Z5I!6$l7NFdkCaD|vsx;<JS@sx{Kjzj?W0^c#C6J*#y?{NvkXvy9 z9`~Fqm7QEKl!RF=9DQ-&q;E2WenqgiJsFW_&V}%=z)wY*mD4n^i(JMa(hRTxE-Fd) z4LX6Gza6M>QWkbCA!5MgiqRAeG768`sPfd^b&Qa+85u|8UqP6HHpi*mVq@>w9Ts2u zf)~mkpw#dq5AA1nlXSJn5-;`RAeo<?^$!}&2QQm1#$OcSEH4{Gx}s%+gWmqh;Nap- z|FCy)+&$`n*MQT43-3Qg66@e*vcb>2!LWaF4AQ|@`6VpM1&1J~DpBd83m~N{z!g$j zDU<z@%B{Qw^bis28ks6q);N3x$#ZPBO%2d0EQDi91pUjl^!9SY0P4rQ^WxGMkr~@c z5>G&`nTAS6sw>KIWXnD=&?bhsLO#=T6Pb2<Ijr<Uqm>HINc*irRml4C35fwRBq)&U zflnZ4)iNgMDz1BnGCSm}y~Y}#@>RCtS-av%qMsz&bY8s5>_>)9P*KRyE=m260%>_S z%S7+Ca7gZ_wYzd_wfx3(oRi|p{AVD=rD3qO>2_^$Vnd<i#;c9Xx1S1cejS7<-BhX- zTmdPcEK?EQxYxkO`n%3_-(tCYNJpF0nMA}S4HsDg$T=nPWZ7j@zx`Biv2TBPDJQJ2 zF9aTg0KN$$uC7|yCnkZ(WwDa2He}?zPL9mT`$TaxApbO*LuC)mhmY~NB{S(tfB+0+ z5^^BOV)yc;>KI_AqB>=}6N0DN(4R^aJ21q&hyrkrBh4>czI<&f-ydlRB&jjsG#zZq z<E<T~9TXp0M5$btsWt3~XjoLCWf3TS(-z%Lic}qXa?r}a3w9Z(X17?O&saF+@rqc2 zR*el@Ngu~7ptIcb=KA=5WQ>e-1fA&pYzB!(bT`VQeknSv9#RfA;(a6yQQCnG$+1Yz z=7@e7wuNk$)icOit6cU26g(+-%D^dwCI^g$B}c}k1RUu%2#NhLX7Y5g9rDaIMu*H# zQ-1~KyG7RZvzAS1g4|^*5M}aJHddTD3-bT_o0r4=r<@$a)qsdU1i`O~HUeyQ?^7r; z(Xg5{RaGV6t2||<5zv2NH9AJJOOEM?Zc{M0gkfalE_3y3tdm1lXinuZQ`e3&**knq z0X)Dwm_)IjfLCZE;#2UJ4zFgEgA{&yXx$Vg6CblFF8q`?um%=1ZRIQ}MnYi)gl(2; zX06M+z+@1h)D?_S&T-_o#G*b=-x8Yu$%GrYD-seyKFzFkETBv*0Y|5bn^KltCC|#( z2H8BW2vP-#K*Pg2JW{o?te__QDilA%Q7W#1mf}8zr<5ar-4q6Pr~H<ol<GiIEGd4Z zi<`{Zc<d-g!m6>x!IIKuapsN+;@xL=ru_pvpkPmUjCR^N8NS8t;ocXSQRzKl<X(9P znw)4S=R@O&bcl@6tWBTvy97Zj;MSZrm~opv%4s>8q(`>Wde{~$y{)yfHA%+qTW3hm zNw=IN>87QwU^~7Ry0Il<C7sCPwOzP!?Lrr_*y<v;W`*n9S|vG><W^8-j5vb>&{v%- z0Ycl$`xm3zQ?aV2LQZ2ZxhFQX)K{h3whPxbWRHS>3%9Ckqfq@?v1Nvz!K6)vw~WC+ zGsllrv0;*)$PBBvqExDmHU7$9zfT0HXdnjM0NlY-;ejh?Rsy9=kl<&+3&n6XzXW;v zVHjUG98x1m+JH}&s2qe`kO|n1VQ`Z&LMK*pSQtaizEY#$w99%wDeH%#ATyt({AwBb zDZ}6bzwZKD_aOXPSWP9$wwF;?)HC`l6Tc#a)}eZxBp`mI0_!}}@BAryO1cP{U)Xhh zDl&hziPKXhmgV<cDAXPIMOi9P{@qr$qm;0-u8EBu=*&kT)|6)D(!8BS{H%N0ccq(J zA9ET8PoZQn-T=79h08!T*F;VdUi$IZUG`$<<zHU@@a^|6|5A8G%HaQi?Wxl06b!@g znze+Y`S@U^fuc?ILS*U09_@%UH|_;$#@F}g|JUBtHn(jYVSkN&#f>JDP&P<YvE#O6 zY>phs({vo$Bg^?v*Havcqlgd$7yuN>I-2Q^=r8Oq={&o8hXV*oPTFzXjP=2m4u`|N z?%nM^`z%>u>`sd2(UXQPhn{;*<rcYa-BZvC;)4ZO^E$H!EDpqd`rWf<XXj@xE<Eoq zb!w7$Y;s+iWQ)ZwOvxO#3T>HMSbSTnt_RBev;x%<oMjl&4u3Fo8HHZFD=t4U*uitb z#N?z<#E~Her&YSlvn!L<=$xZ?IyD%!fa&TPc~3Rdr6QwU26FhA<7uU!(FK2WspmxQ z%T;Ak<#k4vPjKjxF$a1K#A5|{(^_xq)|*-rPpKBW0|&9^Mh*LIb^dAiCN(5XV?c2z zsE=pMs8{8?G>;aUf}O>Miu^&ODbUUG*rY@W8K-KTl`~NUlCD>skQgqA@`dL2!Cfna zcnk36k!(7-u&r16#3(L+$w4eO8Yqdu1}<+R*k0vR5Z=V4;&ZsA@aEELQ2>YlNQn9E z2r13cm5k}B=keUEV+t_OMj|5W;_wC?xQ-!&NkQ?e#jm26fjZg7Z{z>~P_-4JOom$y zZhIJO7+D(awsR<orEzh|^9)2}SsIyzjn}}P<OjN7a&%$=kFN}t7;cZm|D9|p8Uwt= zP$41@cPwR=+I&FYXJT}9N%NVN-(Ii+t=RgkIzly$)!_{Dfm)axbAeXk(nLo@U2T^Y z^_%dpxy4EAuu!kQJ0(w*RbfCshP(?MQ)8nTN1=(s!WS8HzgPBC0Z`NMGZi-G)dtt4 z7<zn1i;XVhu3n<jv@RzZ_}PRXzO9Bt6JHGyow-+qp*Lhsk+_op-!$(2`(S)8d|K~= zyUyavw>LXxGy-td!lYnB-voX--6%t6R25)uD||CoowZJ=<jI_(Z^Y$nVaj--&@RkZ zWl)V!6|;C++IdL%RZ*HHxLP2k7IA^rQ!HnSWN1y)SVp;_P=%3NQ^aZLJvD(PXnh); zD6MAT7_qCUF5o60O^T;m+7F+{S0)8cW~p<%@bCFxL`OG1=HDnc_HoY0U^1%%lDtm# z$={Xuv){z$sHA6kw(uCorPSbq2=3qdRe;QWchtKyWvyl`iX~Kc^H$Q*{(bfFh^GJR zedE_f<UhF^UYR_Ku_0plGM}N1pFoU=NtT)+O|>aOmlsAmLpHgyEi~;IV{DxD6I{J{ z+^${MVE3$nwGm-|xCfup_6navt>ri}T8^`K@2Wq6&w@4)UI|_UpyV+HxEx_*k7%!< zWfS0l7*m9ap^Yk&X7mW7-Y~}hnt`Y$#e1%ogkFJ_^_t09^rDa+z}fq?-r8UulHV`x zmY!ftDfL_J#K404RUDl|8b-gQWRjmgV#!s1R9`BuC0#V7&Q6Xi3E`LgI3ZTXOH2Zc zlY~eWF(!K#3GUF%&CsthPgIikE*yqh>q6m-m%2a=%%{ZjSHcbrl~y5Ee!V=1cG|la z%f#R&<4UE?C_?;_j2S=d>bWWU2p=f;<mvX}NuuLLUEtn6>JFjoRrYZU__YxXrdmMN z=y^p@M3>zdIIVGAs_}CBC+}<ixxh*k%PWBcbs-UR(XnpBmQYjv#w$Hc$LUHw$>mkb zr>Qtp8my}O=ERg{(pXRC(1|q0Um|AkoU>yp2L_4z(lU^Nj3Mpl4VKp1*Ht9|q}A?z zs2<m9?qTvr?e|G<=Njw`q?9RJnzwy4zv`?1S|2v#$NtT6?~3<SHB&~+u;s>s&N%Ke z5e=={Bk4De*G=goQleND){a7j;=lTd7pLEyH(|0}ss~_MU7VZ`{n~h2(hdsIY$y)& z`}Pw;@IeQM;AT8hu*-iF+!UZNXvRnk_5>pgnn4CR!Ll03Uln6%eJc*yqUd>vuLx6L zmp;KR!&1<Cc@A2&2Eks{OmzqpkLjHROt#>rNRuCQj{KlZ#MQ_-U$)xhf|ih9!CoA3 z?TK5J!L6>X7Dij=<#4Iq(AA!{(wqb>bS6Oy*g9y&-c;AagFByoe(OI=J(=rEb01gz z`(h*NEq*Nili%p~`~61`AGW6V`^OKy{;Gec|KRIK{o~_DkNOYq^!vxh5Agq;kMOg? z)_AA?QNHy*_!9##CQ;4kpmqo%i2?pT93IHiY2iPc7S)sYq*i=${`?{%#Kg6qag`^B z!w<1XEb|QZmM)0hYQeJwgLZOh((=??#S;)RBkOsFz+e!UZufMsFnb<-tm+wrm@9?; zvcO|GS`knIPh#TgQnC6*{T~0pqs~9NEa#Uyun_rJpbljvF_^gmfz<FaQiApFwN(M- zi4;UQy$a@tB5uH_NqH4Ojo=@AUef5aZbZFS9h&^l_%vdKJ*;}Jh8vaS2429`eW#l% zWwjCf(wFR0!3rvDfY_X_3=Z`K5+mk#8h!N=aU|JgLxRe#GlZEfXorRZ59;2y9C(3Y zON7BC9jPdiCo?mdlRB_eI!XAZz;`<aB59WeT4KFQaY`YrpoIIQY1|1hO*y4INuXc1 zfAKqA_@ea6B0Qp-gz#LPoP7@_ke6@Hzdifw*^7&FqROv|>XleQ^~+d@)`#tls0F?x zcSOVzG>G%NButfdE(f^(SSe2Z?;a?bpHN0FxvAkePDARN5aOSIO*4g7hol29XV68q zjG-l}QYES3aWf;s%Mw?>fByX+a$o1zn0RVc(G#P_rd%6C!8CZ}IPwRb^kLl@<EDs+ z4y9;JtxJxDFraKG$@x8(>eS-Xcq4;3-j=38Zxt7>sRiFk@A?7-V^H(1sxMx*=#g({ z#gn=6mN)8iK;m8QHQvrmo|=S2WmzWOg;`{kUxjFNzBh~P+!Th82sgmA8XgoEWr8<5 zd)QMdfZ<Ky2`%yqw8Yk_0Dp}}N}@1URDe}}!if+815U#LfLjI-&*OnoM#NT{AMq>V zpeXzX{6xV064t~lGBo7iv(KO)o*Uj0tn}uE|JcFrY#xWkXMmuSlze-4Px-TEvy5|7 zoRmJ7Eg%7nr{BO)0L>OTPGc7tD59#5?%?-p{3jOO%!q3QRh*kGsW?|P$adA0oo3T~ zpM<Jz&L!c@CRL=tWdi2#G+WnRm7e3K2)P-#U)chv1+J@{pUh-tq7}d&%Q)XsH??%1 z?ILenv2~l$4meLG0npnf?)2q#P?4bb1Mf>+841bfeWnMi({$9M$t*))%Xj_FlfsRI z_uYSIaybgx)-ja#>d?(*lRX%}RHY4WYhPx{RYJV(|Deq*rCO*y`1+dChVYFvt0}Xm zlSE<zmC=}O-Ym1aP)ceN>N;4+GkBRB&UCc9Kplb+l%w}*(|}+3H5eJYi#mZT3Frdn zscFUNk#$~MR=df*j5V~1=x=C$0F*2yZIRoMSbkLEP4Kl|RcNNsz9seom#u|Q!9J6d z@N$0Xq4AgV%bOh{ZrT4@y(B!S)?vTb@AX+1XXB1pOrnJcE$5dtprXT5j%tqvUSWP; zaO7FkO;Y{nWt>u*nLFLx+S`pWr5-;lOfo(Eg?(Q0_Cv`xUvFw>@Y*%2!?&;A!j!tV z$`clD&3YEDo`L(Q*C94(*Dw&eKml?f1<wL(?TOI^Dfd$q!q*f5!$Lw*TgVWsEjqVq z$>RE`dH8x^)+V>J-hC%fwmARz)hMhzy8vpr1+UHG2a}6=-iBD0kr#`UJqFu@F}z1s z3zNqa6)yCT0N=%FhOlEL$WtjEU4U{@v3hVP03%%}Jw?ap6c$xu9|@d<SV2)w)d~B( zua0}i4?^`s+23(93#njGiNX<5$2b#DbWTSvIVIZ<TC9Ng9WsR!wwH*|HOk6vVPJ)X z!AoK$O7q}`M)(IK@F2XzAUxPv0WH{YE{;zQ^i5AaMPxOFi3ay<0v=wbS#BcWOL%p< zb3Pefstbr*zDUj$u+oOKX!&NL)j}swR-4E>!LNbWkOt8!gv&|b&fI`CG^R!qvc*eH z+jtpv!XaCA5*;@m^ZTB)g}-vn9p2GB>H0v8?XN`oB>7&svFYeuHBiW1{pHgbpMC$s z**OJ?y&7>Gsh<B|XEf+`-uAnHIO+abcmLMwzPopHH0q27f4SF49esY^+gNICvXamf zZ4&;EKhL56+-^ralFr*lVgP!`NomD^!(bBMoBfx9T@ovVZ7W=&YVNnYCGZ~aq6cyi z&06ADZHW)w9kqSVhP4{m(1$&?fJ!xd(^q3?12v{DYEuS1sFXp(*ZMYoN0NU0Hhx#d zk3i}ZN>d|stOlwJIwpB)Wt2K`5~P!6-fBLoNfcNpZ<HwwEW_^nv+bb@w@0cT*F(yW zi0lV-#yjKocc@LmrNZLpadqCZ-@$|z0?9q<HGa2E$F>1G%iEYDPH_s3)#z2LQQNc` z%c)-Uc-!{)xGIkKhR3^o#(}3cfvPw*Id4522Bp<BVQy?+$Uf2#%(|$FS&Xw98$$Od z%bB*jPnMcuXz`Gxd)rCnTZY`Lz8!b&sp`^!v$FKEttthmj_!SazgL=~>})FGxZRV5 zdWTB48NT?!Wj?Z>lEu2g{w%+5kNkB}D*-*cQ-pguQeC{}t)`87qPAXX`%Jqnnz*33 zbp*{*V9+?F1UIRS*u*C}{~t|eHTzGTMrPBCdW-TS+JE{Ff7gGoWB)mR^z|qE&qw$X zJI<^um%ix+f7fg`o+5miXPa%0C|+>tR(I96a%P26HaDcKw|Je^vH;e5VfE@1KbdbH z;yxf4{biP7TO*qR$2SnwF+OlfVO~`@H8_+HRk2zwldaljtDH2$B^)1sOeR$`e~Cc2 z8jSkTo##3&&{EL6PG*D|M8RO5%qDYFs&bX4Ch4h@F~P`Z20=au`ou@(P#V@_D}1Ol zG8Vl9h-F+~)hWABqj%m&tDm91&~VDj19i^7kV;EJC46%(t$)Hs0d-fMtZ?Bd$>)^2 zm8}o1cS%!EEWV_AH>plb!rcQIs)545eT_+s1RF<vG+h<cp$KAtuD0dbTL-}`=~G`8 zYG!mKyH-HAsqlWjgk=Cj21S7yXVJF32mBV?+5ztml(YeU%zUdD9$c%R$wc=={j6@z zG2Hq5%Sx|(pcFRer#MEvIe%`CQf^}Ub}~D6{&;cmN&y#Qa@uy4%Z_5+<VjTrE*;HP zWd@{TvwYF_s}}@2ON@5lI$B5d;xAByU-&|A&X<#?S+so$TgHfPt7Du%yq7FDUh7ku zgQLOT>4|Rq0<qh;B~$~ho6LlQ;r+|p6qtg1D~oftqFvLaE&SF_OSCs5MuQ5W%)|J; z{$f1?)$Bki1!gXy>){>@y*EO(mr2lfOxHETw%wB9r6DIQEDaOMoC>&XCypHFP+Vlt zeMBr8MU>DhSon(D>o;t$Zw(9&z<AP}rr!Ky<j|BCA@mgV+<FMYrw28g8J(0fwba`r z)8d39gOp|^W-wei$sjAEf%6Huq1dAQ1>|+?iI`h=g-x%LeXBePc{Yrp2jT@)$qXVw z3)I+5^s2xTKqa~OGKQ1|4+wf6SfkRA+$Q5*9$&^Ou%Ig3bluQlrzx{g<PZt;JdVV) z2jjxR@F<AT-VxCxxV`FKX3#3E^_Kj5((W|Dh2Ku(b(YVu=S<K`u&+wt5;>>xc4NOO zyJywRv$?^^5HZ2s^Z;&_*c=)HaL^|IuZIVbnI!0?hQ-dCHP*l~XpwkFwZso8?KO$H zTgt?FpvO6BlT!3MIYiMsrS$<lSm=%qA@-o^&mt6oI+S_gri8dUdxMIOyS?Jlk<a6z zG$~@mA+c6}Tk%HD;hyV4*;t*yPKv*xcb`gk?LMf<TMHVkedxBvwdCN@P}jqcaZLR5 W^Xcc)&z;}i&%Xd`4so6UssR9n@llWf From 2ba4cd152ead0a205054e7cdc806ac350c922630 Mon Sep 17 00:00:00 2001 From: Szymon Chmal <szymon@chmal.it> Date: Thu, 15 Oct 2026 15:19:56 +0200 Subject: [PATCH 2/2] fix(agent-device): drop the untested prerelease-minimum branch of isVersionAtLeast --- src/agent-device/shared/compatibility.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/agent-device/shared/compatibility.ts b/src/agent-device/shared/compatibility.ts index 137019516..3d7657ce0 100644 --- a/src/agent-device/shared/compatibility.ts +++ b/src/agent-device/shared/compatibility.ts @@ -26,5 +26,5 @@ export function isVersionAtLeast(version: string, minimum: string): boolean { const difference = (found.numbers[index] ?? 0) - (needed.numbers[index] ?? 0); if (difference !== 0) return difference > 0; } - return !found.prerelease || needed.prerelease; + return !found.prerelease; }