From ac353f506f155be26a86654fb47dd03e89481265 Mon Sep 17 00:00:00 2001 From: Jean-Philippe Date: Sat, 12 Sep 2026 22:19:45 -0400 Subject: [PATCH] bootstrap: honor macOS .service records that carry the full plist path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On macOS, the runner's svc.sh writes the absolute plist path into .service; on Linux it writes the bare service name. configure_service! assumed the Linux shape and joined the record under launch_agents_dir with a second .plist suffix, so on macOS PlistBuddy targeted a nonexistent doubled path and the real plist never received Umask 002 or AI_FLOW_AGENT_USER — caught live during the plans#36 ceremony. Use the record verbatim when it already names a plist; keep the join for the bare-name shape. Co-authored-by: Cursor --- lib/dev/agent_bootstrap.rb | 7 ++++--- test/dev/agent_bootstrap_test.rb | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/lib/dev/agent_bootstrap.rb b/lib/dev/agent_bootstrap.rb index 8f51810..0b1d237 100644 --- a/lib/dev/agent_bootstrap.rb +++ b/lib/dev/agent_bootstrap.rb @@ -414,8 +414,9 @@ def grant_work_tree!(runner_dir) # Step 7: the runner service plist carries the service env — Umask 002 and # AI_FLOW_AGENT_USER — applied between a service stop/start so launchd - # rereads it. The plist name comes from the `.service` record svc.sh - # wrote at install. + # rereads it. The plist location comes from the `.service` record svc.sh + # wrote at install: on macOS that record is the full plist path; on Linux + # it is the bare service name, which we resolve under launch_agents_dir. # # @param runner_dir [String] # @raise [StepFailedError] when no service was installed in runner_dir @@ -428,7 +429,7 @@ def configure_service!(runner_dir) end service = File.read(service_file).strip - plist = File.join(@launch_agents_dir, "#{service}.plist") + plist = service.end_with?(".plist") ? service : File.join(@launch_agents_dir, "#{service}.plist") @out.puts ">>> Writing Umask 002 + AI_FLOW_AGENT_USER=#{@agent_user} into #{plist} ..." step!("./svc.sh", "stop", chdir: runner_dir) plist_set!(plist, "Umask", "integer", "2") diff --git a/test/dev/agent_bootstrap_test.rb b/test/dev/agent_bootstrap_test.rb index 7142789..6db89e3 100644 --- a/test/dev/agent_bootstrap_test.rb +++ b/test/dev/agent_bootstrap_test.rb @@ -382,6 +382,24 @@ def converged_identity_executor executor.runs.last == ["./svc.sh", "start", { chdir: runner_dir }] end + test "after_enroll! uses the .service record verbatim when it is a plist path (macOS svc.sh shape)" do + Given "a runner dir whose .service records the full plist path, as macOS svc.sh writes it" + executor = RecordedBootstrapExecutor.new + runner_dir = Dir.mktmpdir + agents_dir = Dir.mktmpdir + plist = File.join(agents_dir, "actions.runner.d3mlabs.mac.plist") + File.write(File.join(runner_dir, ".service"), "#{plist}\n") + + When "converging the post-enrollment steps" + bootstrap(executor, launch_agents_dir: agents_dir).after_enroll!(runner_dir: runner_dir) + + Then "PlistBuddy targets the recorded path, not a re-joined one" + executor.runs.include?(["/usr/libexec/PlistBuddy", "-c", "Set :Umask 2", plist]) + executor.runs.include?( + ["/usr/libexec/PlistBuddy", "-c", "Set :EnvironmentVariables:AI_FLOW_AGENT_USER ai-agent", plist], + ) + end + test "after_enroll! adds plist keys when Set finds none" do Given "PlistBuddy Set failing (fresh plist without the keys)" executor = RecordedBootstrapExecutor.new(fail_matching: "Set :")