@astrofoundry/pi-astro 0.34.1 → 0.35.0
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/README.md +1 -1
- package/agents/inference.md +4 -5
- package/extensions/astro-discord/tools-report.test.ts +77 -12
- package/extensions/astro-discord/tools-report.ts +108 -37
- package/extensions/specialist-gate/approvals.test.ts +1 -1
- package/extensions/specialist-gate/index.ts +1 -1
- package/package.json +1 -1
- package/skills/edge/SKILL.md +1 -1
- package/skills/inference/SKILL.md +3 -27
- package/skills/proxmox/SKILL.md +2 -2
- package/skills/security/SKILL.md +2 -2
- package/specialists/AGENTS.md +5 -5
- package/specialists/README.md +19 -13
- package/specialists/edge/run.ts +3 -3
- package/specialists/entry/remote/spc-arcane-install.sh +2 -2
- package/specialists/entry/remote/spc-arcane-op.py +3 -0
- package/specialists/entry/specialist-install.sh +266 -8
- package/specialists/entry/specialist-install.test.ts +50 -1
- package/specialists/inference/run.ts +12 -94
- package/specialists/lib/arcane-host.test.ts +24 -1
- package/specialists/lib/arcane-host.ts +6 -2
- package/specialists/lib/deployment.ts +1 -1
- package/specialists/provision/provision.sh +2 -2
- package/specialists/wrappers.test.ts +6 -16
- package/specialists/entry/remote/spc-inference-hermes-entry.sh +0 -27
- package/specialists/entry/remote/spc-inference-pulsar-entry.sh +0 -14
package/README.md
CHANGED
|
@@ -80,7 +80,7 @@ pi # launch; confirm [Extensions] lists astro-subagents, grimoire
|
|
|
80
80
|
|
|
81
81
|
**Bundled subagents** (call them as `astro.<name>`):
|
|
82
82
|
- General-purpose: `code-reviewer`, `google-tech-lead`, `spec-writer`, `tester-api`, `tester-ui`, `ui-architect`, `ui-design-system`, `ui-frontend-developer`
|
|
83
|
-
- Specialists (one homelab area each, need the host setup in [specialists/README.md](specialists/README.md)): `arcane` (Arcane container platform, GitOps first; sanitized host checks, Mealie and Paperless dumps, and the documented manager upgrade with an Upgrade button on the daily report), `identity` (Zitadel, Pomerium routes through the homelab repository, certificates), `network` (UniFi reads and firewall policies, FreeRADIUS, Tailscale), `dns` (Technitium resolvers, Cloudflare zones), `edge` (Frontdoor VPS over IAP, nginx SNI allowlist through the homelab repository, public route checks), `security` (Wazuh, CrowdSec, log archive, read-only), `backup` (vzdump, Restic layers, GCS offsite, restore tests), `proxmox` (guest lifecycle, snapshots, resources; no create or destroy), `inference` (llama.cpp on Nexus,
|
|
83
|
+
- Specialists (one homelab area each, need the host setup in [specialists/README.md](specialists/README.md)): `arcane` (Arcane container platform, GitOps first; sanitized host checks, Mealie and Paperless dumps, and the documented manager upgrade with an Upgrade button on the daily report), `identity` (Zitadel, Pomerium routes through the homelab repository, certificates), `network` (UniFi reads and firewall policies, FreeRADIUS, Tailscale), `dns` (Technitium resolvers, Cloudflare zones), `edge` (Frontdoor VPS over IAP, nginx SNI allowlist through the homelab repository, public route checks), `security` (Wazuh, CrowdSec, log archive, read-only), `backup` (vzdump, Restic layers, GCS offsite, restore tests), `proxmox` (guest lifecycle, snapshots, resources; no create or destroy), `inference` (llama.cpp on Nexus, Europa health), `media` (trendarr: trending titles, Radarr and Sonarr adds, SABnzbd pause)
|
|
84
84
|
- Release notes: `release-reviewer` (no tools; judges whether an update could break how Cortex uses a tool, for the Cortex Tools Report)
|
|
85
85
|
- Flourish pipeline: `flourish-input-vision` (image to spec), `flourish-input-narrative` (text to spec), `flourish-input-codebase` (HTML to spec), `flourish-architect` (spec to plan), `flourish-template-developer` (plan to SDK template, needs `@flourish/sdk`), `flourish-example-builder` (template to realistic example), `flourish-preview-runner` (multi-breakpoint preview + screenshots, needs `@flourish/sdk`), `flourish-reviewer` (audit)
|
|
86
86
|
|
package/agents/inference.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: inference
|
|
3
|
-
description: Operates the AI serving hosts through the inference tool, covering llama.cpp embeddings and reranking on Nexus
|
|
3
|
+
description: Operates the AI serving hosts through the inference tool, covering llama.cpp embeddings and reranking on Nexus and the Europa health check. Use for inference health, restarts, and logs.
|
|
4
4
|
tools: read, inference, subagent
|
|
5
5
|
skills: inference
|
|
6
6
|
systemPromptMode: replace
|
|
@@ -10,12 +10,11 @@ maxSubagentDepth: 1
|
|
|
10
10
|
model: openai-codex/gpt-6-astra
|
|
11
11
|
thinking: xhigh
|
|
12
12
|
---
|
|
13
|
-
You are the inference specialist. The `inference` tool is your only way to reach Nexus
|
|
13
|
+
You are the inference specialist. The `inference` tool is your only way to reach Nexus and the Europa health endpoint. You have no shell and no file access; if a task needs anything else, say so and stop.
|
|
14
14
|
|
|
15
15
|
Rules:
|
|
16
16
|
|
|
17
|
-
- Health first: `nexus-status`, `
|
|
17
|
+
- Health first: `nexus-status`, `europa-health`. Europa is off most of the time by design; report "off, expected" rather than a fault.
|
|
18
18
|
- Restarts only when the task asks for them or a service is down; check status again afterwards.
|
|
19
|
-
-
|
|
20
|
-
- Never print log lines that contain tokens, keys, or Discord message content; summarise them.
|
|
19
|
+
- Never print log lines that contain tokens or keys; summarise them.
|
|
21
20
|
- Delegate through `subagent` only to another specialist (`astro.proxmox`, `astro.backup`, `astro.arcane`, `astro.identity`, `astro.network`, `astro.dns`, `astro.edge`, `astro.security`) and only for that specialist's own area.
|
|
@@ -7,6 +7,11 @@ import { addRestarts, astrogateActivity, herdrActivity, needsPrompt, readRestart
|
|
|
7
7
|
import {
|
|
8
8
|
allId,
|
|
9
9
|
arcaneServerVersion,
|
|
10
|
+
bwRelease,
|
|
11
|
+
gcloudRelease,
|
|
12
|
+
installedComponent,
|
|
13
|
+
jqRelease,
|
|
14
|
+
pythonBuild,
|
|
10
15
|
brewHasOutdated,
|
|
11
16
|
buildLines,
|
|
12
17
|
type Collected,
|
|
@@ -80,10 +85,23 @@ const COLLECTED: Collected = {
|
|
|
80
85
|
piLatest: "1.3.0",
|
|
81
86
|
piAstroInstalled: "0.33.0",
|
|
82
87
|
piAstroLatest: "0.33.0",
|
|
83
|
-
installer: {
|
|
88
|
+
installer: {
|
|
89
|
+
node: { current: "24.22.0", previous: null },
|
|
90
|
+
wrappers: "0.32.0",
|
|
91
|
+
arcaneCli: "2.16.1",
|
|
92
|
+
trendarr: "0.6.0",
|
|
93
|
+
python: { current: "3.14.8+20261009", previous: null },
|
|
94
|
+
gcloud: { current: "589.0.0", previous: null },
|
|
95
|
+
bw: { current: "2026.10.0", previous: null },
|
|
96
|
+
jq: { current: null, previous: null },
|
|
97
|
+
},
|
|
84
98
|
arcaneServer: "2.17.0",
|
|
85
99
|
trendarrLatest: "0.6.0",
|
|
86
100
|
nodeLts: "24.22.0",
|
|
101
|
+
pythonLatest: "3.14.8+20261013",
|
|
102
|
+
gcloudLatest: "590.0.0",
|
|
103
|
+
bwLatest: "2026.11.0",
|
|
104
|
+
jqLatest: "1.8.2",
|
|
87
105
|
warnings: [],
|
|
88
106
|
};
|
|
89
107
|
|
|
@@ -113,6 +131,34 @@ describe("versions", () => {
|
|
|
113
131
|
});
|
|
114
132
|
});
|
|
115
133
|
|
|
134
|
+
describe("root-owned tool releases", () => {
|
|
135
|
+
it("reads the newest Python build of the installed minor, Bitwarden CLI, jq, and gcloud versions", () => {
|
|
136
|
+
const asset = (v: string) => ({ name: `cpython-${v}+20261013-aarch64-apple-darwin-install_only_stripped.tar.gz` });
|
|
137
|
+
const python = { tag_name: "20261013", assets: [asset("3.14.8"), asset("3.15.0"), asset("3.14.10"), { name: "cpython-3.14.9+20261013-x86_64-apple-darwin-install_only_stripped.tar.gz" }, { name: "SHA256SUMS" }] };
|
|
138
|
+
expect(pythonBuild(python, "3.14")).toBe("3.14.10+20261013");
|
|
139
|
+
expect(pythonBuild(python, "3.13")).toBeNull();
|
|
140
|
+
expect(pythonBuild({ tag_name: "latest", assets: [] }, "3.14")).toBeNull();
|
|
141
|
+
expect(bwRelease([{ tag_name: "desktop-v2026.11.0" }, { tag_name: "cli-v2026.11.1", prerelease: true }, { tag_name: "cli-v2026.10.0" }, { tag_name: "cli-v2026.9.1" }])).toBe("2026.10.0");
|
|
142
|
+
expect(bwRelease({})).toBeNull();
|
|
143
|
+
expect(jqRelease({ tag_name: "jq-1.8.2" })).toBe("1.8.2");
|
|
144
|
+
expect(jqRelease({ tag_name: "1.8.2" })).toBeNull();
|
|
145
|
+
expect(gcloudRelease({ version: "589.0.0", revision: 20261009074343 })).toBe("589.0.0");
|
|
146
|
+
expect(gcloudRelease({ version: "latest" })).toBeNull();
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it("reads each component from the installer status", () => {
|
|
150
|
+
const status = { node: { current: "24.22.0", previous: null }, wrappers: "0.34.1", arcaneCli: "2.16.1", trendarr: null, python: { current: "3.14.8+20261013" }, jq: { current: null } };
|
|
151
|
+
expect(installedComponent(status, "node")).toBe("24.22.0");
|
|
152
|
+
expect(installedComponent(status, "wrappers")).toBe("0.34.1");
|
|
153
|
+
expect(installedComponent(status, "arcane-cli")).toBe("2.16.1");
|
|
154
|
+
expect(installedComponent(status, "trendarr")).toBeNull();
|
|
155
|
+
expect(installedComponent(status, "python")).toBe("3.14.8+20261013");
|
|
156
|
+
expect(installedComponent(status, "jq")).toBeNull();
|
|
157
|
+
expect(installedComponent(status, "gcloud")).toBeNull();
|
|
158
|
+
expect(installedComponent(null, "bw")).toBeNull();
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
116
162
|
describe("report lines", () => {
|
|
117
163
|
it("lists our tools first, then requested formulae and casks; dependencies and pinned packages stay out", () => {
|
|
118
164
|
const lines = buildLines(COLLECTED);
|
|
@@ -120,17 +166,20 @@ describe("report lines", () => {
|
|
|
120
166
|
"pi 0.87.1->1.3.0",
|
|
121
167
|
"spc:wrappers 0.32.0->0.33.0",
|
|
122
168
|
"spc:arcane-cli 2.16.1->2.17.0",
|
|
169
|
+
"spc:python 3.14.8+20261009->3.14.8+20261013",
|
|
170
|
+
"spc:gcloud 589.0.0->590.0.0",
|
|
171
|
+
"spc:bw 2026.10.0->2026.11.0",
|
|
123
172
|
"brew:bitwarden-cli 2026.8.0->2026.9.0",
|
|
124
173
|
"brew:gh 2.97.0->2.101.0",
|
|
125
174
|
"brew:herdr 0.9.0->0.9.1",
|
|
126
175
|
"brew:pnpm 11.22.0->12.6.0",
|
|
127
176
|
"cask:codex 0.152.1->0.156.1",
|
|
128
177
|
"cask:temurin@17 17.0.20,8->17.0.20.1,1",
|
|
129
|
-
"cask:gcloud-cli 585.0.0->586.0.0",
|
|
130
178
|
]);
|
|
131
179
|
expect(lines[0].major).toBe(true);
|
|
132
|
-
expect(lines.find((l) => l.key === "
|
|
133
|
-
expect(lines.find((l) => l.key === "
|
|
180
|
+
expect(lines.find((l) => l.key === "spc:gcloud")?.major).toBe(false);
|
|
181
|
+
expect(lines.find((l) => l.key === "spc:python")?.action).toEqual({ kind: "installer", component: "python" });
|
|
182
|
+
expect(lines.find((l) => l.key === "cask:codex")?.action).toEqual({ kind: "cask", token: "codex" });
|
|
134
183
|
expect(lines.every((l) => l.state === "offered")).toBe(true);
|
|
135
184
|
expect(brewHasOutdated(OUTDATED)).toBe(true);
|
|
136
185
|
expect(brewHasOutdated({ formulae: [OUTDATED.formulae[6]], casks: [] })).toBe(false);
|
|
@@ -166,12 +215,26 @@ describe("collection", () => {
|
|
|
166
215
|
const dir = mkdtempSync(join(tmpdir(), "tools-pkg-"));
|
|
167
216
|
try {
|
|
168
217
|
writeFileSync(join(dir, "package.json"), JSON.stringify({ version: "0.33.0" }));
|
|
169
|
-
const
|
|
218
|
+
const bodies: Record<string, unknown> = {
|
|
219
|
+
"https://nodejs.org/dist/index.json": [{ version: "v24.22.0", lts: "Krypton", files: ["osx-arm64-tar"] }],
|
|
220
|
+
"https://api.github.com/repos/astral-sh/python-build-standalone/releases/latest": { tag_name: "20261013", assets: [{ name: "cpython-3.14.8+20261013-aarch64-apple-darwin-install_only_stripped.tar.gz" }] },
|
|
221
|
+
"https://dl.google.com/dl/cloudsdk/channels/rapid/components-2.json": { version: "590.0.0" },
|
|
222
|
+
"https://api.github.com/repos/bitwarden/clients/releases?per_page=30": [{ tag_name: "cli-v2026.11.0" }],
|
|
223
|
+
};
|
|
224
|
+
const fetched: string[] = [];
|
|
225
|
+
const fetchImpl = vi.fn(async (url: string | URL) => {
|
|
226
|
+
fetched.push(String(url));
|
|
227
|
+
const body = bodies[String(url)];
|
|
228
|
+
return body === undefined ? new Response("{}", { status: 404 }) : new Response(JSON.stringify(body));
|
|
229
|
+
}) as unknown as typeof fetch;
|
|
170
230
|
const c = await collectTools({ exec, fetchImpl, piAstroPackage: join(dir, "package.json"), arcaneServer: async () => "2.16.1" });
|
|
171
231
|
expect(calls[0]).toBe("brew update");
|
|
172
232
|
expect(c.warnings.join(" | ")).toMatch(/brew update failed, versions may be stale: Error: network down/);
|
|
173
233
|
expect(c.warnings.join(" | ")).toMatch(/npm view @astrofoundry\/trendarr failed/);
|
|
174
234
|
expect(c).toMatchObject({ piInstalled: "0.87.1", piLatest: "1.3.0", piAstroInstalled: "0.33.0", nodeLts: "24.22.0", arcaneServer: "2.16.1", trendarrLatest: null });
|
|
235
|
+
expect(c).toMatchObject({ pythonLatest: "3.14.8+20261013", gcloudLatest: "590.0.0", bwLatest: "2026.11.0", jqLatest: null });
|
|
236
|
+
// jq is not installed by the installer yet, so its releases are not even asked for.
|
|
237
|
+
expect(fetched.some((u) => u.includes("jqlang"))).toBe(false);
|
|
175
238
|
} finally {
|
|
176
239
|
rmSync(dir, { recursive: true, force: true });
|
|
177
240
|
}
|
|
@@ -237,8 +300,8 @@ describe("buttons and commands", () => {
|
|
|
237
300
|
|
|
238
301
|
it("runs one fixed command per kind, reads the version back, and knows what to roll back and restart", () => {
|
|
239
302
|
expect(updateCommand({ kind: "brew", name: "monadeo/tap/astrogate" }, "0.0.11")).toEqual({ file: "/opt/homebrew/bin/brew", args: ["upgrade", "monadeo/tap/astrogate"] });
|
|
240
|
-
expect(updateCommand({ kind: "cask", token: "
|
|
241
|
-
expect(updateCommand({ kind: "
|
|
303
|
+
expect(updateCommand({ kind: "cask", token: "codex" }, "0.156.1").args).toEqual(["upgrade", "--cask", "codex"]);
|
|
304
|
+
expect(updateCommand({ kind: "installer", component: "python" }, "3.14.8+20261013").args).toEqual(["-n", "/usr/local/sbin/specialist-install", "python", "3.14.8+20261013"]);
|
|
242
305
|
expect(updateCommand({ kind: "pi" }, "1.3.0")).toEqual({ file: "/opt/homebrew/bin/pi", args: ["update", "--self"] });
|
|
243
306
|
expect(updateCommand({ kind: "pi-astro" }, "0.33.0").args).toEqual(["update", "npm:@astrofoundry/pi-astro"]);
|
|
244
307
|
expect(updateCommand({ kind: "installer", component: "arcane-cli" }, "2.17.0")).toEqual({ file: "/usr/bin/sudo", args: ["-n", "/usr/local/sbin/specialist-install", "arcane-cli", "2.17.0"] });
|
|
@@ -249,6 +312,8 @@ describe("buttons and commands", () => {
|
|
|
249
312
|
expect(verifiedVersion({ kind: "brew", name: "node" }, "node 26.7.0 26.9.0\n", "26.9.0")).toBe(true);
|
|
250
313
|
expect(verifiedVersion({ kind: "brew", name: "gh" }, "gh 2.97.0\n", "2.101.0")).toBe(false);
|
|
251
314
|
expect(verifiedVersion({ kind: "installer", component: "node" }, JSON.stringify({ node: { current: "24.23.0" } }), "24.23.0")).toBe(true);
|
|
315
|
+
expect(verifiedVersion({ kind: "installer", component: "gcloud" }, JSON.stringify({ gcloud: { current: "590.0.0", previous: "589.0.0" } }), "590.0.0")).toBe(true);
|
|
316
|
+
expect(verifiedVersion({ kind: "installer", component: "arcane-cli" }, JSON.stringify({ arcaneCli: "2.17.0" }), "2.17.0")).toBe(true);
|
|
252
317
|
expect(verifiedVersion({ kind: "pi" }, "not json", "1.3.0")).toBe(false);
|
|
253
318
|
expect(servicesFor({ kind: "pi" })).toEqual(["bridge"]);
|
|
254
319
|
expect(servicesFor({ kind: "brew", name: "node" })).toEqual(["astrogate", "bridge"]);
|
|
@@ -262,17 +327,17 @@ describe("release-notes review", () => {
|
|
|
262
327
|
const release = (tag: string, body: string, prerelease = false) => ({ tag_name: tag, name: tag, body, published_at: "2026-10-15T00:00:00Z", html_url: `https://github.com/x/releases/${tag}`, draft: false, prerelease });
|
|
263
328
|
|
|
264
329
|
it("reads only the tool's own releases in a shared repository, between the two versions", async () => {
|
|
265
|
-
const fetchImpl = vi.fn(async () => new Response(JSON.stringify([release("cli-v2026.
|
|
266
|
-
const line = buildLines(COLLECTED).find((l) => l.key === "
|
|
330
|
+
const fetchImpl = vi.fn(async () => new Response(JSON.stringify([release("cli-v2026.11.0", "bw: new list flag"), release("desktop-v2026.11.0", "desktop only"), release("cli-v2026.10.0", "old")]))) as unknown as typeof fetch;
|
|
331
|
+
const line = buildLines(COLLECTED).find((l) => l.key === "spc:bw") as ToolLine;
|
|
267
332
|
const notes = await notesFor(line, fetchImpl);
|
|
268
|
-
expect(notes.notes?.map((n) => n.tag)).toEqual(["v2026.
|
|
333
|
+
expect(notes.notes?.map((n) => n.tag)).toEqual(["v2026.11.0"]);
|
|
269
334
|
const prompt = reviewPrompt(line, notes.notes ?? [], []);
|
|
270
335
|
expect(prompt).toMatch(/How Cortex uses it: specialist-cli provision runs bw/);
|
|
271
|
-
expect(prompt).toMatch(/<<<RELEASE NOTES\n### v2026\.
|
|
336
|
+
expect(prompt).toMatch(/<<<RELEASE NOTES\n### v2026\.11\.0 \(2026-10-15\)\nbw: new list flag\nRELEASE NOTES>>>/);
|
|
272
337
|
});
|
|
273
338
|
|
|
274
339
|
it("says why there is nothing to review", async () => {
|
|
275
|
-
const gcloud = buildLines(COLLECTED).find((l) => l.key === "
|
|
340
|
+
const gcloud = buildLines(COLLECTED).find((l) => l.key === "spc:gcloud") as ToolLine;
|
|
276
341
|
expect(await notesFor(gcloud, fetch)).toEqual({ notes: null, reason: "no release notes to review here; see https://cloud.google.com/sdk/docs/release-notes" });
|
|
277
342
|
const trendarr: ToolLine = { key: "spc:trendarr", label: "trendarr", installed: "0.6.0", latest: "0.7.0", major: false, action: { kind: "installer", component: "trendarr" }, state: "offered" };
|
|
278
343
|
const notFound = vi.fn(async () => new Response("{}", { status: 404 })) as unknown as typeof fetch;
|
|
@@ -17,14 +17,11 @@ const PI_ASTRO = "@astrofoundry/pi-astro";
|
|
|
17
17
|
const TRENDARR = "@astrofoundry/trendarr";
|
|
18
18
|
/** Homebrew's own search path plus the system one; Homebrew and npm scripts start with `#!/usr/bin/env node`. */
|
|
19
19
|
export const TOOL_PATH = "/opt/homebrew/bin:/usr/bin:/bin:/usr/sbin:/sbin";
|
|
20
|
-
|
|
21
|
-
const TRACKED_SELF_UPDATING = ["gcloud-cli"];
|
|
22
|
-
|
|
23
|
-
export type Component = "node" | "wrappers" | "arcane-cli" | "trendarr";
|
|
20
|
+
export type Component = "node" | "wrappers" | "arcane-cli" | "trendarr" | "python" | "gcloud" | "bw" | "jq";
|
|
24
21
|
|
|
25
22
|
export type UpdateAction =
|
|
26
23
|
| { kind: "brew"; name: string }
|
|
27
|
-
| { kind: "cask"; token: string
|
|
24
|
+
| { kind: "cask"; token: string }
|
|
28
25
|
| { kind: "pi" }
|
|
29
26
|
| { kind: "pi-astro" }
|
|
30
27
|
| { kind: "installer"; component: Component };
|
|
@@ -121,6 +118,12 @@ export interface Collected {
|
|
|
121
118
|
arcaneServer: string | null;
|
|
122
119
|
trendarrLatest: string | null;
|
|
123
120
|
nodeLts: string | null;
|
|
121
|
+
/** python-build-standalone's newest build of the installed minor, as `x.y.z+YYYYMMDD`. */
|
|
122
|
+
pythonLatest: string | null;
|
|
123
|
+
/** The version of Google's rapid channel. */
|
|
124
|
+
gcloudLatest: string | null;
|
|
125
|
+
bwLatest: string | null;
|
|
126
|
+
jqLatest: string | null;
|
|
124
127
|
warnings: string[];
|
|
125
128
|
}
|
|
126
129
|
|
|
@@ -130,13 +133,6 @@ interface BrewInfoFormula {
|
|
|
130
133
|
installed?: { installed_on_request?: unknown }[];
|
|
131
134
|
}
|
|
132
135
|
|
|
133
|
-
interface BrewInfoCask {
|
|
134
|
-
token?: unknown;
|
|
135
|
-
installed?: unknown;
|
|
136
|
-
version?: unknown;
|
|
137
|
-
auto_updates?: unknown;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
136
|
interface OutdatedEntry {
|
|
141
137
|
name?: unknown;
|
|
142
138
|
installed_versions?: unknown;
|
|
@@ -173,10 +169,30 @@ function str(v: unknown): string | null {
|
|
|
173
169
|
return typeof v === "string" && v.length > 0 ? v : null;
|
|
174
170
|
}
|
|
175
171
|
|
|
176
|
-
const COMPONENT_LABEL: Record<Component, string> = {
|
|
172
|
+
const COMPONENT_LABEL: Record<Component, string> = {
|
|
173
|
+
node: "specialist Node",
|
|
174
|
+
wrappers: "specialist wrappers",
|
|
175
|
+
"arcane-cli": "arcane-cli",
|
|
176
|
+
trendarr: "trendarr",
|
|
177
|
+
python: "specialist Python",
|
|
178
|
+
gcloud: "specialist gcloud",
|
|
179
|
+
bw: "specialist bw",
|
|
180
|
+
jq: "specialist jq",
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
/** gcloud raises its first version number with every release, so a change there says nothing. */
|
|
184
|
+
const NO_MAJOR_FLAG: readonly string[] = ["spc:gcloud"];
|
|
177
185
|
|
|
178
186
|
function line(key: string, label: string, installed: string, latest: string, action: UpdateAction): ToolLine {
|
|
179
|
-
return { key, label, installed, latest, major: majorChange(installed, latest), action, state: "offered" };
|
|
187
|
+
return { key, label, installed, latest, major: !NO_MAJOR_FLAG.includes(key) && majorChange(installed, latest), action, state: "offered" };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** What `specialist-install status` reports for one component. */
|
|
191
|
+
export function installedComponent(status: unknown, component: Component): string | null {
|
|
192
|
+
const s = (status ?? {}) as Record<string, unknown>;
|
|
193
|
+
const field = component === "arcane-cli" ? s.arcaneCli : s[component];
|
|
194
|
+
const value = typeof field === "object" && field !== null ? (field as { current?: unknown }).current : field;
|
|
195
|
+
return str(value);
|
|
180
196
|
}
|
|
181
197
|
|
|
182
198
|
/** Only real updates: the offered version must be newer than the installed one. */
|
|
@@ -189,12 +205,19 @@ export function buildLines(c: Collected): ToolLine[] {
|
|
|
189
205
|
const lines: ToolLine[] = [];
|
|
190
206
|
if (newer(c.piInstalled, c.piLatest)) lines.push(line("pi", "Pi", c.piInstalled as string, c.piLatest, { kind: "pi" }));
|
|
191
207
|
if (newer(c.piAstroInstalled, c.piAstroLatest)) lines.push(line("pi-astro", "pi-astro", c.piAstroInstalled as string, c.piAstroLatest, { kind: "pi-astro" }));
|
|
192
|
-
const status = (c.installer ?? {}) as { node?: { current?: unknown }; wrappers?: unknown; arcaneCli?: unknown; trendarr?: unknown };
|
|
193
|
-
const installedOf: Record<Component, string | null> = { node: str(status.node?.current), wrappers: str(status.wrappers), "arcane-cli": str(status.arcaneCli), trendarr: str(status.trendarr) };
|
|
194
208
|
// The wrappers follow the pi-astro release Pi has installed; arcane-cli follows the Arcane server.
|
|
195
|
-
const targetOf: Record<Component, string | null> = {
|
|
196
|
-
|
|
197
|
-
|
|
209
|
+
const targetOf: Record<Component, string | null> = {
|
|
210
|
+
node: c.nodeLts,
|
|
211
|
+
wrappers: c.piAstroInstalled,
|
|
212
|
+
"arcane-cli": c.arcaneServer,
|
|
213
|
+
trendarr: c.trendarrLatest,
|
|
214
|
+
python: c.pythonLatest,
|
|
215
|
+
gcloud: c.gcloudLatest,
|
|
216
|
+
bw: c.bwLatest,
|
|
217
|
+
jq: c.jqLatest,
|
|
218
|
+
};
|
|
219
|
+
for (const component of ["wrappers", "arcane-cli", "trendarr", "node", "python", "gcloud", "bw", "jq"] as const) {
|
|
220
|
+
const installed = installedComponent(c.installer, component);
|
|
198
221
|
const target = targetOf[component];
|
|
199
222
|
if (newer(installed, target)) lines.push(line(`spc:${component}`, COMPONENT_LABEL[component], installed as string, target, { kind: "installer", component }));
|
|
200
223
|
}
|
|
@@ -207,14 +230,7 @@ export function buildLines(c: Collected): ToolLine[] {
|
|
|
207
230
|
const casks = outdatedEntries(c.brewOutdated, "casks");
|
|
208
231
|
for (const k of casks.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
209
232
|
if (k.pinned) continue;
|
|
210
|
-
lines.push(line(`cask:${k.name}`, k.name, k.installed, k.latest, { kind: "cask", token: k.name
|
|
211
|
-
}
|
|
212
|
-
const infoCasks = (c.brewInfo as { casks?: BrewInfoCask[] } | null)?.casks;
|
|
213
|
-
for (const k of Array.isArray(infoCasks) ? infoCasks : []) {
|
|
214
|
-
if (typeof k.token !== "string" || !TRACKED_SELF_UPDATING.includes(k.token) || casks.some((o) => o.name === k.token)) continue;
|
|
215
|
-
const installed = str(k.installed);
|
|
216
|
-
const latest = str(k.version);
|
|
217
|
-
if (newer(installed, latest)) lines.push(line(`cask:${k.token}`, k.token, installed as string, latest, { kind: "cask", token: k.token, greedy: true }));
|
|
233
|
+
lines.push(line(`cask:${k.name}`, k.name, k.installed, k.latest, { kind: "cask", token: k.name }));
|
|
218
234
|
}
|
|
219
235
|
return lines;
|
|
220
236
|
}
|
|
@@ -240,6 +256,42 @@ export function arcaneServerVersion(json: unknown): string | null {
|
|
|
240
256
|
return typeof v === "string" && /^v?\d+(\.\d+)+$/.test(v) ? v.replace(/^v/, "") : null;
|
|
241
257
|
}
|
|
242
258
|
|
|
259
|
+
/** The newest `cpython-<minor>.N+<build>` install-only build for macOS arm64 in a python-build-standalone release. */
|
|
260
|
+
export function pythonBuild(release: unknown, minor: string): string | null {
|
|
261
|
+
const r = release as { tag_name?: unknown; assets?: { name?: unknown }[] } | null;
|
|
262
|
+
if (!r || typeof r.tag_name !== "string" || !/^\d{8}$/.test(r.tag_name) || !Array.isArray(r.assets)) return null;
|
|
263
|
+
const prefix = `cpython-${minor}.`;
|
|
264
|
+
const suffix = `+${r.tag_name}-aarch64-apple-darwin-install_only_stripped.tar.gz`;
|
|
265
|
+
const versions = r.assets
|
|
266
|
+
.map((a) => (typeof a.name === "string" && a.name.startsWith(prefix) && a.name.endsWith(suffix) ? a.name.slice("cpython-".length, -suffix.length) : null))
|
|
267
|
+
.filter((v): v is string => v !== null && /^\d+\.\d+\.\d+$/.test(v))
|
|
268
|
+
.sort((a, b) => compareLoose(b, a));
|
|
269
|
+
return versions[0] ? `${versions[0]}+${r.tag_name}` : null;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/** The newest Bitwarden CLI release, from a page of bitwarden/clients releases (tags `cli-vX.Y.Z`). */
|
|
273
|
+
export function bwRelease(releases: unknown): string | null {
|
|
274
|
+
if (!Array.isArray(releases)) return null;
|
|
275
|
+
const versions = (releases as { tag_name?: unknown; prerelease?: unknown; draft?: unknown }[])
|
|
276
|
+
.filter((r) => typeof r.tag_name === "string" && r.prerelease !== true && r.draft !== true)
|
|
277
|
+
.map((r) => /^cli-v(\d+\.\d+\.\d+)$/.exec(r.tag_name as string)?.[1] ?? null)
|
|
278
|
+
.filter((v): v is string => v !== null)
|
|
279
|
+
.sort((a, b) => compareLoose(b, a));
|
|
280
|
+
return versions[0] ?? null;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/** `jq-X.Y.Z` of jqlang/jq's latest release. */
|
|
284
|
+
export function jqRelease(release: unknown): string | null {
|
|
285
|
+
const tag = (release as { tag_name?: unknown } | null)?.tag_name;
|
|
286
|
+
return typeof tag === "string" ? (/^jq-(\d+\.\d+\.\d+)$/.exec(tag)?.[1] ?? null) : null;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/** `version` of Google's components snapshot. */
|
|
290
|
+
export function gcloudRelease(snapshot: unknown): string | null {
|
|
291
|
+
const v = (snapshot as { version?: unknown } | null)?.version;
|
|
292
|
+
return typeof v === "string" && /^\d+\.\d+\.\d+$/.test(v) ? v : null;
|
|
293
|
+
}
|
|
294
|
+
|
|
243
295
|
export function npmGlobalVersion(json: unknown, pkg: string): string | null {
|
|
244
296
|
const v = (json as { dependencies?: Record<string, { version?: unknown }> } | null)?.dependencies?.[pkg]?.version;
|
|
245
297
|
return typeof v === "string" ? v : null;
|
|
@@ -323,6 +375,25 @@ export async function collectTools(options: CollectOptions): Promise<Collected>
|
|
|
323
375
|
} catch (err) {
|
|
324
376
|
warnings.push(`nodejs.org index failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
325
377
|
}
|
|
378
|
+
const fetchJson = async (url: string, what: string): Promise<unknown> => {
|
|
379
|
+
try {
|
|
380
|
+
const res = await options.fetchImpl(url, { headers: { accept: "application/json", "user-agent": "pi-astro-tools-report" }, signal: AbortSignal.timeout(30_000) });
|
|
381
|
+
if (!res.ok) {
|
|
382
|
+
warnings.push(`${what} answered ${res.status}`);
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
return await res.json();
|
|
386
|
+
} catch (err) {
|
|
387
|
+
warnings.push(`${what} failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
// Only for tools the installer manages; the first install of each is a manual step.
|
|
392
|
+
const python = installedComponent(installer, "python");
|
|
393
|
+
const pythonLatest = python ? pythonBuild(await fetchJson("https://api.github.com/repos/astral-sh/python-build-standalone/releases/latest", "python-build-standalone"), python.split(".").slice(0, 2).join(".")) : null;
|
|
394
|
+
const gcloudLatest = installedComponent(installer, "gcloud") ? gcloudRelease(await fetchJson("https://dl.google.com/dl/cloudsdk/channels/rapid/components-2.json", "the gcloud release snapshot")) : null;
|
|
395
|
+
const bwLatest = installedComponent(installer, "bw") ? bwRelease(await fetchJson("https://api.github.com/repos/bitwarden/clients/releases?per_page=30", "bitwarden/clients releases")) : null;
|
|
396
|
+
const jqLatest = installedComponent(installer, "jq") ? jqRelease(await fetchJson("https://api.github.com/repos/jqlang/jq/releases/latest", "jqlang/jq releases")) : null;
|
|
326
397
|
let arcaneServer: string | null = null;
|
|
327
398
|
try {
|
|
328
399
|
arcaneServer = await options.arcaneServer();
|
|
@@ -341,6 +412,10 @@ export async function collectTools(options: CollectOptions): Promise<Collected>
|
|
|
341
412
|
arcaneServer,
|
|
342
413
|
trendarrLatest: await npmLatest(exec, TRENDARR, warnings),
|
|
343
414
|
nodeLts,
|
|
415
|
+
pythonLatest,
|
|
416
|
+
gcloudLatest,
|
|
417
|
+
bwLatest,
|
|
418
|
+
jqLatest,
|
|
344
419
|
warnings,
|
|
345
420
|
};
|
|
346
421
|
}
|
|
@@ -380,11 +455,11 @@ export const REVIEW_SOURCES: Record<string, ReviewSource> = {
|
|
|
380
455
|
repo: "astrofoundry/trendarr",
|
|
381
456
|
usage: "The media wrapper runs trendarr trending, search, add-movies, add-series and sabnzbd status|pause|resume with --json and parses that JSON; it writes ~/.trendarr/config.json for each call.",
|
|
382
457
|
},
|
|
383
|
-
"
|
|
458
|
+
"spc:gcloud": {
|
|
384
459
|
notesUrl: "https://cloud.google.com/sdk/docs/release-notes",
|
|
385
|
-
usage: "The edge specialist runs gcloud auth activate-service-account --key-file and gcloud compute start-iap-tunnel <instance> 22 --local-host-port, with CLOUDSDK_CONFIG and CLOUDSDK_PYTHON set.",
|
|
460
|
+
usage: "The edge specialist runs gcloud auth activate-service-account --key-file and gcloud compute start-iap-tunnel <instance> 22 --local-host-port, with CLOUDSDK_CONFIG and CLOUDSDK_PYTHON set; gcloud runs on the specialist Python from python-build-standalone.",
|
|
386
461
|
},
|
|
387
|
-
"
|
|
462
|
+
"spc:bw": {
|
|
388
463
|
repo: "bitwarden/clients",
|
|
389
464
|
tagPrefix: "cli-",
|
|
390
465
|
usage: "specialist-cli provision runs bw status, config server, login --apikey, unlock --passwordfile --raw, sync, list collections, list items --collectionid, and lock, parsing the JSON with jq.",
|
|
@@ -464,7 +539,7 @@ export function updateCommand(action: UpdateAction, version: string): Command {
|
|
|
464
539
|
case "brew":
|
|
465
540
|
return { file: BREW, args: ["upgrade", action.name] };
|
|
466
541
|
case "cask":
|
|
467
|
-
return { file: BREW, args: ["upgrade", "--cask",
|
|
542
|
+
return { file: BREW, args: ["upgrade", "--cask", action.token] };
|
|
468
543
|
case "pi":
|
|
469
544
|
return { file: PI, args: ["update", "--self"] };
|
|
470
545
|
case "pi-astro":
|
|
@@ -507,11 +582,7 @@ export function verifiedVersion(action: UpdateAction, stdout: string, version: s
|
|
|
507
582
|
return false;
|
|
508
583
|
}
|
|
509
584
|
if (action.kind === "pi") return npmGlobalVersion(json, PI_PACKAGE) === version;
|
|
510
|
-
if (action.kind === "installer")
|
|
511
|
-
const s = json as { node?: { current?: unknown }; wrappers?: unknown; arcaneCli?: unknown; trendarr?: unknown };
|
|
512
|
-
const now = action.component === "node" ? s.node?.current : action.component === "wrappers" ? s.wrappers : action.component === "arcane-cli" ? s.arcaneCli : s.trendarr;
|
|
513
|
-
return now === version;
|
|
514
|
-
}
|
|
585
|
+
if (action.kind === "installer") return installedComponent(json, action.component) === version;
|
|
515
586
|
return false;
|
|
516
587
|
}
|
|
517
588
|
|
|
@@ -534,7 +605,7 @@ const SECONDARY = 2;
|
|
|
534
605
|
const SUCCESS = 3;
|
|
535
606
|
const DANGER = 4;
|
|
536
607
|
const CUSTOM_ID_MAX = 100;
|
|
537
|
-
const KEY = /^(pi|pi-astro|spc:(?:node|wrappers|arcane-cli|trendarr)|(?:brew|cask):[A-Za-z0-9@._/+-]{1,60})$/;
|
|
608
|
+
const KEY = /^(pi|pi-astro|spc:(?:node|wrappers|arcane-cli|trendarr|python|gcloud|bw|jq)|(?:brew|cask):[A-Za-z0-9@._/+-]{1,60})$/;
|
|
538
609
|
const VERSION = /^[A-Za-z0-9._,+-]{1,40}$/;
|
|
539
610
|
|
|
540
611
|
export function updateId(line: ToolLine): string {
|
|
@@ -31,7 +31,7 @@ describe("approvals", () => {
|
|
|
31
31
|
expect(isRisky("proxmox", ["set", "107", "memory=768"])).toBe(true);
|
|
32
32
|
expect(isRisky("proxmox", ["shutdown", "107", "--confirm"])).toBe(true);
|
|
33
33
|
expect(isRisky("security", ["decisions"])).toBe(false);
|
|
34
|
-
expect(isRisky("inference", ["
|
|
34
|
+
expect(isRisky("inference", ["nexus-restart", "embed"])).toBe(true);
|
|
35
35
|
expect(isRisky("backup", ["restic-snapshots"])).toBe(false);
|
|
36
36
|
expect(isRisky("backup", ["vzdump-run", "107"])).toBe(true);
|
|
37
37
|
expect(isRisky("media", ["add-movies", "tt1160419"])).toBe(false);
|
|
@@ -37,7 +37,7 @@ export const RISKY: Readonly<Record<string, readonly RegExp[]>> = {
|
|
|
37
37
|
edge: [/^vps\s+deploy-nginx\b/],
|
|
38
38
|
backup: [/^(vzdump-run|host-backup-run|arcane-backup-run|gcs-run|restic-check-run|restic-restore|restore-clean)\b/],
|
|
39
39
|
proxmox: [/^(set|snapshot-create|start)\b/],
|
|
40
|
-
inference: [/^(nexus-restart|nexus-start
|
|
40
|
+
inference: [/^(nexus-restart|nexus-start)\b/],
|
|
41
41
|
// media has no entry: trendarr cannot delete anything, and adds and queue toggles run on the caller's request.
|
|
42
42
|
};
|
|
43
43
|
|
package/package.json
CHANGED
package/skills/edge/SKILL.md
CHANGED
|
@@ -13,7 +13,7 @@ Fetches `https://auth.37pla.net/.well-known/openid-configuration` through the VP
|
|
|
13
13
|
|
|
14
14
|
## `route-probe [name...]`
|
|
15
15
|
|
|
16
|
-
HTTP status of the public routes of services on the Arcane VM, from Cortex through public DNS, the way users reach them: no credentials, no redirect followed. Without names it checks all of them: `supabase-auth-settings` (expects 401: it wants an API key), `supabase-jwks`, `supabase-openid`, `supabase-oauth-discovery
|
|
16
|
+
HTTP status of the public routes of services on the Arcane VM, from Cortex through public DNS, the way users reach them: no credentials, no redirect followed. Without names it checks all of them: `supabase-auth-settings` (expects 401: it wants an API key), `supabase-jwks`, `supabase-openid`, `supabase-oauth-discovery` (expects 404 while the Supabase Auth OAuth server is off), `supabase-studio` (expects a redirect to the Pomerium sign-in), `grimoire-public`, `grimoire-public-health` (200), `mealie-public` (expects a redirect to the Pomerium sign-in; checked 2026-10-19). Supabase Auth serves the OAuth discovery document only when its OAuth server is enabled; if it is turned on, change that expectation to 200. Each row has `httpCode`, `expected`, `ok`, `seconds`. A status says the route answered; it does not prove the application works or that a browser login succeeds. A failing route with a passing `probe` points at that service's route or container; ask `astro.identity` (Pomerium route) or `astro.arcane` (container) for their side.
|
|
17
17
|
|
|
18
18
|
## `vps <command>`
|
|
19
19
|
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: inference
|
|
3
|
-
description: How the inference specialist operates llama.cpp on Nexus
|
|
3
|
+
description: How the inference specialist operates llama.cpp on Nexus and the Europa health check through the inference tool.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Inference specialist
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Two hosts serve AI workloads. Nexus (Mac Studio, `10.0.40.38`, user `nexus`) runs two llama.cpp LaunchAgents: `com.nexus.llama-embed` (Qwen3-Embedding-8B, port 8081, `/v1/embeddings`) and `com.nexus.llama-rerank` (Qwen3-Reranker-4B, port 8082, `/v1/rerank`); Pomerium publishes them as `embed.37pla.net` and `rerank.37pla.net`. Europa (`10.0.60.5`, Windows, RTX 5090) runs an on-demand embedding server on 8081 and is off most of the time by design. `["--help"]` lists every command.
|
|
9
9
|
|
|
10
10
|
## Nexus
|
|
11
11
|
|
|
@@ -20,35 +20,11 @@ Three hosts serve AI workloads. Nexus (Mac Studio, `10.0.40.38`, user `nexus`) r
|
|
|
20
20
|
|
|
21
21
|
A model load after restart takes up to a minute; `health=503` during that time is loading, not failure.
|
|
22
22
|
|
|
23
|
-
## Hermes
|
|
24
|
-
|
|
25
|
-
| Command | Effect |
|
|
26
|
-
|---|---|
|
|
27
|
-
| `hermes-status` | both user services |
|
|
28
|
-
| `hermes-journal <since>` | user journal of both services |
|
|
29
|
-
| `hermes-errors` | `hermes logs errors` |
|
|
30
|
-
| `hermes-restart <gateway|api>` | restart one service |
|
|
31
|
-
| `hermes-api-health` | `/health` (expect 200) and `/v1/models` (expect 401: the endpoint is up and requires the backend key) |
|
|
32
|
-
| `hermes-version` | installed version |
|
|
33
|
-
| `hermes-guest-status`, `hermes-snapshots` | LXC 101 state and snapshots (Pulsar) |
|
|
34
|
-
| `hermes-releases [n]` | upstream releases of `NousResearch/hermes-agent`, notes cut at 6000 characters |
|
|
35
|
-
| `hermes-release <tag>` | one release with its complete notes |
|
|
36
|
-
| `hermes-upgrade --confirm` | Pulsar wrapper: snapshot `preupgrade<timestamp>`, `hermes update`, `hermes config migrate`, restart both gateways, wait for a Discord connection and API 401, keep the two newest snapshots |
|
|
37
|
-
|
|
38
|
-
## Hermes upgrade procedure
|
|
39
|
-
|
|
40
|
-
1. `hermes-version` and `hermes-releases 10`.
|
|
41
|
-
2. Read every release from the installed version up to the target; use `hermes-release <tag>` for any note the list cut off. Stop and report when a note mentions a manual step, a configuration change beyond `hermes config migrate`, a breaking change, a new Python or system dependency, or a required intermediate version. Quote the sentence.
|
|
42
|
-
3. `hermes-status` and `hermes-api-health` must be healthy before the upgrade; `hermes-snapshots` shows the existing snapshots.
|
|
43
|
-
4. `hermes-upgrade --confirm` (up to 20 minutes). The wrapper prints the rollback command when a step fails; repeat it verbatim in your report and do not roll back yourself.
|
|
44
|
-
5. `hermes-version`, `hermes-status`, `hermes-api-health`. Report old and new version, the snapshot name, and the health results.
|
|
45
|
-
|
|
46
23
|
## Europa
|
|
47
24
|
|
|
48
25
|
`europa-health` returns `ok: false` with `httpCode: null` when Europa is off. That is the normal state; report it as expected unless the task says Europa should be running. Starting or stopping Europa is an operator task.
|
|
49
26
|
|
|
50
27
|
## When to stop and report
|
|
51
28
|
|
|
52
|
-
- A release note requires a manual step or the installed version is more than one release behind with breaking changes in between.
|
|
53
29
|
- A Nexus service stays at `health=503` or `000` for more than two minutes after a restart: report the last log lines (without any key material).
|
|
54
|
-
- The task asks for model, port, or configuration changes: those are edits to the tracked plists in the homelab repository (`04-nexus-macstudio/llama.cpp/launchagents/`)
|
|
30
|
+
- The task asks for model, port, or configuration changes: those are edits to the tracked plists in the homelab repository (`04-nexus-macstudio/llama.cpp/launchagents/`), an operator task.
|
package/skills/proxmox/SKILL.md
CHANGED
|
@@ -5,7 +5,7 @@ description: How the proxmox specialist operates Proxmox VE on Pulsar through th
|
|
|
5
5
|
|
|
6
6
|
# Proxmox specialist
|
|
7
7
|
|
|
8
|
-
Pulsar (`10.0.10.37`, Proxmox VE 9 on Debian 13, node name `pulsar`) runs every guest: VM 100 `arcane` (Docker platform), LXC 101 `hermes
|
|
8
|
+
Pulsar (`10.0.10.37`, Proxmox VE 9 on Debian 13, node name `pulsar`) runs every guest: VM 100 `arcane` (Docker platform), LXC 101 `hermes` (being retired by the operator; no specialist operates it), LXC 102 `backup` (GCS pusher), VM 103 `identity` (Zitadel), VM 104 `dmz` (Pomerium), LXC 105 `tailscale`, VM 106 `observability`, LXC 107 `technitium2`, LXC 108 `radius`. Storage `local-lvm` holds the disks, `unas-backup` the vzdump archives. The `proxmox` tool talks to the API with a token whose role allows audit everywhere and, on guests, power, snapshots, and CPU/memory/option/network settings; nothing else. `["--help"]` lists the commands.
|
|
9
9
|
|
|
10
10
|
## API commands
|
|
11
11
|
|
|
@@ -27,7 +27,7 @@ Power actions and snapshot changes return `{ upid, exitstatus }`; `OK` means don
|
|
|
27
27
|
```json
|
|
28
28
|
{ "args": ["guests"] }
|
|
29
29
|
{ "args": ["guest", "106"] }
|
|
30
|
-
{ "args": ["snapshot-create", "
|
|
30
|
+
{ "args": ["snapshot-create", "107", "pre-change", "before Technitium config edit"] }
|
|
31
31
|
{ "args": ["shutdown", "107", "--confirm"] }
|
|
32
32
|
{ "args": ["set", "107", "memory=768"] }
|
|
33
33
|
```
|
package/skills/security/SKILL.md
CHANGED
|
@@ -48,10 +48,10 @@ The `security` tool is read-only. Every call is a fixed command on the guest; `[
|
|
|
48
48
|
| Wazuh agent | Host | Specialist that operates it |
|
|
49
49
|
|---|---|---|
|
|
50
50
|
| `frontdoor-1337` | Frontdoor VPS, Debian 13 | `astro.edge` |
|
|
51
|
-
| `arcane` | VM
|
|
51
|
+
| `arcane` | VM 100, Docker host | `astro.arcane` for containers |
|
|
52
52
|
| `identity` | VM 103, ZITADEL | `astro.identity` |
|
|
53
53
|
| `dmz` | VM 104, Pomerium and nginx | `astro.identity` |
|
|
54
|
-
| `hermes` | LXC
|
|
54
|
+
| `hermes` | LXC 101, Hermes (being retired) | none; the operator |
|
|
55
55
|
| `NEXUS` | Mac Studio | `astro.inference` |
|
|
56
56
|
| `CORTEX`, `tailscale` | Mac mini, LXC 105 | none; the operator |
|
|
57
57
|
|