mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-28 12:04:41 +01:00
fix(tailscale-https-broker): version floor + install docs (PAP-17079)
Replace the pinned Tailscale minor allowlist with a numeric minimum-version comparison (floor 1.80) so current releases like 1.102.2 pass --doctor while 1.78 and malformed output are still rejected; add boundary + malformed unit coverage. Correct broker lifecycle commands to the installed unit name paperclip-tailscale-https-broker and document copying the built package to /opt/paperclip/packages/tailscale-https-broker to match ExecStart and doctor. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
90cf40bf0f
commit
78b5ac1ce5
3 files changed
+67
-12
No files matched your search
@@ -87,10 +87,18 @@ Tailscale-operator service account distinct from the Paperclip app account.
|
||||
The broker refuses to start if the registry path's parent is group/other
|
||||
writable.
|
||||
|
||||
5. **Build and install the packaged systemd unit.**
|
||||
5. **Build, install the package under `/opt/paperclip`, and install the
|
||||
packaged systemd unit.** The unit's `ExecStart` (and the doctor command
|
||||
below) run the build output from
|
||||
`/opt/paperclip/packages/tailscale-https-broker/dist`, so copy it there
|
||||
explicitly. The output is self-contained (Node builtins only; no
|
||||
`node_modules` needed).
|
||||
|
||||
```sh
|
||||
pnpm --filter @paperclipai/tailscale-https-broker build
|
||||
sudo install -d -m 0755 /opt/paperclip/packages/tailscale-https-broker
|
||||
sudo cp -r packages/tailscale-https-broker/dist \
|
||||
/opt/paperclip/packages/tailscale-https-broker/
|
||||
sudo install -D -m 0644 \
|
||||
packages/tailscale-https-broker/deploy/paperclip-tailscale-https-broker.service \
|
||||
/etc/systemd/system/paperclip-tailscale-https-broker.service
|
||||
@@ -154,12 +162,14 @@ Tailscale-operator service account distinct from the Paperclip app account.
|
||||
node identity. Exit 0 = ready. It never mutates Serve state.
|
||||
|
||||
7. **Enable.** `sudo systemctl daemon-reload && sudo systemctl enable --now
|
||||
paperclip-tailscale-broker`. Confirm the socket is `0660
|
||||
paperclip-tailscale-https-broker`. Confirm the socket is `0660
|
||||
paperclip-tsbroker:paperclip-tsbroker-sock`.
|
||||
|
||||
## Upgrade
|
||||
|
||||
Deploy new package output, `systemctl restart paperclip-tailscale-broker`. On
|
||||
Deploy new package output to
|
||||
`/opt/paperclip/packages/tailscale-https-broker/dist`, then
|
||||
`sudo systemctl restart paperclip-tailscale-https-broker`. On
|
||||
restart the broker re-reads its root-owned registry and adopts only exact-lease
|
||||
matches; a changed `BROKER_NODE_IDENTITY` (host reimage / boot-id change) forces
|
||||
quarantine and operator reconciliation rather than silently re-adopting.
|
||||
@@ -173,7 +183,7 @@ resets Serve or changes the primary route.
|
||||
`expose`). Existing previews drain on runtime stop.
|
||||
2. Drain owned listeners: stop each managed runtime so Paperclip issues `remove`
|
||||
for its own leases (proven by handle).
|
||||
3. `sudo systemctl disable --now paperclip-tailscale-broker`.
|
||||
3. `sudo systemctl disable --now paperclip-tailscale-https-broker`.
|
||||
4. Optional cleanup: remove the state dirs and `sudo tailscale set --operator=`
|
||||
to drop the operator grant. Do **not** run `tailscale serve reset` — remove
|
||||
only the specific per-port Serve entries if any remain.
|
||||
@@ -192,7 +202,7 @@ redacted.
|
||||
## Tests
|
||||
|
||||
```sh
|
||||
pnpm --filter @paperclipai/tailscale-https-broker test # 60 tests
|
||||
pnpm --filter @paperclipai/tailscale-https-broker test # 65 tests
|
||||
pnpm --filter @paperclipai/tailscale-https-broker typecheck
|
||||
pnpm --filter @paperclipai/tailscale-https-broker build
|
||||
```
|
||||
@@ -0,0 +1,39 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { isSupportedTailscaleVersion, parseTailscaleVersion } from "./tailscale-cli.js";
|
||||
|
||||
describe("parseTailscaleVersion", () => {
|
||||
it("parses the numeric major.minor prefix of CLI output", () => {
|
||||
expect(parseTailscaleVersion("1.102.2\n tailscale commit: abc\n")).toEqual({ major: 1, minor: 102 });
|
||||
expect(parseTailscaleVersion(" 1.80.3")).toEqual({ major: 1, minor: 80 });
|
||||
});
|
||||
|
||||
it("returns null for malformed output", () => {
|
||||
expect(parseTailscaleVersion("")).toBeNull();
|
||||
expect(parseTailscaleVersion("garbage")).toBeNull();
|
||||
expect(parseTailscaleVersion("v1.80.1")).toBeNull();
|
||||
expect(parseTailscaleVersion("1")).toBeNull();
|
||||
expect(parseTailscaleVersion("tailscale 1.80")).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("isSupportedTailscaleVersion", () => {
|
||||
it("accepts the minimum version and anything newer", () => {
|
||||
expect(isSupportedTailscaleVersion("1.80.0")).toBe(true);
|
||||
expect(isSupportedTailscaleVersion("1.84.1")).toBe(true);
|
||||
// Numeric, not lexicographic: "1.102" < "1.80" as strings.
|
||||
expect(isSupportedTailscaleVersion("1.102.2\n tailscale commit: abc\n")).toBe(true);
|
||||
expect(isSupportedTailscaleVersion("2.0.0")).toBe(true);
|
||||
});
|
||||
|
||||
it("rejects versions below the minimum", () => {
|
||||
expect(isSupportedTailscaleVersion("1.78")).toBe(false);
|
||||
expect(isSupportedTailscaleVersion("1.79.9")).toBe(false);
|
||||
expect(isSupportedTailscaleVersion("0.99.0")).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects malformed version output", () => {
|
||||
expect(isSupportedTailscaleVersion("")).toBe(false);
|
||||
expect(isSupportedTailscaleVersion("garbage")).toBe(false);
|
||||
expect(isSupportedTailscaleVersion("v1.102.2")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -10,8 +10,12 @@ import type { CliResult } from "./broker-core.js";
|
||||
const MAX_OUTPUT_BYTES = 256 * 1024;
|
||||
const DEFAULT_TIMEOUT_MS = 10_000;
|
||||
|
||||
/** Tailscale CLI versions this broker has been validated against (pinned). */
|
||||
export const SUPPORTED_TAILSCALE_VERSIONS = new Set<string>(["1.80", "1.82", "1.84"]);
|
||||
/**
|
||||
* Minimum Tailscale CLI version the broker accepts. Serve subcommand syntax and
|
||||
* JSON status output are stable from this release onward, so newer releases are
|
||||
* accepted by numeric comparison rather than a pinned allowlist.
|
||||
*/
|
||||
export const MIN_TAILSCALE_VERSION = { major: 1, minor: 80 } as const;
|
||||
|
||||
export function createTailscaleRunner(options: { timeoutMs?: number } = {}) {
|
||||
const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
||||
@@ -39,13 +43,15 @@ export function createTailscaleRunner(options: { timeoutMs?: number } = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
/** Parse the `major.minor` prefix of `tailscale version` output. */
|
||||
export function parseTailscaleMinor(versionOutput: string): string | null {
|
||||
/** Parse the numeric `major.minor` prefix of `tailscale version` output. */
|
||||
export function parseTailscaleVersion(versionOutput: string): { major: number; minor: number } | null {
|
||||
const match = /^\s*(\d+)\.(\d+)/.exec(versionOutput);
|
||||
return match ? `${match[1]}.${match[2]}` : null;
|
||||
return match ? { major: Number(match[1]), minor: Number(match[2]) } : null;
|
||||
}
|
||||
|
||||
export function isSupportedTailscaleVersion(versionOutput: string): boolean {
|
||||
const minor = parseTailscaleMinor(versionOutput);
|
||||
return minor !== null && SUPPORTED_TAILSCALE_VERSIONS.has(minor);
|
||||
const version = parseTailscaleVersion(versionOutput);
|
||||
if (version === null) return false;
|
||||
if (version.major !== MIN_TAILSCALE_VERSION.major) return version.major > MIN_TAILSCALE_VERSION.major;
|
||||
return version.minor >= MIN_TAILSCALE_VERSION.minor;
|
||||
}
|
||||
Reference in new issue
Block a user