Skip to content

Commit 6f304e5

Browse files
fluffy314cursoragent
authored andcommitted
feat(prefill): operationalize remote MLX workers
Align head and worker launch contracts for Thunderbolt prefill compute, expose source-specific runtime counters, and add a live acceptance probe so deployment requires evidence of remote jobs and KV imports. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent b4ab9ca commit 6f304e5

11 files changed

Lines changed: 254 additions & 8 deletions

deploy/install_prefill_worker_launchd.sh

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,16 @@ MODEL_REVISION="${KAKEYA_MODEL_REVISION:-}"
1818
TOKENIZER_REVISION="${KAKEYA_TOKENIZER_REVISION:-}"
1919
QUANTIZATION="${KAKEYA_CACHE_QUANTIZATION:-4bit-mlx}"
2020
ROPE_HASH="${KAKEYA_ROPE_HASH:-}"
21+
SINK="${KAKEYA_WORKER_SINK:-4}"
22+
WINDOW="${KAKEYA_WORKER_WINDOW:-64}"
23+
BLOCK_TOKENS="${KAKEYA_CACHE_BLOCK_TOKENS:-64}"
24+
PREFILL_TPS="${KAKEYA_WORKER_PREFILL_TPS:-20}"
25+
NETWORK="${KAKEYA_WORKER_NETWORK:-lan}"
26+
PRIORITY="${KAKEYA_WORKER_PRIORITY:-50}"
27+
RTT_MS="${KAKEYA_WORKER_RTT_MS:-1.0}"
28+
PEER="${KAKEYA_WORKER_PEER:-}"
29+
MAX_CONCURRENT_JOBS="${KAKEYA_WORKER_MAX_CONCURRENT_JOBS:-1}"
30+
MAX_PROMPT_TOKENS="${KAKEYA_WORKER_MAX_PROMPT_TOKENS:-131072}"
2131
LABEL="ai.kakeya.prefill-worker"
2232
PLIST="$HOME/Library/LaunchAgents/$LABEL.plist"
2333
LOG_DIR="$HOME/.kakeya"
@@ -27,6 +37,10 @@ psk_xml=""
2737
if [[ -n "$PSK_FILE" ]]; then
2838
psk_xml="<string>--fleet-psk-file</string><string>$PSK_FILE</string>"
2939
fi
40+
peer_xml=""
41+
if [[ -n "$PEER" ]]; then
42+
peer_xml="<string>--peer</string><string>$PEER</string>"
43+
fi
3044

3145
cat > "$PLIST" <<EOF
3246
<?xml version="1.0" encoding="UTF-8"?>
@@ -49,6 +63,16 @@ cat > "$PLIST" <<EOF
4963
<string>--layer-geometry-hash</string><string>$KAKEYA_LAYER_GEOMETRY_HASH</string>
5064
<string>--tenant-id</string><string>$TENANT</string>
5165
<string>--cache-gb</string><string>$CACHE_GB</string>
66+
<string>--sink</string><string>$SINK</string>
67+
<string>--window</string><string>$WINDOW</string>
68+
<string>--block-size-tokens</string><string>$BLOCK_TOKENS</string>
69+
<string>--prefill-tps</string><string>$PREFILL_TPS</string>
70+
<string>--network</string><string>$NETWORK</string>
71+
<string>--priority</string><string>$PRIORITY</string>
72+
<string>--rtt-ms</string><string>$RTT_MS</string>
73+
<string>--max-concurrent-jobs</string><string>$MAX_CONCURRENT_JOBS</string>
74+
<string>--max-prompt-tokens</string><string>$MAX_PROMPT_TOKENS</string>
75+
$peer_xml
5276
$psk_xml
5377
</array>
5478
<key>WorkingDirectory</key><string>$KAKEYA_WORKER_REPO</string>

