@rubytech/create-maxy-code 0.1.287 → 0.1.289

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,13 +1,14 @@
1
1
  // Task 602 — cloudflared.slice unit emitted at install time.
2
- // Every cloudflared-<brand>.scope spawned by resume-tunnel.sh drops under
2
+ // Every cloudflared-<brand>.service spawned by resume-tunnel.sh drops under
3
3
  // this slice for cohort observability via `systemctl --user status cloudflared.slice`.
4
+ // (Task 757 changed the per-brand unit from a scope to a supervised service.)
4
5
  import test from "node:test";
5
6
  import assert from "node:assert/strict";
6
7
  import { buildCloudflaredSliceUnitFile } from "../port-resolution.js";
7
8
  test("buildCloudflaredSliceUnitFile emits a valid systemd slice unit", () => {
8
9
  const unit = buildCloudflaredSliceUnitFile();
9
10
  assert.match(unit, /^\[Unit\]$/m);
10
- assert.match(unit, /^Description=Cloudflare tunnel scopes \(Task 602\)$/m);
11
+ assert.match(unit, /^Description=Cloudflare tunnel connectors \(Task 602\)$/m);
11
12
  assert.match(unit, /^\[Slice\]$/m);
12
13
  assert.match(unit, /^\[Install\]$/m);
13
14
  assert.match(unit, /^WantedBy=default\.target$/m);
@@ -0,0 +1,24 @@
1
+ // Task 757 — the cloudflared binary version is installer-owned and pinned.
2
+ // The connector runs with --no-autoupdate (resume-tunnel.sh), so it never
3
+ // self-replaces; the only way the binary version moves is bumping the pin in
4
+ // installCloudflared and republishing the installer. This guards against a
5
+ // regression back to the `releases/latest/download/` URL that let the binary
6
+ // drift (the 2026-06-10 realagent-code outage root cause).
7
+ import test from "node:test";
8
+ import assert from "node:assert/strict";
9
+ import { readFileSync } from "node:fs";
10
+ import { fileURLToPath } from "node:url";
11
+ import { dirname, join } from "node:path";
12
+ // The test runner compiles src -> dist and runs dist/__tests__/*.test.js, so
13
+ // read the compiled sibling index.js; the URL string literal survives tsc.
14
+ const here = dirname(fileURLToPath(import.meta.url));
15
+ const compiled = readFileSync(join(here, "../index.js"), "utf8");
16
+ test("cloudflared version is pinned to 2026.6.0", () => {
17
+ assert.match(compiled, /CLOUDFLARED_VERSION\s*=\s*"2026\.6\.0"/);
18
+ });
19
+ test("the .deb fetch URL is wired to the pinned version constant", () => {
20
+ assert.match(compiled, /releases\/download\/\$\{CLOUDFLARED_VERSION\}\/cloudflared-linux-/);
21
+ });
22
+ test("cloudflared .deb fetch no longer uses the drifting latest URL", () => {
23
+ assert.doesNotMatch(compiled, /releases\/latest\/download\/cloudflared-linux-/);
24
+ });
package/dist/index.js CHANGED
@@ -1599,6 +1599,13 @@ function installUv() {
1599
1599
  console.error(" WARNING: uv installed but uvx not on PATH — check $HOME/.local/bin");
1600
1600
  }
1601
1601
  }
1602
+ // Task 757 — cloudflared version is installer-owned and pinned. The connector
1603
+ // runs with --no-autoupdate (resume-tunnel.sh), so it never self-replaces; the
1604
+ // only way the binary version moves is bumping this pin and republishing the
1605
+ // installer. Fetching `latest` (the prior behaviour) let the binary drift at
1606
+ // install time and, combined with the connector's own auto-update, caused the
1607
+ // 2026-06-10 realagent-code tunnel outage.
1608
+ const CLOUDFLARED_VERSION = "2026.6.0";
1602
1609
  function installCloudflared() {
1603
1610
  if (commandExists("cloudflared")) {
1604
1611
  log("6", TOTAL, "Cloudflared already installed.");
@@ -1619,7 +1626,7 @@ function installCloudflared() {
1619
1626
  }
1620
1627
  const arch = isArm64() ? "arm64" : "amd64";
1621
1628
  const debPath = "/tmp/cloudflared.deb";
1622
- shellRetry("curl", ["-fSL", "--progress-bar", `https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-${arch}.deb`, "-o", debPath], { timeout: 120_000 }, 3, 10);
1629
+ shellRetry("curl", ["-fSL", "--progress-bar", `https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/cloudflared-linux-${arch}.deb`, "-o", debPath], { timeout: 120_000 }, 3, 10);
1623
1630
  console.log(" [privileged] dpkg -i");
1624
1631
  shell("dpkg", ["-i", debPath], { sudo: true });
1625
1632
  spawnSync("rm", ["-f", debPath]);
@@ -3180,11 +3187,12 @@ function installService() {
3180
3187
  writeFileSync(join(serviceDir, claudePtysSliceName), buildClaudePtysSliceUnitFile());
3181
3188
  logFile(` ${claudePtysSliceName}: Task 250 slice for claude-session-*.scope cohort`);
3182
3189
  // Task 602 — write the brand-agnostic `cloudflared.slice` unit. Every
3183
- // cloudflared-<brand>.scope spawned by resume-tunnel.sh drops under this
3184
- // slice. Writing once per install is idempotent; daemon-reload below picks it up.
3190
+ // cloudflared-<brand>.service spawned by resume-tunnel.sh drops under this
3191
+ // slice (Task 757 changed the unit from a scope to a supervised service).
3192
+ // Writing once per install is idempotent; daemon-reload below picks it up.
3185
3193
  const cloudflaredSliceName = "cloudflared.slice";
3186
3194
  writeFileSync(join(serviceDir, cloudflaredSliceName), buildCloudflaredSliceUnitFile());
3187
- logFile(` ${cloudflaredSliceName}: Task 602 slice for cloudflared-*.scope cohort`);
3195
+ logFile(` ${cloudflaredSliceName}: Task 602 slice for cloudflared-*.service cohort`);
3188
3196
  // Task 600 — per-brand RSS sampler: a long-running user service that
3189
3197
  // periodically writes per-claude.exe RSS + aggregate RSS to disk.
3190
3198
  // Named per-brand (BRAND.hostname prefix) so two brands on the same
@@ -187,16 +187,17 @@ WantedBy=default.target
187
187
  `;
188
188
  }
189
189
  // ---------------------------------------------------------------------------
190
- // Task 602 — `cloudflared.slice` unit. Every cloudflared-<brand>.scope
190
+ // Task 602 — `cloudflared.slice` unit. Every cloudflared-<brand>.service
191
191
  // spawned by resume-tunnel.sh drops under this slice for one-glance cohort
192
- // observability (`systemctl --user status cloudflared.slice`).
192
+ // observability (`systemctl --user status cloudflared.slice`). Task 757
193
+ // changed the per-brand unit from a transient scope to a supervised service.
193
194
  //
194
195
  // Brand-agnostic: one slice across all co-resident brands, mirroring the
195
196
  // Task-250 claude-ptys.slice precedent.
196
197
  // ---------------------------------------------------------------------------
197
198
  export function buildCloudflaredSliceUnitFile() {
198
199
  return `[Unit]
199
- Description=Cloudflare tunnel scopes (Task 602)
200
+ Description=Cloudflare tunnel connectors (Task 602)
200
201
  Documentation=https://github.com/rubytech/maxy-code/blob/main/.tasks/archive/602-cloudflared-tunnel-must-survive-brand-service-restart-via-systemd-scope.md
201
202
  Before=shutdown.target
202
203
 
package/dist/uninstall.js CHANGED
@@ -181,24 +181,25 @@ function stopServices() {
181
181
  catch {
182
182
  console.log(` ${neo4jService} not running`);
183
183
  }
184
- // Stop the cloudflared scope for this brand. The scope unit name mirrors
185
- // what resume-tunnel.sh derives: strip leading dot from configDir.
186
- // BRAND.configDir is ".maxy-code"; strip the dot → "maxy-code".
184
+ // Stop the cloudflared service for this brand (Task 757 the connector now
185
+ // runs as a supervised cloudflared-<brand>.service, not a transient scope).
186
+ // The unit name mirrors what resume-tunnel.sh derives: strip leading dot from
187
+ // configDir. BRAND.configDir is ".maxy-code"; strip the dot → "maxy-code".
187
188
  // --no-block: uninstall does not wait for graceful shutdown of the tunnel.
188
- // Exit 5 (no such unit) = scope absent = success.
189
+ // Exit 5 (no such unit) = service absent = success.
189
190
  const brand = BRAND.configDir.replace(/^\./, "");
190
- const scopeUnit = `cloudflared-${brand}.scope`;
191
- const stopResult = spawnSync("systemctl", ["--user", "stop", "--no-block", scopeUnit], { stdio: "pipe" });
191
+ const serviceUnit = `cloudflared-${brand}.service`;
192
+ const stopResult = spawnSync("systemctl", ["--user", "stop", "--no-block", serviceUnit], { stdio: "pipe" });
192
193
  if (stopResult.status === 0) {
193
- console.log(` Stopped ${scopeUnit}`);
194
+ console.log(` Stopped ${serviceUnit}`);
194
195
  }
195
196
  else if (stopResult.status === 5) {
196
- // exit 5 = unit not found — scope already absent, which is expected
197
- console.log(` ${scopeUnit} not running (scope absent)`);
197
+ // exit 5 = unit not found — service already absent, which is expected
198
+ console.log(` ${serviceUnit} not running (service absent)`);
198
199
  }
199
200
  else {
200
201
  // Any other non-zero exit means systemctl itself failed (e.g. DBus unavailable)
201
- console.log(` systemctl stop ${scopeUnit} exited ${stopResult.status} — scope may still be running`);
202
+ console.log(` systemctl stop ${serviceUnit} exited ${stopResult.status} — service may still be running`);
202
203
  }
203
204
  // Brand isolation: the VNC stack and Ollama daemon are
204
205
  // device-wide singletons. Killing them during uninstall would interrupt
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy-code",
3
- "version": "0.1.287",
3
+ "version": "0.1.289",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy-code": "./dist/index.js"
@@ -104,6 +104,47 @@ describe("resolveCapabilitiesHere", () => {
104
104
  ]);
105
105
  expect(result.registeredAgentTypes).toEqual(["plugin:real-agent:negotiator"]);
106
106
  });
107
+ it("counts the SiteOffice specialist on a siteoffice brand and not on others", async () => {
108
+ // siteoffice brand: ships the bundle, so its agents/ dir is walked and the
109
+ // one specialist (file basename, per the <bundle>--<name>.md convention) is
110
+ // counted under installedPremiumSpecialists.
111
+ const shipped = seedTree(root, {
112
+ accountId: "acct-so",
113
+ productName: "SiteOffice",
114
+ shipsPremiumBundles: ["siteoffice"],
115
+ platformSpecialists: ["project-manager"],
116
+ premiumSpecialists: {
117
+ siteoffice: ["siteoffice--site-manager"],
118
+ },
119
+ registeredAgents: ["plugin:siteoffice:site-manager", "plugin:admin:project-manager"],
120
+ });
121
+ const shippedResult = await resolveCapabilitiesHere(shipped.input, "2026-06-10T07:00:00Z");
122
+ expect(shippedResult.installedPremiumSpecialists).toEqual([
123
+ { bundle: "siteoffice", name: "siteoffice--site-manager" },
124
+ ]);
125
+ // Name collision guard: the registry lists the core project-manager and the
126
+ // siteoffice specialist under distinct names.
127
+ expect(shippedResult.registeredAgentTypes).toEqual([
128
+ "plugin:admin:project-manager",
129
+ "plugin:siteoffice:site-manager",
130
+ ]);
131
+ // Brand isolation: a brand that does not ship siteoffice never walks the
132
+ // bundle's agents/, even with the same specialist file on disk.
133
+ const isolated = seedTree(root, {
134
+ accountId: "acct-ra-iso",
135
+ productName: "Real Agent",
136
+ shipsPremiumBundles: ["real-agent"],
137
+ platformSpecialists: ["project-manager"],
138
+ premiumSpecialists: {
139
+ siteoffice: ["siteoffice--site-manager"],
140
+ "real-agent": ["valuer"],
141
+ },
142
+ });
143
+ const isolatedResult = await resolveCapabilitiesHere(isolated.input, "2026-06-10T07:00:00Z");
144
+ expect(isolatedResult.installedPremiumSpecialists).toEqual([
145
+ { bundle: "real-agent", name: "valuer" },
146
+ ]);
147
+ });
107
148
  it("handles missing fields and missing directories gracefully", async () => {
108
149
  const accountConfigPath = join(root, "account.json");
109
150
  const brandConfigPath = join(root, "brand.json");
@@ -1 +1 @@
1
- {"version":3,"file":"capabilities-here.test.js","sourceRoot":"","sources":["../../src/__tests__/capabilities-here.test.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"capabilities-here.test.js","sourceRoot":"","sources":["../../src/__tests__/capabilities-here.test.ts"],"names":[],"mappings":"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"}
@@ -422,7 +422,7 @@ Before persisting, **read every rendered page** of `base.pdf` — full-bleed pag
422
422
 
423
423
  ## 6. Sweep + dispatch
424
424
 
425
- When the durable routine 8.2) runs, or when the operator asks for new acceptances. Mint one **D1-Edit** token here — `${MINTED_D1_EDIT}` — per `api.md` § Minting a narrow token, resolving the permission-group id as `d1-data-capture.md` § 0 does. Reads use the Edit token too: the D1 **query** endpoint rejects a D1-Read token (`d1-data-capture.md`), so a separate read-scoped token is not minted.
425
+ Dispatch runs **only** inside a live operator session on the account's own device — when the operator asks for new acceptances, or when an in-session watcher running inside a still-live session performs the sweep. There is no off-device dispatcher; never register a Claude Code `/schedule` routine or any headless/cloud scheduler to run it (§ 8.2 says why). Mint one **D1-Edit** token here — `${MINTED_D1_EDIT}` — per `api.md` § Minting a narrow token, resolving the permission-group id as `d1-data-capture.md` § 0 does. Reads use the Edit token too: the D1 **query** endpoint rejects a D1-Read token (`d1-data-capture.md`), so a separate read-scoped token is not minted.
426
426
 
427
427
  1. **Read the unswept rows** with `${MINTED_D1_EDIT}`:
428
428
 
@@ -530,16 +530,20 @@ The flow has no application logging — the signal surface is the **D1 row state
530
530
 
531
531
  Signal fires *without reproducing end to end*: the test POST returns non-`ok`, and the confirming `SELECT WHERE swept = 0` shows no new row. Fix: re-mint the token confirming **both** Pages Edit and D1 Edit (`d1-data-capture.md` § 0). First thing to check when acceptances stop arriving.
532
532
 
533
- ### 8.2 Acceptance recorded but never dispatched (no-event) — the durable health check
533
+ ### 8.2 Acceptance recorded but never dispatched (no-event) — the in-session reconciliation check
534
534
 
535
- The sweep never ran, or `email-send` failed after the row existed — no log, because no action was taken. The sweep6) runs from a **durable platform routine**, not a session-only cron: a session cron dies with the session and produces no signal, so aged acceptances would sit undispatched silently. The routine's **health check** is the standing reconciliation:
535
+ The sweep never ran, or `email-send` failed after the row existed — no log, because no action was taken. **Capture is durable; dispatch is not.** The acceptance is recorded at the **edge** in D1 (intro / § 1a) the instant the signer submits nothing is ever lost, whether or not a session is running. **Dispatch** (stamp + email) needs the account's Pi-local tooling — the D1-Edit token, `email-send`, `stamp.mjs`, `ACCOUNT_DIR` — so it happens **only** while an operator session is live. An acceptance captured while no session is running sits **unswept** until the next session runs the sweep; latency is bounded by operator session cadence, not a cron. That is a deliberate limitation of the in-session-only model, not a defect — the durable D1 row guarantees no loss. The standing check is the operator's **session-open / before-close ritual**: run the reconciliation inside a live session, and any aged unswept row is the signal to sweep now.
536
+
537
+ > Dispatch runs **only** inside a live operator session on the account's own device. Never register a Claude Code `/schedule` routine, a `RemoteTrigger`, a `CronCreate` durable job, or any off-device/headless scheduler to run the sweep. `/schedule` spawns a fresh agent on **claude.ai's cloud**, which has none of this account's D1 token, `email-send`, `stamp.mjs`, or `ACCOUNT_DIR` — it physically cannot stamp or send. It is a Claude Code capability, not a Maxy one, and it is not a substitute for the in-session sweep. Monitoring and dispatch happen in-session, while the session is live. That is the only path.
538
+
539
+ The reconciliation query, run inside a live session at open and before close:
536
540
 
537
541
  ```bash
538
542
  CLOUDFLARE_API_TOKEN="${MINTED_D1_EDIT}" wrangler d1 execute <db-name> --remote --command \
539
543
  "SELECT count(*), min(accepted_at) FROM acceptances WHERE swept = 0;"
540
544
  ```
541
545
 
542
- Any row whose `accepted_at` is older than the routine's cadence is an undispatched acceptance — the failure signal. Because § 6 flips `swept` only after both sends verify (and a text-only degrade is treated as a failed send, § 0 item 3), a failed or degraded dispatch leaves the row here for the next pass.
546
+ Any row whose `accepted_at` is older than the operator's session cadence is an undispatched acceptance — the failure signal. Because § 6 flips `swept` only after both sends verify (and a text-only degrade is treated as a failed send, § 0 item 3), a failed or degraded dispatch leaves the row here for the next pass.
543
547
 
544
548
  ### 8.3 Document modified after an acceptance, or base render missing (signed-content drift)
545
549
 
@@ -226,6 +226,7 @@ Only if that line prints nothing (all vars set) do you proceed to write the conf
226
226
  cat > "${CFG_DIR}/config.yml" <<EOF
227
227
  tunnel: ${TUNNEL_ID}
228
228
  credentials-file: ${CFG_DIR}/${TUNNEL_ID}.json
229
+ no-autoupdate: true
229
230
  ingress:
230
231
  - hostname: admin.maxy.bot
231
232
  service: http://localhost:${PORT}
@@ -235,7 +236,7 @@ ingress:
235
236
  EOF
236
237
  ```
237
238
 
238
- **Why:** Tells cloudflared at startup which tunnel to run, where its credentials are, and how to route each hostname to the brand's local service port. The trailing `http_status:404` line is the mandatory catch-all.
239
+ **Why:** Tells cloudflared at startup which tunnel to run, where its credentials are, and how to route each hostname to the brand's local service port. The trailing `http_status:404` line is the mandatory catch-all. `no-autoupdate: true` stops the connector downloading a new binary and replacing itself (which exits the running process); the cloudflared version is installer-owned and bumped deliberately. This is belt-and-braces with the `--no-autoupdate` flag `resume-tunnel.sh` already passes on the spawn argv — the flag covers every existing install on its next service restart, this line covers a connector started any other way from a freshly-provisioned config. Task 757.
239
240
 
240
241
  **Success:** Confirm with:
241
242
 
@@ -425,7 +426,22 @@ A non-530 response means the tunnel is live.
425
426
 
426
427
  **Do NOT use `sudo cloudflared service install`.** It writes `/etc/systemd/system/cloudflared.service`, copies your config to `/etc/cloudflared/config.yml` (a system-wide path), and runs the connector as root — breaking brand isolation on any device that hosts more than one brand under the same Linux user. We tried this path on 2026-04-19; it created a duplicate connector parallel to the existing user-space one and required an explicit uninstall to undo.
427
428
 
428
- The correct production pattern is already in place on every Maxy device: the brand's platform UI itself is a **user-space systemd service** at `~/.config/systemd/user/<brand>.service`, and that service spawns the cloudflared connector as an `ExecStartPre=` via `platform/scripts/resume-tunnel.sh`. When the user-space service restarts, the connector restarts with it. The connector runs as the Linux user (not root), its config is read from `${CFG_DIR}/config.yml` (brand-scoped, never `/etc`), and multiple brands coexist because each has its own unit file (`maxy.service`, `realagent.service`, etc.).
429
+ The correct production pattern is already in place on every Maxy device: the brand's platform UI itself is a **user-space systemd service** at `~/.config/systemd/user/<brand>.service`, and that service spawns the cloudflared connector as an `ExecStartPre=` via `platform/scripts/resume-tunnel.sh`. The connector runs as the Linux user (not root), its config is read from `${CFG_DIR}/config.yml` (brand-scoped, never `/etc`), and multiple brands coexist because each has its own unit file (`maxy.service`, `realagent.service`, etc.).
430
+
431
+ **The connector is a supervised, self-healing unit (Task 602 + Task 757).** `resume-tunnel.sh` does not run cloudflared as a child of the brand service. It spawns it into its own transient systemd **service** — `cloudflared-<brand>.service` under `cloudflared.slice` — with `Restart=always`. Two consequences: a brand-service restart no longer reaps the connector (Task 602's cgroup decoupling), and systemd resurrects the connector on **any** exit — crash, OOM, manual kill (Task 757). The connector also runs with `--no-autoupdate`, so it never downloads a new binary and replaces itself; the version is installer-owned (pinned in the installer, bumped deliberately). `StartLimitIntervalSec=300`/`StartLimitBurst=5` mean a genuinely broken binary surfaces as a `failed` unit instead of looping forever.
432
+
433
+ **Observability.** A connector death is now a logged, countable event, not an invisible gap:
434
+
435
+ ```
436
+ # Every restart systemd performs is journaled for the connector's own unit:
437
+ journalctl --user -u cloudflared-<brand>.service | grep "Scheduled restart job"
438
+ # (each line: "Scheduled restart job, restart counter is at N")
439
+
440
+ # Confirm auto-update is off — cloudflared prints its effective settings at startup:
441
+ grep "no-autoupdate:true" "${CFG_DIR}/logs/cloudflared.log"
442
+ ```
443
+
444
+ `no-autoupdate:true` in cloudflared's startup `Settings:` line is the per-box rollout acceptance signal. A unit that has tripped its start limit shows `Active: failed` under `systemctl --user status cloudflared-<brand>.service` — investigate the binary rather than restarting blindly.
429
445
 
430
446
  **Durability prerequisites (one-time per device):**
431
447
 
@@ -1,5 +1,8 @@
1
1
  #!/usr/bin/env bash
2
- # Tests for resume-tunnel.sh scope-based spawn path (Task 602).
2
+ # Tests for resume-tunnel.sh supervised-service spawn path (Task 602 + Task 757).
3
+ # Task 602 decoupled the connector into its own transient unit under
4
+ # cloudflared.slice; Task 757 changed that unit from a --scope (no restart
5
+ # policy) to a --service with Restart=always and --no-autoupdate.
3
6
  # Stubs systemd-run, systemctl, and cloudflared binaries via a fake $PATH entry.
4
7
  set -uo pipefail
5
8
 
@@ -80,10 +83,12 @@ exit 0
80
83
  SH
81
84
  chmod +x "$bin_dir/systemctl"
82
85
 
83
- # Stub systemd-run: logs its --unit= and --slice= args, optionally writes connection line.
86
+ # Stub systemd-run: echoes every arg (so tests can assert the unit name, slice,
87
+ # restart properties, and the connector argv), optionally writes connection line.
84
88
  cat > "$bin_dir/systemd-run" <<SH
85
89
  #!/bin/bash
86
90
  for arg in "\$@"; do
91
+ echo "[stub] arg=\$arg"
87
92
  case "\$arg" in
88
93
  --unit=*) echo "[stub] unit=\${arg#--unit=}" ;;
89
94
  --slice=*) echo "[stub] slice=\${arg#--slice=}" ;;
@@ -120,7 +125,7 @@ case_skip_when_scope_active() {
120
125
  if [[ $rc -ne 0 ]]; then
121
126
  echo " expected exit 0, got $rc"; return 1
122
127
  fi
123
- if ! echo "$log_content" | grep -q "already running.*scope.*cloudflared-maxy-code"; then
128
+ if ! echo "$log_content" | grep -q "already running.*service.*cloudflared-maxy-code.service"; then
124
129
  echo " skip branch missing expected log line"
125
130
  echo "$log_content"
126
131
  return 1
@@ -149,8 +154,8 @@ case_spawn_succeeds() {
149
154
  if [[ $rc -ne 0 ]]; then
150
155
  echo " expected exit 0, got $rc"; return 1
151
156
  fi
152
- if ! echo "$log_content" | grep -q "spawned scope=cloudflared-maxy-code.scope"; then
153
- echo " missing spawned scope log line"; echo "$log_content"; return 1
157
+ if ! echo "$log_content" | grep -q "spawned service=cloudflared-maxy-code.service"; then
158
+ echo " missing spawned service log line"; echo "$log_content"; return 1
154
159
  fi
155
160
  if ! echo "$log_content" | grep -q "tunnel verified"; then
156
161
  echo " missing tunnel verified log line"; echo "$log_content"; return 1
@@ -186,7 +191,7 @@ case_spawn_no_connection() {
186
191
  return 0
187
192
  }
188
193
 
189
- # CASE 4: scope unit name derived correctly from configDir (.real-agent → cloudflared-real-agent.scope)
194
+ # CASE 4: service unit name derived correctly from configDir (.real-agent → cloudflared-real-agent.service)
190
195
  case_scope_unit_name_from_config_dir() {
191
196
  local root
192
197
  root=$(mktemp -d /tmp/resume-tunnel-test-XXXXXX)
@@ -207,8 +212,8 @@ case_scope_unit_name_from_config_dir() {
207
212
  if [[ $rc -ne 0 ]]; then
208
213
  echo " expected exit 0, got $rc"; return 1
209
214
  fi
210
- if ! echo "$log_content" | grep -q "spawned scope=cloudflared-real-agent.scope"; then
211
- echo " scope unit name not derived correctly from .real-agent"
215
+ if ! echo "$log_content" | grep -q "spawned service=cloudflared-real-agent.service"; then
216
+ echo " service unit name not derived correctly from .real-agent"
212
217
  echo "$log_content"; return 1
213
218
  fi
214
219
  return 0
@@ -240,11 +245,43 @@ case_slice_flag_present() {
240
245
  return 0
241
246
  }
242
247
 
243
- run_case "skip when scope active" case_skip_when_scope_active
248
+ # CASE 6: supervision contract — Restart=always, crash-loop limits, --no-autoupdate,
249
+ # and the unit is a .service (Task 757), all reaching systemd-run.
250
+ case_supervision_contract() {
251
+ local root
252
+ root=$(mktemp -d /tmp/resume-tunnel-test-XXXXXX)
253
+ setup "$root" ".maxy-code" "inactive" "yes"
254
+
255
+ local log="$root/home/.maxy-code/logs/cloudflared.log"
256
+
257
+ HOME="$root/home" \
258
+ PATH="$root/bin:$PATH" \
259
+ MAXY_PLATFORM_ROOT="$root/platform_root" \
260
+ bash "$RESUME"
261
+
262
+ local log_content
263
+ log_content=$(cat "$log" 2>/dev/null || echo "")
264
+ cleanup "$root"
265
+
266
+ local missing=""
267
+ echo "$log_content" | grep -q "\[stub\] arg=--unit=cloudflared-maxy-code.service" || missing="$missing unit=.service"
268
+ echo "$log_content" | grep -q "\[stub\] arg=Restart=always" || missing="$missing Restart=always"
269
+ echo "$log_content" | grep -q "\[stub\] arg=StartLimitIntervalSec=300" || missing="$missing StartLimitIntervalSec"
270
+ echo "$log_content" | grep -q "\[stub\] arg=StartLimitBurst=5" || missing="$missing StartLimitBurst"
271
+ echo "$log_content" | grep -q "\[stub\] arg=--no-autoupdate" || missing="$missing --no-autoupdate"
272
+ if [[ -n "$missing" ]]; then
273
+ echo " systemd-run argv missing:$missing"
274
+ echo "$log_content"; return 1
275
+ fi
276
+ return 0
277
+ }
278
+
279
+ run_case "skip when service active" case_skip_when_scope_active
244
280
  run_case "spawn succeeds and logs verified" case_spawn_succeeds
245
281
  run_case "spawn with no connection logs ERROR exits 0" case_spawn_no_connection
246
- run_case "scope unit name derived from configDir" case_scope_unit_name_from_config_dir
282
+ run_case "service unit name derived from configDir" case_scope_unit_name_from_config_dir
247
283
  run_case "slice flag passed to systemd-run" case_slice_flag_present
284
+ run_case "supervision contract: restart-always + no-autoupdate + .service" case_supervision_contract
248
285
 
249
286
  echo ""
250
287
  echo "Results: $PASS passed, $FAIL failed"
@@ -1,9 +1,15 @@
1
1
  #!/bin/bash
2
2
  # resume-tunnel.sh — Resume the Cloudflare tunnel on brand-service start.
3
3
  #
4
- # Spawns cloudflared into its own transient systemd scope (cloudflared-<brand>.scope)
4
+ # Spawns cloudflared into its own transient systemd SERVICE (cloudflared-<brand>.service)
5
5
  # under cloudflared.slice, completely decoupled from the brand service's cgroup.
6
- # A brand-service restart can no longer reap the tunnel. Task 602.
6
+ # A brand-service restart can no longer reap the tunnel (Task 602's cgroup
7
+ # decoupling). The unit is a Restart=always service (Task 757), so systemd
8
+ # resurrects the connector on ANY exit — crash, OOM, manual kill — and the
9
+ # connector runs with --no-autoupdate so it never replaces itself; the binary
10
+ # version is installer-owned. This is still the single spawner: only the unit
11
+ # type changed from a transient --scope (no restart policy) to a supervised
12
+ # --service. No second spawner is introduced.
7
13
  #
8
14
  # Called as ExecStartPre in maxy.service. All failures exit 0 — this script must
9
15
  # never prevent maxy.service from starting.
@@ -49,16 +55,16 @@ if [ -z "$TUNNEL_ID" ]; then
49
55
  exit 0
50
56
  fi
51
57
 
52
- # Derive scope unit name: strip leading dot from CONFIG_DIR.
53
- # .maxy-code → cloudflared-maxy-code.scope
58
+ # Derive service unit name: strip leading dot from CONFIG_DIR.
59
+ # .maxy-code → cloudflared-maxy-code.service
54
60
  BRAND="${CONFIG_DIR#.}"
55
- SCOPE_UNIT="cloudflared-${BRAND}.scope"
61
+ SERVICE_UNIT="cloudflared-${BRAND}.service"
56
62
 
57
- # Liveness check: query the scope unit state, not a recorded PID.
58
- # The scope is owned by user-systemd, fully outside the brand service's cgroup.
59
- if systemctl --user is-active "$SCOPE_UNIT" >/dev/null 2>&1; then
60
- SCOPE_PID=$(systemctl --user show "$SCOPE_UNIT" --property=MainPID --value 2>/dev/null || echo "unknown")
61
- log "Tunnel already running (scope $SCOPE_UNIT active, MainPID $SCOPE_PID) — skipping resume"
63
+ # Liveness check: query the service unit state, not a recorded PID.
64
+ # The service is owned by user-systemd, fully outside the brand service's cgroup.
65
+ if systemctl --user is-active "$SERVICE_UNIT" >/dev/null 2>&1; then
66
+ SERVICE_PID=$(systemctl --user show "$SERVICE_UNIT" --property=MainPID --value 2>/dev/null || echo "unknown")
67
+ log "Tunnel already running (service $SERVICE_UNIT active, MainPID $SERVICE_PID) — skipping resume"
62
68
  exit 0
63
69
  fi
64
70
 
@@ -82,26 +88,37 @@ log "Resuming tunnel $TUNNEL_ID for $DOMAIN..."
82
88
  mkdir -p "$LOG_DIR"
83
89
  LOG_OFFSET=$(wc -c < "$LOG_FILE" 2>/dev/null | awk '{print $1+0}' || echo 0)
84
90
 
85
- # Spawn into a transient scope under cloudflared.slice.
86
- # The scope is owned by user-systemd, not by the brand service's cgroup.
87
- # systemd-run --scope blocks until cloudflared exits (it is the scope leader),
88
- # so we background it. $? after & is always 0 (fork success); the verification
89
- # gate below detects actual spawn failures via connection timeout.
90
- # stdout/stderr of cloudflared append to LOG_FILE via the redirect.
91
+ # Spawn into a SUPERVISED transient service under cloudflared.slice (Task 757).
92
+ # Unlike a --scope (no restart policy), a Restart=always service is resurrected
93
+ # by systemd on ANY exit crash, OOM, manual kill. --no-autoupdate stops the
94
+ # connector replacing its own binary, so the version stays installer-owned.
95
+ # StartLimitIntervalSec/StartLimitBurst trip a genuinely broken binary to
96
+ # `failed` instead of looping forever (5 starts within 300s → failed).
97
+ # A service's stdout/stderr go to the journal, not this shell, so route them to
98
+ # LOG_FILE via StandardOutput/StandardError=append so the verification gate
99
+ # below and the operator's cloudflared.log keep working. systemd-run returns
100
+ # once the unit is active (it does not block for the process lifetime), so there
101
+ # is no shell child to background. --quiet suppresses systemd-run's "Running as
102
+ # unit:" line; the trailing redirect only captures any error output it prints.
91
103
  systemd-run \
92
104
  --user \
93
- --scope \
94
105
  --quiet \
95
- --unit="$SCOPE_UNIT" \
106
+ --unit="$SERVICE_UNIT" \
96
107
  --slice=cloudflared \
108
+ -p Restart=always \
109
+ -p RestartSec=2 \
110
+ -p StartLimitIntervalSec=300 \
111
+ -p StartLimitBurst=5 \
97
112
  -p TimeoutStopSec=15 \
113
+ -p "StandardOutput=append:$LOG_FILE" \
114
+ -p "StandardError=append:$LOG_FILE" \
98
115
  -- \
99
- "$CLOUDFLARED_BIN" --origincert "$CERT_PATH" --config "$CONFIG_PATH" tunnel run \
100
- >> "$LOG_FILE" 2>&1 &
116
+ "$CLOUDFLARED_BIN" --no-autoupdate --origincert "$CERT_PATH" --config "$CONFIG_PATH" tunnel run \
117
+ >> "$LOG_FILE" 2>&1
101
118
 
102
- # Log spawn receipt so the operator can distinguish the scoped path from legacy nohup.
103
- SCOPE_PID=$(systemctl --user show "$SCOPE_UNIT" --property=MainPID --value 2>/dev/null || echo "unknown")
104
- log "spawned scope=$SCOPE_UNIT slice=cloudflared main-pid=$SCOPE_PID"
119
+ # Log spawn receipt so the operator can distinguish the supervised path from legacy nohup.
120
+ SERVICE_PID=$(systemctl --user show "$SERVICE_UNIT" --property=MainPID --value 2>/dev/null || echo "unknown")
121
+ log "spawned service=$SERVICE_UNIT slice=cloudflared main-pid=$SERVICE_PID"
105
122
 
106
123
  # Post-spawn connection verification gate.
107
124
  # Polls only new log content (from LOG_OFFSET onwards) to avoid false positives
@@ -127,16 +144,16 @@ done
127
144
  if [ "$VERIFIED" -eq 1 ]; then
128
145
  ELAPSED=$(( $(date +%s) - VERIFY_START ))
129
146
  CONN_COUNT=$(tail -c "+$((LOG_OFFSET + 1))" "$LOG_FILE" 2>/dev/null | grep -c "Registered tunnel connection" || echo "?")
130
- log "tunnel verified scope=$SCOPE_UNIT connections=$CONN_COUNT ms=$(( ELAPSED * 1000 ))"
147
+ log "tunnel verified service=$SERVICE_UNIT connections=$CONN_COUNT ms=$(( ELAPSED * 1000 ))"
131
148
  else
132
- log "ERROR: tunnel scope started but no edge connection within ${VERIFY_DEADLINE}s — scope=$SCOPE_UNIT"
149
+ log "ERROR: tunnel service started but no edge connection within ${VERIFY_DEADLINE}s — service=$SERVICE_UNIT"
133
150
  fi
134
151
 
135
- # Update state file: keep pid/startedAt for observability, add scopeUnit as control signal.
152
+ # Update state file: keep pid/startedAt for observability, add serviceUnit as control signal.
136
153
  NEW_STATE=$(echo "$STATE" | jq \
137
- --arg unit "$SCOPE_UNIT" \
154
+ --arg unit "$SERVICE_UNIT" \
138
155
  --argjson ts "$(date +%s)000" \
139
- '.scopeUnit = $unit | .startedAt = $ts' 2>/dev/null || echo "")
156
+ '.serviceUnit = $unit | .startedAt = $ts' 2>/dev/null || echo "")
140
157
 
141
158
  if [ -n "$NEW_STATE" ] && echo "$NEW_STATE" > "$STATE_FILE.tmp" 2>/dev/null && mv "$STATE_FILE.tmp" "$STATE_FILE" 2>/dev/null; then
142
159
  :