@rubytech/create-maxy-code 0.1.233 → 0.1.235
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/__tests__/cloudflared-slice.test.js +19 -0
- package/dist/index.js +7 -1
- package/dist/port-resolution.js +20 -0
- package/dist/uninstall.js +18 -15
- package/package.json +1 -1
- package/payload/platform/plugins/replicate/mcp/dist/tools/image-generate.d.ts.map +1 -1
- package/payload/platform/plugins/replicate/mcp/dist/tools/image-generate.js +29 -20
- package/payload/platform/plugins/replicate/mcp/dist/tools/image-generate.js.map +1 -1
- package/payload/platform/scripts/__tests__/resume-tunnel.test.sh +251 -0
- package/payload/platform/scripts/resume-tunnel.sh +86 -48
- package/payload/platform/templates/specialists/agents/content-producer.md +1 -1
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Task 602 — cloudflared.slice unit emitted at install time.
|
|
2
|
+
// Every cloudflared-<brand>.scope spawned by resume-tunnel.sh drops under
|
|
3
|
+
// this slice for cohort observability via `systemctl --user status cloudflared.slice`.
|
|
4
|
+
import test from "node:test";
|
|
5
|
+
import assert from "node:assert/strict";
|
|
6
|
+
import { buildCloudflaredSliceUnitFile } from "../port-resolution.js";
|
|
7
|
+
test("buildCloudflaredSliceUnitFile emits a valid systemd slice unit", () => {
|
|
8
|
+
const unit = buildCloudflaredSliceUnitFile();
|
|
9
|
+
assert.match(unit, /^\[Unit\]$/m);
|
|
10
|
+
assert.match(unit, /^Description=Cloudflare tunnel scopes \(Task 602\)$/m);
|
|
11
|
+
assert.match(unit, /^\[Slice\]$/m);
|
|
12
|
+
assert.match(unit, /^\[Install\]$/m);
|
|
13
|
+
assert.match(unit, /^WantedBy=default\.target$/m);
|
|
14
|
+
});
|
|
15
|
+
test("buildCloudflaredSliceUnitFile output has no per-brand template tokens", () => {
|
|
16
|
+
const unit = buildCloudflaredSliceUnitFile();
|
|
17
|
+
assert.doesNotMatch(unit, /\$\{/);
|
|
18
|
+
assert.doesNotMatch(unit, /__[A-Z_]+__/);
|
|
19
|
+
});
|
package/dist/index.js
CHANGED
|
@@ -5,7 +5,7 @@ import { registerSpecialistAgentsAt, SpecialistSymlinkCollision } from "./specia
|
|
|
5
5
|
import { seedBypassPermissionsSettings, assertBypassPermissionsSeed } from "./permissions-seed.js";
|
|
6
6
|
import { resolve, join, dirname } from "node:path";
|
|
7
7
|
import { randomBytes } from "node:crypto";
|
|
8
|
-
import { resolveInstallPortFromFs, buildMaxyUnitFile, buildClaudeSessionManagerUnitFile, buildClaudePtysSliceUnitFile } from "./port-resolution.js";
|
|
8
|
+
import { resolveInstallPortFromFs, buildMaxyUnitFile, buildClaudeSessionManagerUnitFile, buildClaudePtysSliceUnitFile, buildCloudflaredSliceUnitFile } from "./port-resolution.js";
|
|
9
9
|
import { validateTierFlag, validateEntitlementBase64, applyTierToAccountConfig, entitlementPath } from "./tier-flag.js";
|
|
10
10
|
import { parseOsRelease, isUbuntuLike as isUbuntuLikePure, parseAptCacheCandidate, decideAptResolution, } from "./apt-resolve.js";
|
|
11
11
|
import { findPeerBrandOnDefaultNeo4jPort } from "./peer-brand-detect.js";
|
|
@@ -3120,6 +3120,12 @@ function installService() {
|
|
|
3120
3120
|
const claudePtysSliceName = "claude-ptys.slice";
|
|
3121
3121
|
writeFileSync(join(serviceDir, claudePtysSliceName), buildClaudePtysSliceUnitFile());
|
|
3122
3122
|
logFile(` ${claudePtysSliceName}: Task 250 slice for claude-session-*.scope cohort`);
|
|
3123
|
+
// Task 602 — write the brand-agnostic `cloudflared.slice` unit. Every
|
|
3124
|
+
// cloudflared-<brand>.scope spawned by resume-tunnel.sh drops under this
|
|
3125
|
+
// slice. Writing once per install is idempotent; daemon-reload below picks it up.
|
|
3126
|
+
const cloudflaredSliceName = "cloudflared.slice";
|
|
3127
|
+
writeFileSync(join(serviceDir, cloudflaredSliceName), buildCloudflaredSliceUnitFile());
|
|
3128
|
+
logFile(` ${cloudflaredSliceName}: Task 602 slice for cloudflared-*.scope cohort`);
|
|
3123
3129
|
// Task 600 — per-brand RSS sampler: a long-running user service that
|
|
3124
3130
|
// periodically writes per-claude.exe RSS + aggregate RSS to disk.
|
|
3125
3131
|
// Named per-brand (BRAND.hostname prefix) so two brands on the same
|
package/dist/port-resolution.js
CHANGED
|
@@ -186,3 +186,23 @@ Before=shutdown.target
|
|
|
186
186
|
WantedBy=default.target
|
|
187
187
|
`;
|
|
188
188
|
}
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
// Task 602 — `cloudflared.slice` unit. Every cloudflared-<brand>.scope
|
|
191
|
+
// spawned by resume-tunnel.sh drops under this slice for one-glance cohort
|
|
192
|
+
// observability (`systemctl --user status cloudflared.slice`).
|
|
193
|
+
//
|
|
194
|
+
// Brand-agnostic: one slice across all co-resident brands, mirroring the
|
|
195
|
+
// Task-250 claude-ptys.slice precedent.
|
|
196
|
+
// ---------------------------------------------------------------------------
|
|
197
|
+
export function buildCloudflaredSliceUnitFile() {
|
|
198
|
+
return `[Unit]
|
|
199
|
+
Description=Cloudflare tunnel scopes (Task 602)
|
|
200
|
+
Documentation=https://github.com/rubytech/maxy-code/blob/main/.tasks/archive/602-cloudflared-tunnel-must-survive-brand-service-restart-via-systemd-scope.md
|
|
201
|
+
Before=shutdown.target
|
|
202
|
+
|
|
203
|
+
[Slice]
|
|
204
|
+
|
|
205
|
+
[Install]
|
|
206
|
+
WantedBy=default.target
|
|
207
|
+
`;
|
|
208
|
+
}
|
package/dist/uninstall.js
CHANGED
|
@@ -181,21 +181,24 @@ function stopServices() {
|
|
|
181
181
|
catch {
|
|
182
182
|
console.log(` ${neo4jService} not running`);
|
|
183
183
|
}
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
//
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
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".
|
|
187
|
+
// --no-block: uninstall does not wait for graceful shutdown of the tunnel.
|
|
188
|
+
// Exit 5 (no such unit) = scope absent = success.
|
|
189
|
+
const brand = BRAND.configDir.replace(/^\./, "");
|
|
190
|
+
const scopeUnit = `cloudflared-${brand}.scope`;
|
|
191
|
+
const stopResult = spawnSync("systemctl", ["--user", "stop", "--no-block", scopeUnit], { stdio: "pipe" });
|
|
192
|
+
if (stopResult.status === 0) {
|
|
193
|
+
console.log(` Stopped ${scopeUnit}`);
|
|
194
|
+
}
|
|
195
|
+
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)`);
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
// 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`);
|
|
199
202
|
}
|
|
200
203
|
// Brand isolation: the VNC stack and Ollama daemon are
|
|
201
204
|
// device-wide singletons. Killing them during uninstall would interrupt
|
package/package.json
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image-generate.d.ts","sourceRoot":"","sources":["../../src/tools/image-generate.ts"],"names":[],"mappings":"AAgCA,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,wBAAsB,aAAa,CACjC,KAAK,EAAE,kBAAkB,EACzB,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC;IAAE,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAAC,OAAO,CAAC,EAAE,IAAI,CAAA;CAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"image-generate.d.ts","sourceRoot":"","sources":["../../src/tools/image-generate.ts"],"names":[],"mappings":"AAgCA,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,wBAAsB,aAAa,CACjC,KAAK,EAAE,kBAAkB,EACzB,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC;IAAE,OAAO,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAAC,OAAO,CAAC,EAAE,IAAI,CAAA;CAAE,CAAC,CAoL7E"}
|
|
@@ -99,29 +99,38 @@ export async function imageGenerate(input, accountId, platformRoot) {
|
|
|
99
99
|
};
|
|
100
100
|
}
|
|
101
101
|
// --- Extract image URL from output ---
|
|
102
|
-
// Replicate output varies by model:
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
102
|
+
// Replicate output varies by model: string URL, FileOutput, or array thereof.
|
|
103
|
+
// FileOutput has a url() method (returns URL object) and toString() which in some
|
|
104
|
+
// SDK versions returns a CSS-wrapped url(https://...) rather than a bare URL.
|
|
105
|
+
const extractUrl = (val) => {
|
|
106
|
+
if (typeof val === "string")
|
|
107
|
+
return val.startsWith("http") ? val : null;
|
|
108
|
+
if (!val || typeof val !== "object")
|
|
109
|
+
return null;
|
|
110
|
+
const obj = val;
|
|
111
|
+
if (typeof obj["url"] === "function") {
|
|
112
|
+
try {
|
|
113
|
+
return obj["url"]().href;
|
|
114
|
+
}
|
|
115
|
+
catch { /* fall through */ }
|
|
111
116
|
}
|
|
112
|
-
|
|
113
|
-
|
|
117
|
+
let str = String(val);
|
|
118
|
+
if (str.startsWith("url(") && str.endsWith(")"))
|
|
119
|
+
str = str.slice(4, -1);
|
|
120
|
+
return str.startsWith("http") ? str : null;
|
|
121
|
+
};
|
|
122
|
+
let imageUrl = null;
|
|
123
|
+
if (Array.isArray(output)) {
|
|
124
|
+
for (const item of output) {
|
|
125
|
+
const u = extractUrl(item);
|
|
126
|
+
if (u) {
|
|
127
|
+
imageUrl = u;
|
|
128
|
+
break;
|
|
129
|
+
}
|
|
114
130
|
}
|
|
115
131
|
}
|
|
116
|
-
else
|
|
117
|
-
imageUrl = output
|
|
118
|
-
}
|
|
119
|
-
// FileOutput objects can be coerced to URL string via toString()
|
|
120
|
-
if (!imageUrl && output && typeof output === "object" && typeof output.toString === "function") {
|
|
121
|
-
const str = String(output);
|
|
122
|
-
if (str.startsWith("http")) {
|
|
123
|
-
imageUrl = str;
|
|
124
|
-
}
|
|
132
|
+
else {
|
|
133
|
+
imageUrl = extractUrl(output);
|
|
125
134
|
}
|
|
126
135
|
if (!imageUrl) {
|
|
127
136
|
const latencyMs = Date.now() - startMs;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"image-generate.js","sourceRoot":"","sources":["../../src/tools/image-generate.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,SAAS,CAAC;AACnD,OAAO,SAAS,MAAM,WAAW,CAAC;AAClC,OAAO,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAC;AAChC,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAEpD,8EAA8E;AAC9E,iBAAiB;AACjB,8EAA8E;AAE9E,MAAM,MAAM,GAAmD;IAC7D,iBAAiB,EAAE,EAAE,EAAE,EAAE,wBAAwB,EAAE;IACnD,YAAY,EAAE,EAAE,EAAE,EAAE,uBAAuB,EAAE,KAAK,EAAE,2BAA2B,EAAE;IACjF,cAAc,EAAE,EAAE,EAAE,EAAE,gCAAgC,EAAE;CACzD,CAAC;AAEF,8EAA8E;AAC9E,uEAAuE;AACvE,8EAA8E;AAE9E,MAAM,aAAa,GAAsD;IACvE,KAAK,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE;IACpC,KAAK,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE;IACnC,KAAK,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE;IACnC,MAAM,EAAE,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE;IACpC,MAAM,EAAE,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE;CACrC,CAAC;AAaF,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,KAAyB,EACzB,SAAiB,EACjB,YAAoB;IAEpB,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,GAAG,KAAK,EAAE,YAAY,GAAG,KAAK,EAAE,GAAG,KAAK,CAAC;IAE3E,sBAAsB;IACtB,MAAM,WAAW,GAAG,SAAS,EAAE,CAAC;IAChC,IAAI,CAAC,WAAW,EAAE,CAAC;QACjB,OAAO,CAAC,KAAK,CAAC,2BAA2B,CAAC,CAAC;QAC3C,OAAO;YACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,wEAAwE,EAAE,CAAC;YAC3G,OAAO,EAAE,IAAI;SACd,CAAC;IACJ,CAAC;IAED,yBAAyB;IACzB,IAAI,YAAY,KAAK,KAAK,IAAI,KAAK,KAAK,YAAY,EAAE,CAAC;QACrD,OAAO;YACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,+CAA+C,EAAE,CAAC;YAClF,OAAO,EAAE,IAAI;SACd,CAAC;IACJ,CAAC;IAED,2BAA2B;IAC3B,MAAM,UAAU,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IACjC,IAAI,CAAC,UAAU,EAAE,CAAC;QAChB,OAAO;YACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,kBAAkB,KAAK,EAAE,EAAE,CAAC;YAC5D,OAAO,EAAE,IAAI;SACd,CAAC;IACJ,CAAC;IACD,MAAM,OAAO,GAAG,YAAY,KAAK,KAAK,IAAI,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,UAAU,CAAC,EAAE,CAAC;IAE9F,OAAO,CAAC,KAAK,CAAC,gCAAgC,KAAK,WAAW,YAAY,WAAW,WAAW,EAAE,CAAC,CAAC;IACpG,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;IAE3B,4BAA4B;IAC5B,MAAM,UAAU,GAAG,aAAa,CAAC,WAAW,CAAC,IAAI,aAAa,CAAC,KAAK,CAAC,CAAC;IACtE,MAAM,UAAU,GAA4B;QAC1C,MAAM;QACN,KAAK,EAAE,UAAU,CAAC,KAAK;QACvB,MAAM,EAAE,UAAU,CAAC,MAAM;KAC1B,CAAC;IACF,mEAAmE;IACnE,IAAI,KAAK,KAAK,cAAc,EAAE,CAAC;QAC7B,UAAU,CAAC,YAAY,GAAG,WAAW,CAAC;QACtC,OAAO,UAAU,CAAC,KAAK,CAAC;QACxB,OAAO,UAAU,CAAC,MAAM,CAAC;IAC3B,CAAC;IACD,IAAI,YAAY,KAAK,KAAK,EAAE,CAAC;QAC3B,UAAU,CAAC,aAAa,GAAG,YAAY,CAAC;IAC1C,CAAC;IAED,yBAAyB;IACzB,MAAM,SAAS,GAAG,IAAI,SAAS,CAAC,EAAE,IAAI,EAAE,WAAW,CAAC,KAAK,EAAE,CAAC,CAAC;IAC7D,IAAI,MAAe,CAAC;IACpB,IAAI,CAAC;QACH,MAAM,GAAG,MAAM,SAAS,CAAC,GAAG,CAAC,OAAgC,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,CAAC,CAAC;IACxF,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,OAAO,CAAC;QACvC,MAAM,GAAG,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAE7D,2BAA2B;QAC3B,IAAI,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,KAAK,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,cAAc,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,gBAAgB,CAAC,EAAE,CAAC;YACjH,OAAO,CAAC,KAAK,CAAC,kDAAkD,SAAS,IAAI,CAAC,CAAC;YAC/E,OAAO;gBACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,kEAAkE,EAAE,CAAC;gBACrG,OAAO,EAAE,IAAI;aACd,CAAC;QACJ,CAAC;QAED,iBAAiB;QACjB,IAAI,GAAG,CAAC,QAAQ,CAAC,cAAc,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,GAAG,CAAC,QAAQ,CAAC,cAAc,CAAC,EAAE,CAAC;YAC3H,OAAO,CAAC,KAAK,CAAC,wCAAwC,GAAG,YAAY,SAAS,IAAI,CAAC,CAAC;YACpF,OAAO;gBACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,4DAA4D,EAAE,CAAC;gBAC/F,OAAO,EAAE,IAAI;aACd,CAAC;QACJ,CAAC;QAED,0BAA0B;QAC1B,OAAO,CAAC,KAAK,CAAC,sCAAsC,KAAK,UAAU,GAAG,YAAY,SAAS,IAAI,CAAC,CAAC;QACjG,OAAO;YACL,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,4BAA4B,GAAG,EAAE,EAAE,CAAC;YACpE,OAAO,EAAE,IAAI;SACd,CAAC;IACJ,CAAC;IAED,wCAAwC;IACxC,
|
|
1
|
+
{"version":3,"file":"image-generate.js","sourceRoot":"","sources":["../../src/tools/image-generate.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Tests for resume-tunnel.sh scope-based spawn path (Task 602).
|
|
3
|
+
# Stubs systemd-run, systemctl, and cloudflared binaries via a fake $PATH entry.
|
|
4
|
+
set -uo pipefail
|
|
5
|
+
|
|
6
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
7
|
+
RESUME="$SCRIPT_DIR/../resume-tunnel.sh"
|
|
8
|
+
|
|
9
|
+
PASS=0
|
|
10
|
+
FAIL=0
|
|
11
|
+
|
|
12
|
+
run_case() {
|
|
13
|
+
local name="$1"; shift
|
|
14
|
+
echo ""
|
|
15
|
+
echo "=== CASE: $name ==="
|
|
16
|
+
if "$@"; then
|
|
17
|
+
PASS=$((PASS + 1))
|
|
18
|
+
echo "PASS: $name"
|
|
19
|
+
else
|
|
20
|
+
FAIL=$((FAIL + 1))
|
|
21
|
+
echo "FAIL: $name"
|
|
22
|
+
fi
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
# Build a minimal install tree with stubs and state file.
|
|
26
|
+
# Args: root config_dir scope_active connect_success
|
|
27
|
+
setup() {
|
|
28
|
+
local root="$1"
|
|
29
|
+
local config_dir="${2:-.maxy-code}"
|
|
30
|
+
local scope_active="${3:-inactive}"
|
|
31
|
+
local connect_success="${4:-yes}"
|
|
32
|
+
|
|
33
|
+
local home_dir="$root/home"
|
|
34
|
+
local platform_root="$root/platform_root"
|
|
35
|
+
local bin_dir="$root/bin"
|
|
36
|
+
mkdir -p "$home_dir/$config_dir/cloudflared" \
|
|
37
|
+
"$home_dir/$config_dir/logs" \
|
|
38
|
+
"$platform_root/config" \
|
|
39
|
+
"$bin_dir"
|
|
40
|
+
|
|
41
|
+
cat > "$platform_root/config/brand.json" <<JSON
|
|
42
|
+
{"configDir": "$config_dir"}
|
|
43
|
+
JSON
|
|
44
|
+
|
|
45
|
+
cat > "$home_dir/$config_dir/cloudflared/tunnel.state" <<JSON
|
|
46
|
+
{
|
|
47
|
+
"tunnelId": "test-tunnel-id",
|
|
48
|
+
"domain": "test.example.com",
|
|
49
|
+
"configPath": "$home_dir/$config_dir/cloudflared/config.yml",
|
|
50
|
+
"pid": 99999
|
|
51
|
+
}
|
|
52
|
+
JSON
|
|
53
|
+
|
|
54
|
+
touch "$home_dir/$config_dir/cloudflared/cert.pem"
|
|
55
|
+
touch "$home_dir/$config_dir/cloudflared/config.yml"
|
|
56
|
+
|
|
57
|
+
local log_file="$home_dir/$config_dir/logs/cloudflared.log"
|
|
58
|
+
|
|
59
|
+
# Stub cloudflared: does nothing.
|
|
60
|
+
cat > "$bin_dir/cloudflared" <<'SH'
|
|
61
|
+
#!/bin/bash
|
|
62
|
+
sleep 0.1
|
|
63
|
+
SH
|
|
64
|
+
chmod +x "$bin_dir/cloudflared"
|
|
65
|
+
|
|
66
|
+
# Stub systemctl: is-active returns based on scope_active.
|
|
67
|
+
cat > "$bin_dir/systemctl" <<SH
|
|
68
|
+
#!/bin/bash
|
|
69
|
+
if [[ "\$*" == *"is-active"* ]]; then
|
|
70
|
+
if [[ "${scope_active}" == "active" ]]; then
|
|
71
|
+
echo "active"; exit 0
|
|
72
|
+
else
|
|
73
|
+
echo "inactive"; exit 3
|
|
74
|
+
fi
|
|
75
|
+
fi
|
|
76
|
+
if [[ "\$*" == *"--property=MainPID"* ]]; then
|
|
77
|
+
echo "12345"
|
|
78
|
+
fi
|
|
79
|
+
exit 0
|
|
80
|
+
SH
|
|
81
|
+
chmod +x "$bin_dir/systemctl"
|
|
82
|
+
|
|
83
|
+
# Stub systemd-run: logs its --unit= and --slice= args, optionally writes connection line.
|
|
84
|
+
cat > "$bin_dir/systemd-run" <<SH
|
|
85
|
+
#!/bin/bash
|
|
86
|
+
for arg in "\$@"; do
|
|
87
|
+
case "\$arg" in
|
|
88
|
+
--unit=*) echo "[stub] unit=\${arg#--unit=}" ;;
|
|
89
|
+
--slice=*) echo "[stub] slice=\${arg#--slice=}" ;;
|
|
90
|
+
esac
|
|
91
|
+
done
|
|
92
|
+
if [[ "${connect_success}" == "yes" ]]; then
|
|
93
|
+
echo "2026-06-02T00:00:00Z INF Registered tunnel connection connIndex=0" >> "${log_file}"
|
|
94
|
+
fi
|
|
95
|
+
exit 0
|
|
96
|
+
SH
|
|
97
|
+
chmod +x "$bin_dir/systemd-run"
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
cleanup() { rm -rf "$1"; }
|
|
101
|
+
|
|
102
|
+
# CASE 1: skip branch — scope is active
|
|
103
|
+
case_skip_when_scope_active() {
|
|
104
|
+
local root
|
|
105
|
+
root=$(mktemp -d /tmp/resume-tunnel-test-XXXXXX)
|
|
106
|
+
setup "$root" ".maxy-code" "active" "yes"
|
|
107
|
+
|
|
108
|
+
local log="$root/home/.maxy-code/logs/cloudflared.log"
|
|
109
|
+
|
|
110
|
+
HOME="$root/home" \
|
|
111
|
+
PATH="$root/bin:$PATH" \
|
|
112
|
+
MAXY_PLATFORM_ROOT="$root/platform_root" \
|
|
113
|
+
bash "$RESUME"
|
|
114
|
+
local rc=$?
|
|
115
|
+
|
|
116
|
+
local log_content
|
|
117
|
+
log_content=$(cat "$log" 2>/dev/null || echo "")
|
|
118
|
+
cleanup "$root"
|
|
119
|
+
|
|
120
|
+
if [[ $rc -ne 0 ]]; then
|
|
121
|
+
echo " expected exit 0, got $rc"; return 1
|
|
122
|
+
fi
|
|
123
|
+
if ! echo "$log_content" | grep -q "already running.*scope.*cloudflared-maxy-code"; then
|
|
124
|
+
echo " skip branch missing expected log line"
|
|
125
|
+
echo "$log_content"
|
|
126
|
+
return 1
|
|
127
|
+
fi
|
|
128
|
+
return 0
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
# CASE 2: spawn branch — scope inactive, connection succeeds
|
|
132
|
+
case_spawn_succeeds() {
|
|
133
|
+
local root
|
|
134
|
+
root=$(mktemp -d /tmp/resume-tunnel-test-XXXXXX)
|
|
135
|
+
setup "$root" ".maxy-code" "inactive" "yes"
|
|
136
|
+
|
|
137
|
+
local log="$root/home/.maxy-code/logs/cloudflared.log"
|
|
138
|
+
|
|
139
|
+
HOME="$root/home" \
|
|
140
|
+
PATH="$root/bin:$PATH" \
|
|
141
|
+
MAXY_PLATFORM_ROOT="$root/platform_root" \
|
|
142
|
+
bash "$RESUME"
|
|
143
|
+
local rc=$?
|
|
144
|
+
|
|
145
|
+
local log_content
|
|
146
|
+
log_content=$(cat "$log" 2>/dev/null || echo "")
|
|
147
|
+
cleanup "$root"
|
|
148
|
+
|
|
149
|
+
if [[ $rc -ne 0 ]]; then
|
|
150
|
+
echo " expected exit 0, got $rc"; return 1
|
|
151
|
+
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
|
|
154
|
+
fi
|
|
155
|
+
if ! echo "$log_content" | grep -q "tunnel verified"; then
|
|
156
|
+
echo " missing tunnel verified log line"; echo "$log_content"; return 1
|
|
157
|
+
fi
|
|
158
|
+
return 0
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
# CASE 3: spawn branch — no edge connection within timeout
|
|
162
|
+
case_spawn_no_connection() {
|
|
163
|
+
local root
|
|
164
|
+
root=$(mktemp -d /tmp/resume-tunnel-test-XXXXXX)
|
|
165
|
+
setup "$root" ".maxy-code" "inactive" "no"
|
|
166
|
+
|
|
167
|
+
local log="$root/home/.maxy-code/logs/cloudflared.log"
|
|
168
|
+
|
|
169
|
+
HOME="$root/home" \
|
|
170
|
+
PATH="$root/bin:$PATH" \
|
|
171
|
+
MAXY_PLATFORM_ROOT="$root/platform_root" \
|
|
172
|
+
RESUME_TUNNEL_VERIFY_DEADLINE=2 \
|
|
173
|
+
bash "$RESUME"
|
|
174
|
+
local rc=$?
|
|
175
|
+
|
|
176
|
+
local log_content
|
|
177
|
+
log_content=$(cat "$log" 2>/dev/null || echo "")
|
|
178
|
+
cleanup "$root"
|
|
179
|
+
|
|
180
|
+
if [[ $rc -ne 0 ]]; then
|
|
181
|
+
echo " expected exit 0 even on failure, got $rc"; return 1
|
|
182
|
+
fi
|
|
183
|
+
if ! echo "$log_content" | grep -q "ERROR.*no edge connection"; then
|
|
184
|
+
echo " missing ERROR log line on connection failure"; echo "$log_content"; return 1
|
|
185
|
+
fi
|
|
186
|
+
return 0
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
# CASE 4: scope unit name derived correctly from configDir (.real-agent → cloudflared-real-agent.scope)
|
|
190
|
+
case_scope_unit_name_from_config_dir() {
|
|
191
|
+
local root
|
|
192
|
+
root=$(mktemp -d /tmp/resume-tunnel-test-XXXXXX)
|
|
193
|
+
setup "$root" ".real-agent" "inactive" "yes"
|
|
194
|
+
|
|
195
|
+
local log="$root/home/.real-agent/logs/cloudflared.log"
|
|
196
|
+
|
|
197
|
+
HOME="$root/home" \
|
|
198
|
+
PATH="$root/bin:$PATH" \
|
|
199
|
+
MAXY_PLATFORM_ROOT="$root/platform_root" \
|
|
200
|
+
bash "$RESUME"
|
|
201
|
+
local rc=$?
|
|
202
|
+
|
|
203
|
+
local log_content
|
|
204
|
+
log_content=$(cat "$log" 2>/dev/null || echo "")
|
|
205
|
+
cleanup "$root"
|
|
206
|
+
|
|
207
|
+
if [[ $rc -ne 0 ]]; then
|
|
208
|
+
echo " expected exit 0, got $rc"; return 1
|
|
209
|
+
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"
|
|
212
|
+
echo "$log_content"; return 1
|
|
213
|
+
fi
|
|
214
|
+
return 0
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
# CASE 5: --slice=cloudflared flag passed to systemd-run
|
|
218
|
+
case_slice_flag_present() {
|
|
219
|
+
local root
|
|
220
|
+
root=$(mktemp -d /tmp/resume-tunnel-test-XXXXXX)
|
|
221
|
+
setup "$root" ".maxy-code" "inactive" "yes"
|
|
222
|
+
|
|
223
|
+
local log="$root/home/.maxy-code/logs/cloudflared.log"
|
|
224
|
+
|
|
225
|
+
HOME="$root/home" \
|
|
226
|
+
PATH="$root/bin:$PATH" \
|
|
227
|
+
MAXY_PLATFORM_ROOT="$root/platform_root" \
|
|
228
|
+
bash "$RESUME"
|
|
229
|
+
|
|
230
|
+
local log_content
|
|
231
|
+
log_content=$(cat "$log" 2>/dev/null || echo "")
|
|
232
|
+
cleanup "$root"
|
|
233
|
+
|
|
234
|
+
# The systemd-run stub echoes "[stub] slice=cloudflared" to stdout,
|
|
235
|
+
# which is redirected to LOG_FILE by the script's >> redirect.
|
|
236
|
+
if ! echo "$log_content" | grep -q "\[stub\] slice=cloudflared"; then
|
|
237
|
+
echo " --slice=cloudflared not seen by systemd-run stub"
|
|
238
|
+
echo "$log_content"; return 1
|
|
239
|
+
fi
|
|
240
|
+
return 0
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
run_case "skip when scope active" case_skip_when_scope_active
|
|
244
|
+
run_case "spawn succeeds and logs verified" case_spawn_succeeds
|
|
245
|
+
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
|
|
247
|
+
run_case "slice flag passed to systemd-run" case_slice_flag_present
|
|
248
|
+
|
|
249
|
+
echo ""
|
|
250
|
+
echo "Results: $PASS passed, $FAIL failed"
|
|
251
|
+
[[ $FAIL -eq 0 ]]
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
#!/bin/bash
|
|
2
|
-
# resume-tunnel.sh — Resume the Cloudflare tunnel on
|
|
2
|
+
# resume-tunnel.sh — Resume the Cloudflare tunnel on brand-service start.
|
|
3
3
|
#
|
|
4
|
-
#
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
# All failures exit 0 — this script must never prevent maxy.service from starting.
|
|
4
|
+
# Spawns cloudflared into its own transient systemd scope (cloudflared-<brand>.scope)
|
|
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.
|
|
8
7
|
#
|
|
9
|
-
#
|
|
8
|
+
# Called as ExecStartPre in maxy.service. All failures exit 0 — this script must
|
|
9
|
+
# never prevent maxy.service from starting.
|
|
10
|
+
#
|
|
11
|
+
# Dependencies: jq, cloudflared, systemd-run, systemctl
|
|
10
12
|
|
|
11
13
|
set -uo pipefail
|
|
12
14
|
|
|
13
|
-
# Derive configDir from brand.json
|
|
14
|
-
# Fall back to .maxy if brand.json is unreadable.
|
|
15
|
+
# Derive configDir from brand.json. Fall back to .maxy if unreadable.
|
|
15
16
|
CONFIG_DIR=".maxy"
|
|
16
17
|
if [ -n "${MAXY_PLATFORM_ROOT:-}" ] && [ -f "$MAXY_PLATFORM_ROOT/config/brand.json" ]; then
|
|
17
18
|
BRAND_CONFIG_DIR=$(jq -r '.configDir // empty' "$MAXY_PLATFORM_ROOT/config/brand.json" 2>/dev/null || true)
|
|
@@ -20,9 +21,7 @@ if [ -n "${MAXY_PLATFORM_ROOT:-}" ] && [ -f "$MAXY_PLATFORM_ROOT/config/brand.js
|
|
|
20
21
|
fi
|
|
21
22
|
fi
|
|
22
23
|
|
|
23
|
-
# State file under ~/{configDir}/cloudflared/ — each brand has its own tunnel state.
|
|
24
24
|
STATE_FILE="$HOME/$CONFIG_DIR/cloudflared/tunnel.state"
|
|
25
|
-
|
|
26
25
|
LOG_DIR="$HOME/$CONFIG_DIR/logs"
|
|
27
26
|
LOG_FILE="$LOG_DIR/cloudflared.log"
|
|
28
27
|
|
|
@@ -32,13 +31,10 @@ log() {
|
|
|
32
31
|
}
|
|
33
32
|
|
|
34
33
|
# No state file — no tunnel configured. Normal boot.
|
|
35
|
-
|
|
36
|
-
exit 0
|
|
37
|
-
fi
|
|
34
|
+
[ -f "$STATE_FILE" ] || exit 0
|
|
38
35
|
|
|
39
36
|
log "reading state from $STATE_FILE (configDir=$CONFIG_DIR)"
|
|
40
37
|
|
|
41
|
-
# Parse state file
|
|
42
38
|
if ! STATE=$(jq -r '.' "$STATE_FILE" 2>/dev/null); then
|
|
43
39
|
log "WARNING: tunnel.state is not valid JSON — skipping resume"
|
|
44
40
|
exit 0
|
|
@@ -47,64 +43,106 @@ fi
|
|
|
47
43
|
TUNNEL_ID=$(echo "$STATE" | jq -r '.tunnelId // empty')
|
|
48
44
|
DOMAIN=$(echo "$STATE" | jq -r '.domain // empty')
|
|
49
45
|
CONFIG_PATH=$(echo "$STATE" | jq -r '.configPath // empty')
|
|
50
|
-
OLD_PID=$(echo "$STATE" | jq -r '.pid // empty')
|
|
51
46
|
|
|
52
47
|
if [ -z "$TUNNEL_ID" ]; then
|
|
53
48
|
log "WARNING: tunnel.state has no tunnelId — skipping resume"
|
|
54
49
|
exit 0
|
|
55
50
|
fi
|
|
56
51
|
|
|
57
|
-
#
|
|
58
|
-
|
|
52
|
+
# Derive scope unit name: strip leading dot from CONFIG_DIR.
|
|
53
|
+
# .maxy-code → cloudflared-maxy-code.scope
|
|
54
|
+
BRAND="${CONFIG_DIR#.}"
|
|
55
|
+
SCOPE_UNIT="cloudflared-${BRAND}.scope"
|
|
59
56
|
|
|
60
|
-
#
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
log "Tunnel already running (PID $OLD_PID) — skipping resume"
|
|
67
|
-
exit 0
|
|
68
|
-
fi
|
|
69
|
-
# PID alive but not cloudflared — stale PID from before reboot
|
|
70
|
-
log "PID $OLD_PID is alive but is '$PROC_NAME', not cloudflared — will start new daemon"
|
|
71
|
-
fi
|
|
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"
|
|
62
|
+
exit 0
|
|
72
63
|
fi
|
|
73
64
|
|
|
74
|
-
|
|
65
|
+
CERT_PATH="$HOME/$CONFIG_DIR/cloudflared/cert.pem"
|
|
75
66
|
CLOUDFLARED_BIN=$(command -v cloudflared 2>/dev/null || true)
|
|
67
|
+
|
|
76
68
|
if [ -z "$CLOUDFLARED_BIN" ]; then
|
|
77
69
|
log "ERROR: cloudflared binary not found — cannot resume tunnel"
|
|
78
70
|
exit 0
|
|
79
71
|
fi
|
|
80
72
|
|
|
81
|
-
|
|
82
|
-
log "Resuming tunnel $TUNNEL_ID for $DOMAIN..."
|
|
83
|
-
mkdir -p "$LOG_DIR"
|
|
84
|
-
|
|
85
|
-
if [ -n "$CONFIG_PATH" ] && [ -f "$CONFIG_PATH" ] && [ -f "$CERT_PATH" ]; then
|
|
86
|
-
log "Starting with config: $CONFIG_PATH, cert: $CERT_PATH"
|
|
87
|
-
nohup "$CLOUDFLARED_BIN" --origincert "$CERT_PATH" --config "$CONFIG_PATH" tunnel run >> "$LOG_FILE" 2>&1 &
|
|
88
|
-
else
|
|
73
|
+
if [ -z "$CONFIG_PATH" ] || [ ! -f "$CONFIG_PATH" ] || [ ! -f "$CERT_PATH" ]; then
|
|
89
74
|
log "ERROR: missing configPath or cert — cannot resume tunnel"
|
|
90
75
|
exit 0
|
|
91
76
|
fi
|
|
92
|
-
NEW_PID=$!
|
|
93
77
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
78
|
+
log "Resuming tunnel $TUNNEL_ID for $DOMAIN..."
|
|
79
|
+
|
|
80
|
+
# Capture log byte offset before spawning so the verification poll only
|
|
81
|
+
# matches lines written by THIS invocation, not lines from a prior run.
|
|
82
|
+
mkdir -p "$LOG_DIR"
|
|
83
|
+
LOG_OFFSET=$(wc -c < "$LOG_FILE" 2>/dev/null | awk '{print $1+0}' || echo 0)
|
|
84
|
+
|
|
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
|
+
systemd-run \
|
|
92
|
+
--user \
|
|
93
|
+
--scope \
|
|
94
|
+
--quiet \
|
|
95
|
+
--unit="$SCOPE_UNIT" \
|
|
96
|
+
--slice=cloudflared \
|
|
97
|
+
-p TimeoutStopSec=15 \
|
|
98
|
+
-- \
|
|
99
|
+
"$CLOUDFLARED_BIN" --origincert "$CERT_PATH" --config "$CONFIG_PATH" tunnel run \
|
|
100
|
+
>> "$LOG_FILE" 2>&1 &
|
|
101
|
+
|
|
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"
|
|
105
|
+
|
|
106
|
+
# Post-spawn connection verification gate.
|
|
107
|
+
# Polls only new log content (from LOG_OFFSET onwards) to avoid false positives
|
|
108
|
+
# from "Registered tunnel connection" lines written by a prior run.
|
|
109
|
+
# Overridable via RESUME_TUNNEL_VERIFY_DEADLINE for testing.
|
|
110
|
+
# On failure: log explicit ERROR and exit 0 — must not block brand-service start.
|
|
111
|
+
VERIFY_DEADLINE="${RESUME_TUNNEL_VERIFY_DEADLINE:-20}"
|
|
112
|
+
VERIFY_START=$(date +%s)
|
|
113
|
+
VERIFIED=0
|
|
114
|
+
while true; do
|
|
115
|
+
if tail -c "+$((LOG_OFFSET + 1))" "$LOG_FILE" 2>/dev/null | grep -q "Registered tunnel connection"; then
|
|
116
|
+
VERIFIED=1
|
|
117
|
+
break
|
|
118
|
+
fi
|
|
119
|
+
NOW=$(date +%s)
|
|
120
|
+
ELAPSED=$(( NOW - VERIFY_START ))
|
|
121
|
+
if [ "$ELAPSED" -ge "$VERIFY_DEADLINE" ]; then
|
|
122
|
+
break
|
|
123
|
+
fi
|
|
124
|
+
sleep 1
|
|
125
|
+
done
|
|
126
|
+
|
|
127
|
+
if [ "$VERIFIED" -eq 1 ]; then
|
|
128
|
+
ELAPSED=$(( $(date +%s) - VERIFY_START ))
|
|
129
|
+
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 ))"
|
|
131
|
+
else
|
|
132
|
+
log "ERROR: tunnel scope started but no edge connection within ${VERIFY_DEADLINE}s — scope=$SCOPE_UNIT"
|
|
97
133
|
fi
|
|
98
134
|
|
|
99
|
-
# Update state file
|
|
100
|
-
|
|
101
|
-
|
|
135
|
+
# Update state file: keep pid/startedAt for observability, add scopeUnit as control signal.
|
|
136
|
+
NEW_STATE=$(echo "$STATE" | jq \
|
|
137
|
+
--arg unit "$SCOPE_UNIT" \
|
|
138
|
+
--argjson ts "$(date +%s)000" \
|
|
139
|
+
'.scopeUnit = $unit | .startedAt = $ts' 2>/dev/null || echo "")
|
|
102
140
|
|
|
103
|
-
if echo "$
|
|
104
|
-
|
|
141
|
+
if [ -n "$NEW_STATE" ] && echo "$NEW_STATE" > "$STATE_FILE.tmp" 2>/dev/null && mv "$STATE_FILE.tmp" "$STATE_FILE" 2>/dev/null; then
|
|
142
|
+
:
|
|
105
143
|
else
|
|
106
144
|
rm -f "$STATE_FILE.tmp" 2>/dev/null
|
|
107
|
-
log "WARNING: tunnel started
|
|
145
|
+
log "WARNING: tunnel started but failed to update state file"
|
|
108
146
|
fi
|
|
109
147
|
|
|
110
148
|
exit 0
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: content-producer
|
|
3
3
|
description: "Author professional business documents (proposal, report, brief, memo, plan, case study, brochure), render PDFs, generate images, and publish static websites. Delegate when the operator asks for a written document, a PDF render, a generated image, or to publish a prepared website."
|
|
4
4
|
summary: "Produces visual output from your graph: generates images, renders pages to PDF, and hosts static websites you upload as a zip."
|
|
5
|
-
model: claude-
|
|
5
|
+
model: claude-opus-4-8
|
|
6
6
|
tools: Bash, Read, Edit, Write, MultiEdit, Skill, mcp__plugin_memory_memory__memory-search, mcp__plugin_replicate_replicate__image-generate, mcp__plugin_browser_browser__browser-render, mcp__plugin_browser_browser__browser-navigate, mcp__plugin_browser_browser__browser-evaluate, mcp__plugin_browser_browser__browser-resize, mcp__plugin_browser_browser__browser-screenshot, mcp__plugin_browser_browser__browser-pdf-save, mcp__plugin_admin_admin__plugin-read, mcp__plugin_admin_admin__public-hostname, mcp__plugin_admin_admin__publish-site
|
|
7
7
|
---
|
|
8
8
|
|