deploy/launchd/ai.kakeya.grpc-runtime-prefill.plist

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,31 @@
1717
<string>--skip-cache-check</string>
1818
<string>--enable-prefill-cache</string>
1919
<string>--prefill-cache-gb</string><string>1</string>
20-
<string>--cache-peer</string><string>169.254.27.104:52051</string>
20+
<string>--peer</string><string>169.254.27.104:53051</string>
21+
<string>--cache-peer</string><string>169.254.27.104:53051</string>
2122
<string>--cache-model-id</string><string>gemma-4-26B-A4B-it-mlx-4bit</string>
2223
<string>--model-revision</string><string>local-4bit-v1</string>
2324
<string>--tokenizer-revision</string><string>gemma4-v1</string>
2425
<string>--cache-quantization</string><string>4bit-mlx</string>
2526
<string>--cache-kv-dtype</string><string>bfloat16</string>
2627
<string>--cache-block-tokens</string><string>64</string>
28+
<string>--cache-tenant-id</string><string>private-fleet</string>
29+
<string>--fleet-psk-file</string><string>/Users/fluffy314/.kakeya/fleet.psk</string>
2730
<string>--node-id</string><string>head-runtime</string>
2831
<string>--advertise</string><string>169.254.187.239:51051</string>
2932
<string>--cache-advertise</string><string>169.254.187.239:51051</string>
3033
<string>--network-label</string><string>thunderbolt</string>
3134
<string>--network-priority</string><string>100</string>
3235
<string>--measured-rtt-ms</string><string>0.55</string>
36+
<string>--remote-prefill-min-tokens</string><string>128</string>
37+
<string>--cache-link-mbps</string><string>10000</string>
38+
<string>--cache-default-rtt-ms</string><string>0.55</string>
39+
<string>--prefill-min-savings-ratio</string><string>0</string>
40+
<string>--primary-prefill-penalty-ms</string><string>1000</string>
41+
<string>--network-http-host</string><string>127.0.0.1</string>
42+
<string>--network-http-port</string><string>8090</string>
43+
<string>--network-api-key</string><string>__NETWORK_KEY__</string>
44+
<string>--network-state</string><string>/Users/fluffy314/.kakeya/inference_network.json</string>
3345
<string>--network-telemetry-url</string><string>http://127.0.0.1:8090/v1/network/telemetry/tokens</string>
3446
<string>--network-telemetry-api-key</string><string>__NETWORK_KEY__</string>
3547
<string>--log-level</string><string>INFO</string>

docs/ops/distributed-prefill-kv-network.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,16 @@ export KAKEYA_CACHE_MODEL_ID="gemma-4-26B-A4B-it-mlx-4bit"
126126
export KAKEYA_MODEL_REVISION="local-4bit-v1"
127127
export KAKEYA_TOKENIZER_REVISION="gemma4-v1"
128128
export KAKEYA_WORKER_NODE_ID="prefill-mini-1"
129+
export KAKEYA_WORKER_BIND="169.254.27.104:53051"
129130
export KAKEYA_WORKER_ADVERTISE="169.254.27.104:53051"
130131
export KAKEYA_LAYER_GEOMETRY_HASH="<same-value-as-primary>"
132+
export KAKEYA_WORKER_SINK="4"
133+
export KAKEYA_WORKER_WINDOW="2048"
134+
export KAKEYA_CACHE_BLOCK_TOKENS="64"
135+
export KAKEYA_WORKER_NETWORK="thunderbolt"
136+
export KAKEYA_WORKER_PRIORITY="100"
137+
export KAKEYA_WORKER_RTT_MS="0.55"
138+
export KAKEYA_WORKER_PEER="169.254.187.239:51051"
131139
export KAKEYA_FLEET_PSK_FILE="$HOME/.kakeya/fleet.psk"
132140
export KAKEYA_TENANT_ID="private-fleet"
133141
bash deploy/install_prefill_worker_launchd.sh
@@ -158,7 +166,7 @@ KEY="$(cat ~/.kakeya/network_api_key)"
158166
curl -fsS -X POST http://127.0.0.1:8090/v1/network/nodes/register \
159167
-H "Content-Type: application/json" \
160168
-H "X-API-Key: $KEY" \
161-
-d '{"alias":"peer-mini","address":"169.254.27.104:52051","region":"Private","role":"cache"}'
169+
-d '{"alias":"peer-mini","address":"169.254.27.104:53051","region":"Private","role":"hybrid"}'
162170
```
163171

164172
Create a paired group:
@@ -192,8 +200,20 @@ Minimal acceptance:
192200
curl -fsS https://kakeya.ai/healthz
193201
curl -fsS https://kakeya.ai/v1/network/summary
194202
curl -fsS https://kakeya.ai/v1/network/tokens
203+
curl -fsS https://kakeya.ai/v1/network/prefill
204+
205+
PYTHONPATH=.:sdks/python python scripts/verify_remote_prefill_e2e.py \
206+
--address 127.0.0.1:51051 \
207+
--dashboard http://127.0.0.1:8090 \
208+
--tokenizer-id ~/kakeya-models/gemma-4-26B-A4B-it-mlx-4bit
195209
```
196210

211+
The verifier exits non-zero unless a live worker capability is present and one
212+
cold unique prefix increments `remote_jobs`, `remote_hits`, and
213+
`tokens_reused`. Decode throughput is reported separately; remote prefill is
214+
accepted on lower TTFT/prefill time and higher request throughput, not a change
215+
to single-stream decode tokens/s.
216+
197217
## Rollback
198218

199219
The cache is an optimization; inference correctness does not depend on it.

inference_engine/network/api.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,10 @@ def tokens():
102102
"hit_rate": summary["kv_hit_rate"],
103103
}
104104

105+
@app.get("/v1/network/prefill")
106+
def prefill():
107+
return state.prefill_stats()
108+
105109
@app.post(
106110
"/v1/network/telemetry/tokens",
107111
dependencies=[Depends(require_key)],

inference_engine/network/dashboard.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def dashboard_html() -> str:
1616
h1{font-size:24px;margin:0}h2{font-size:17px;margin:22px 0 10px}.sub,.muted{color:var(--muted)}
1717
button{background:transparent;border:1px solid var(--line);color:var(--text);border-radius:7px;padding:8px 12px;cursor:pointer}
1818
button.active,.primary{background:var(--accent);color:#0b1020;border-color:var(--accent);font-weight:650}
19-
.tabs{display:flex;gap:7px}.stats,.grid2,.grid3{display:grid;gap:12px}.stats{grid-template-columns:repeat(5,1fr);margin-top:22px}.grid2{grid-template-columns:1.2fr .8fr}.grid3{grid-template-columns:repeat(3,1fr)}
19+
.tabs{display:flex;gap:7px}.stats,.grid2,.grid3{display:grid;gap:12px}.stats{grid-template-columns:repeat(auto-fit,minmax(140px,1fr));margin-top:22px}.grid2{grid-template-columns:1.2fr .8fr}.grid3{grid-template-columns:repeat(3,1fr)}
2020
.card{background:var(--panel);border:1px solid var(--line);border-radius:9px;padding:15px}.stat b{font-size:23px;display:block}.stat span{color:var(--muted);font-size:12px}
2121
.map{height:280px;position:relative;overflow:hidden}.map:before{content:"";position:absolute;inset:34px;border:1px dashed var(--line);border-radius:45%}
2222
.dot{position:absolute;transform:translate(-50%,-50%)}.dot i{display:block;width:12px;height:12px;background:var(--ok);border:2px solid var(--bg);border-radius:50%}.dot.head i{width:16px;height:16px;background:var(--accent)}.dot small{white-space:nowrap}
@@ -31,8 +31,8 @@ def dashboard_html() -> str:
3131
<body><main class="wrap">
3232
<div class="top"><div><h1>Kakeya Inference Network</h1><div class="sub">P2P Prefill KV sharing across trusted inference nodes</div></div>
3333
<div class="tabs"><button class="active" data-tab="overview">Overview</button><button data-tab="nodes">Nodes</button><button data-tab="groups">Groups</button><button class="primary" id="registerBtn">Register node</button></div></div>
34-
<section id="register" class="card register hidden"><h3>Register inference node</h3><div class="grid3"><label>Alias<input id="alias" placeholder="cache-peer-tb"></label><label>Address<input id="address" placeholder="169.254.27.104:52051"></label><label>Region<input id="region" placeholder="Hong Kong"></label></div><label>Admin API key<input id="adminKey" type="password" placeholder="Required for network changes"></label><button class="primary" id="createRegistration">Create pairing token</button><code id="pairing" class="hidden"></code></section>
35-
<section class="stats"><div class="card stat"><b id="online">0</b><span>Online nodes</span></div><div class="card stat"><b id="groupCount">0</b><span>Inference groups</span></div><div class="card stat"><b id="tokens">0</b><span>Completed tokens</span></div><div class="card stat"><b id="hitRate">0%</b><span>KV-assisted tokens</span></div><div class="card stat"><b id="cache">0 GB</b><span>Shared cache online</span></div></section>
34+
<section id="register" class="card register hidden"><h3>Register inference node</h3><div class="grid3"><label>Alias<input id="alias" placeholder="prefill-worker-tb"></label><label>Address<input id="address" placeholder="169.254.27.104:53051"></label><label>Region<input id="region" placeholder="Hong Kong"></label></div><label>Admin API key<input id="adminKey" type="password" placeholder="Required for network changes"></label><button class="primary" id="createRegistration">Create pairing token</button><code id="pairing" class="hidden"></code></section>
35+
<section class="stats"><div class="card stat"><b id="online">0</b><span>Online nodes</span></div><div class="card stat"><b id="groupCount">0</b><span>Inference groups</span></div><div class="card stat"><b id="tokens">0</b><span>Completed tokens</span></div><div class="card stat"><b id="hitRate">0%</b><span>KV-assisted tokens</span></div><div class="card stat"><b id="cache">0 GB</b><span>Shared cache online</span></div><div class="card stat"><b id="remoteJobs">0</b><span>Remote prefill jobs</span></div><div class="card stat"><b id="remoteHits">0</b><span>Remote KV imports</span></div><div class="card stat"><b id="reusedTokens">0</b><span>Tokens reused</span></div></section>
3636
<section id="overview" class="tab">
3737
<div class="grid2"><div><h2>Online node distribution</h2><div class="card map" id="map"></div></div><div><h2>Live KV discovery</h2><div class="card" id="events"><div class="event"><time>live</time><div><b>Waiting for node telemetry</b><div class="muted">Capability gossip and prefix lookups appear here.</div></div></div></div><h2>Cache capacity</h2><div class="card"><span id="capacityLabel">0 / 0 GB</span><div class="bar"><i id="capacityBar" style="width:0%"></i></div></div></div></div>
3838
</section>
@@ -49,7 +49,7 @@ def dashboard_html() -> str:
4949
$('createGroup').onclick=async()=>{await fetch('/v1/network/groups',{method:'POST',headers:writeHeaders(),body:JSON.stringify({name:$('groupName').value,node_ids:$('groupNodes').value.split(',').map(x=>x.trim()).filter(Boolean)})});load()};
5050
function nodePosition(i,total){let a=(i/Math.max(total,1))*Math.PI*2;return {x:50+38*Math.cos(a),y:53+35*Math.sin(a)}}
5151
async function load(){let [s,n,g]=await Promise.all([fetch('/v1/network/summary').then(r=>r.json()),fetch('/v1/network/nodes').then(r=>r.json()),fetch('/v1/network/groups').then(r=>r.json())]);
52-
$('online').textContent=s.online_nodes;$('groupCount').textContent=s.groups;$('tokens').textContent=fmt(s.completed_tokens);$('hitRate').textContent=(s.kv_hit_rate*100).toFixed(0)+'%';$('cache').textContent=gb(s.cache_bytes_used+s.cache_bytes_free)+' GB';
52+
$('online').textContent=s.online_nodes;$('groupCount').textContent=s.groups;$('tokens').textContent=fmt(s.completed_tokens);$('hitRate').textContent=(s.kv_hit_rate*100).toFixed(0)+'%';$('cache').textContent=gb(s.cache_bytes_used+s.cache_bytes_free)+' GB';let p=s.prefill||{};$('remoteJobs').textContent=fmt(p.remote_jobs);$('remoteHits').textContent=fmt(p.remote_hits);$('reusedTokens').textContent=fmt(p.tokens_reused);
5353
let total=s.cache_bytes_used+s.cache_bytes_free,pct=total?s.cache_bytes_used/total*100:0;$('capacityLabel').textContent=`${gb(s.cache_bytes_used)} / ${gb(total)} GB`;$('capacityBar').style.width=pct+'%';
5454
$('map').innerHTML=n.map((x,i)=>{let p=nodePosition(i,n.length);return `<div class="dot ${x.role.includes('head')?'head':''}" style="left:${p.x}%;top:${p.y}%"><i></i><small>${x.region}<br>${x.alias}</small></div>`}).join('');
5555
$('nodesBody').innerHTML=n.map(x=>`<tr><td>${x.alias}</td><td>${x.role}</td><td>${x.region}</td><td>${x.cache?(x.cache.model_id+' / '+x.cache.format):'—'}</td><td>${x.endpoint.network} ${x.endpoint.rtt_ms?x.endpoint.rtt_ms+'ms':''}</td><td class="${x.status}">${x.status}</td></tr>`).join('');

inference_engine/network/state.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,9 @@
66
import secrets
77
import threading
88
import time
9+
from dataclasses import asdict, is_dataclass
910
from pathlib import Path
10-
from typing import Any
11+
from typing import Any, Callable
1112

1213
from inference_engine.distributed.capability import CapabilityRegistry
1314
from inference_engine.distributed.prefill_cache import PrefixCacheStore
@@ -20,10 +21,12 @@ def __init__(
2021
cache_store: PrefixCacheStore,
2122
*,
2223
state_path: str | Path,
24+
prefill_stats_provider: Callable[[], Any] | None = None,
2325
) -> None:
2426
self.registry = registry
2527
self.cache_store = cache_store
2628
self.state_path = Path(state_path).expanduser()
29+
self.prefill_stats_provider = prefill_stats_provider
2730
self._lock = threading.RLock()
2831
self._data = self._load()
2932

@@ -218,8 +221,17 @@ def summary(self) -> dict[str, Any]:
218221
"local_lookup_hits": cache_stats.lookup_hits,
219222
"local_lookup_misses": cache_stats.lookup_misses,
220223
"local_tokens_served": cache_stats.tokens_served,
224+
"prefill": self.prefill_stats(),
221225
}
222226

227+
def prefill_stats(self) -> dict[str, Any]:
228+
if self.prefill_stats_provider is None:
229+
return {}
230+
stats = self.prefill_stats_provider()
231+
if is_dataclass(stats) and not isinstance(stats, type):
232+
return asdict(stats)
233+
return dict(stats)
234+
223235
def topology(self) -> dict[str, Any]:
224236
nodes = self.nodes()
225237
edges = []

scripts/start_grpc_runtime_server.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,10 @@ async def _serve(args: argparse.Namespace) -> int:
557557
registry,
558558
prefill_store,
559559
state_path=args.network_state,
560+
prefill_stats_provider=(
561+
(lambda: prefill_hook.stats)
562+
if prefill_hook is not None else None
563+
),
560564
)
561565
http_server = uvicorn.Server(uvicorn.Config(
562566
create_network_app(
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
#!/usr/bin/env python3
2+
"""Run one cold-prefix request and prove that a remote prefill worker served it."""
3+
from __future__ import annotations
4+
5+
import argparse
6+
import json
7+
import secrets
8+
import time
9+
import urllib.request
10+
11+
12+
def _get_json(url: str):
13+
with urllib.request.urlopen(url, timeout=5) as response:
14+
return json.load(response)
15+
16+
17+
def _delta(before: dict, after: dict, key: str) -> int:
18+
return int(after.get(key, 0)) - int(before.get(key, 0))
19+
20+
21+
def main() -> int:
22+
parser = argparse.ArgumentParser()
23+
parser.add_argument("--address", default="127.0.0.1:51051")
24+
parser.add_argument("--dashboard", default="http://127.0.0.1:8090")
25+
parser.add_argument("--tokenizer-id", required=True)
26+
parser.add_argument("--minimum-prefix-tokens", type=int, default=128)
27+
args = parser.parse_args()
28+
29+
from kakeya import Client
30+
from transformers import AutoTokenizer
31+
32+
from scripts.chat_grpc import _resolve_eos_token_ids
33+
34+
tokenizer = AutoTokenizer.from_pretrained(args.tokenizer_id)
35+
nonce = secrets.token_hex(8)
36+
sentence = (
37+
"Kakeya remote prefill verification context. "
38+
f"Unique run {nonce}. "
39+
)
40+
text = sentence
41+
token_ids = tokenizer.encode(text, add_special_tokens=True)
42+
while len(token_ids) < args.minimum_prefix_tokens:
43+
text += sentence
44+
token_ids = tokenizer.encode(text, add_special_tokens=True)
45+
46+
nodes = _get_json(f"{args.dashboard}/v1/network/nodes")
47+
workers = [
48+
node for node in nodes
49+
if node.get("prefill_worker") and node.get("status") == "online"
50+
]
51+
if not workers:
52+
print(json.dumps({
53+
"ok": False,
54+
"reason": "no online prefill worker capability",
55+
"nodes": nodes,
56+
}, indent=2))
57+
return 2
58+
59+
before = _get_json(f"{args.dashboard}/v1/network/prefill")
60+
started = time.perf_counter()
61+
with Client(args.address) as client:
62+
with client.create_session(
63+
eos_token_ids=_resolve_eos_token_ids(tokenizer),
64+
client_label="remote-prefill-e2e",
65+
) as session:
66+
session.append(token_ids)
67+
list(session.generate(max_tokens=1))
68+
elapsed = time.perf_counter() - started
69+
after = _get_json(f"{args.dashboard}/v1/network/prefill")
70+
71+
result = {
72+
"ok": (
73+
_delta(before, after, "remote_jobs") >= 1
74+
and _delta(before, after, "remote_hits") >= 1
75+
and _delta(before, after, "tokens_reused")
76+
>= args.minimum_prefix_tokens
77+
),
78+
"worker_nodes": [node["id"] for node in workers],
79+
"prefix_tokens": len(token_ids),
80+
"wall_seconds": elapsed,
81+
"delta": {
82+
key: _delta(before, after, key)
83+
for key in (
84+
"remote_jobs",
85+
"remote_hits",
86+
"tokens_reused",
87+
"tokens_computed",
88+
"bytes_received",
89+
"remote_job_failures",
90+
"fallbacks",
91+
)
92+
},
93+
"before": before,
94+
"after": after,
95+
}
96+
print(json.dumps(result, indent=2))
97+
return 0 if result["ok"] else 1
98+
99+
100+
if __name__ == "__main__":
101+
raise SystemExit(main())
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
from pathlib import Path
2+
3+
4+
ROOT = Path(__file__).resolve().parents[3]
5+
INSTALLER = ROOT / "deploy" / "install_prefill_worker_launchd.sh"
6+
HEAD_PLIST = ROOT / "deploy" / "launchd" / "ai.kakeya.grpc-runtime-prefill.plist"
7+
8+
9+
def test_worker_installer_emits_full_cache_compatibility_contract():
10+
source = INSTALLER.read_text()
11+
for flag in (
12+
"--sink",
13+
"--window",
14+
"--block-size-tokens",
15+
"--prefill-tps",
16+
"--network",
17+
"--priority",
18+
"--rtt-ms",
19+
"--max-concurrent-jobs",
20+
"--max-prompt-tokens",
21+
):
22+
assert f"<string>{flag}</string>" in source
23+
assert 'PEER="${KAKEYA_WORKER_PEER:-}"' in source
24+
assert "<string>--peer</string>" in source
25+
26+
27+
def test_head_runtime_discovers_and_uses_worker_cache_port():
28+
plist = HEAD_PLIST.read_text()
29+
assert (
30+
"<string>--peer</string><string>169.254.27.104:53051</string>"
31+
in plist
32+
)
33+
assert (
34+
"<string>--cache-peer</string><string>169.254.27.104:53051</string>"
35+
in plist
36+
)
37+
assert "<string>--primary-prefill-penalty-ms</string>" in plist
38+
assert (
39+
"<string>--cache-tenant-id</string><string>private-fleet</string>"
40+
in plist
41+
)
42+
assert "<string>--fleet-psk-file</string>" in plist

0 commit comments

Comments
 (0)