@astrofoundry/pi-astro 0.18.1 → 0.18.2
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/agents/arcane.md +1 -1
- package/extensions/astro-subagents/index.ts +11 -3
- package/package.json +1 -1
- package/skills/arcane/SKILL.md +6 -4
- package/specialists/AGENTS.md +1 -1
- package/specialists/README.md +1 -1
- package/specialists/arcane/run.ts +21 -14
- package/specialists/wrappers.test.ts +5 -2
package/agents/arcane.md
CHANGED
|
@@ -13,7 +13,7 @@ You are the Arcane specialist. The `arcane` tool is your only way to reach Arcan
|
|
|
13
13
|
Rules:
|
|
14
14
|
|
|
15
15
|
- Read first. Run `arcane` with `["projects", "list"]` or `["containers", "list"]` before proposing a change.
|
|
16
|
-
- Project definitions (Compose files, `.env` layout, project folders) change only through `gitops write`, `gitops commit`, `gitops push`, then a sync or `projects up`. Never
|
|
16
|
+
- Project definitions of GitOps-managed projects (Compose files, `.env` layout, project folders) change only through `gitops show`, `gitops write`, `gitops commit`, `gitops push`, then a sync or `projects up`. Never edit a managed project through `projects update` or `projects workspace`, even though the CLI allows it.
|
|
17
17
|
- Operational actions (restart, logs, stats, redeploy, image pull, prune) go straight through the CLI.
|
|
18
18
|
- Confirm destructive operations (`down`, `delete`, `prune`) are explicitly requested in the task before running them.
|
|
19
19
|
- Report facts from the JSON output. Sensitive fields are already removed; never guess at what was removed.
|
|
@@ -247,10 +247,18 @@ export default function astroSubagents(pi: ExtensionAPI, options: GateOptions =
|
|
|
247
247
|
}
|
|
248
248
|
const agents = listAgents(ctx.cwd, "user").agents;
|
|
249
249
|
ctx.ui.notify(`running ${parsed.agent}...`, "info");
|
|
250
|
-
|
|
250
|
+
ctx.ui.setStatus("subagent", `${parsed.agent} running`);
|
|
251
|
+
let result: RunResult;
|
|
252
|
+
try {
|
|
253
|
+
result = await runOne(ctx, agents, parsed.agent, parsed.task, undefined, undefined, undefined, undefined);
|
|
254
|
+
} finally {
|
|
255
|
+
ctx.ui.setStatus("subagent", undefined);
|
|
256
|
+
}
|
|
257
|
+
const status = isFailed(result) ? "failed" : "done";
|
|
258
|
+
ctx.ui.notify(`${parsed.agent} ${status}`, isFailed(result) ? "warning" : "info");
|
|
251
259
|
pi.sendMessage(
|
|
252
|
-
{ customType: "astro-subagents", content: `
|
|
253
|
-
{ deliverAs: "
|
|
260
|
+
{ customType: "astro-subagents", content: `Result from /run ${parsed.agent} (${status}). Task: ${parsed.task}\n\n${resultOutput(result)}`, display: true },
|
|
261
|
+
{ deliverAs: "followUp", triggerTurn: true },
|
|
254
262
|
);
|
|
255
263
|
},
|
|
256
264
|
});
|
package/package.json
CHANGED
package/skills/arcane/SKILL.md
CHANGED
|
@@ -21,11 +21,13 @@ Arcane manages the Docker host on VM 100 (`10.0.40.30`). Every app is a Compose
|
|
|
21
21
|
|
|
22
22
|
## Two kinds of operations
|
|
23
23
|
|
|
24
|
-
1. **Definitions
|
|
25
|
-
`gitops pull` → `gitops write <path> <content>` → `gitops diff` → `gitops commit <message>` → `gitops push` → then `["projects", "up", "<name>"]
|
|
26
|
-
2. **Operations**: restart, logs, stats, redeploy, pull images, prune. Straight through the CLI.
|
|
24
|
+
1. **Definitions** of GitOps-managed projects (every project whose `gitOpsManagedBy` field is set, including `arcane` itself): Compose files, env layout, new or removed projects. Only through the repository:
|
|
25
|
+
`gitops pull` → `gitops ls` / `gitops show <path>` to read the current files → `gitops write <path> <content>` → `gitops diff` → `gitops commit <message>` → `gitops push` → then the project's GitOps sync or `["projects", "up", "<name>"]`, then verify with `["projects", "list"]` and container logs. Editing a managed project directly (`projects update`, `projects workspace`) makes the repository lie; do not do it.
|
|
26
|
+
2. **Operations**: restart, logs, stats, redeploy, pull images, upgrade, prune. Straight through the CLI.
|
|
27
27
|
|
|
28
|
-
The tool refuses
|
|
28
|
+
The full `arcane-cli` is available. The tool refuses only `config`, `auth`, `self-update`, `completion`, which would change its own setup.
|
|
29
|
+
|
|
30
|
+
Updating Arcane itself: the `arcane` project is GitOps-managed. Bump the image tag in its Compose file through gitops, push, sync. The sync redeploys the manager that runs the sync; if the manager does not come back, tell the caller to finish with `docker compose pull && docker compose up -d` in `/opt/docker/arcane` on VM 100.
|
|
29
31
|
|
|
30
32
|
## Output
|
|
31
33
|
|
package/specialists/AGENTS.md
CHANGED
|
@@ -38,7 +38,7 @@ Wrappers ship as TypeScript and run under Node 24 type stripping: erasable synta
|
|
|
38
38
|
- Exit codes: `UsageError` 2, `ServiceError` 1, success 0. Diagnostics on stderr, results on stdout, JSON where possible.
|
|
39
39
|
- Output filters: Arcane responses pass `stripKeys(ARCANE_DENIED_KEYS)`; Zitadel responses pass `redactSecrets`. Add keys there rather than in a wrapper.
|
|
40
40
|
- Fixed remote commands: `DMZ_COMMANDS` and `PULSAR_COMMANDS` must match the entry scripts in `entry/remote/` and their tracked copies in the homelab repository (`02-pulsar-proxmox/dmz/system/`, `02-pulsar-proxmox/pulsar/system/`). Change both sides in the same commit and redeploy the remote script.
|
|
41
|
-
-
|
|
41
|
+
- `REFUSED_PREFIXES` protects only the wrapper itself (`config`, `auth`, `self-update`, `completion`). The GitOps-first rule for managed projects lives in the skill and the agent prompt, not in code, because unmanaged projects and emergencies need the direct CLI.
|
|
42
42
|
- Every wrapper answers `--help` without reading config, so agents can discover subcommands before anything else is set up.
|
|
43
43
|
|
|
44
44
|
## Gate
|
package/specialists/README.md
CHANGED
|
@@ -4,7 +4,7 @@ A specialist is a Pi subagent that is the only way to operate one area of the ho
|
|
|
4
4
|
|
|
5
5
|
| Agent | Area | What it can do |
|
|
6
6
|
|---|---|---|
|
|
7
|
-
| `astro.arcane` | Arcane container platform (VM 100) and its GitOps repository | Every `arcane-cli` operation
|
|
7
|
+
| `astro.arcane` | Arcane container platform (VM 100) and its GitOps repository | Every `arcane-cli` operation except its own `config`, `auth`, `self-update`; Compose changes of GitOps-managed projects through `gitops show|write|commit|push`, then sync |
|
|
8
8
|
| `astro.identity` | Zitadel; Pomerium, nginx, lego on VM 104 | Any Zitadel API call (v2, management, admin, auth) with responses redacted; `dmz status|journal|config-template|render|pomerium-restart|nginx-reload|lego-renew` |
|
|
9
9
|
| `astro.network` | UniFi, FreeRADIUS (LXC 108), Tailscale (LXC 105) | Read UniFi sites, devices, clients, networks, firewall policies, WANs; RADIUS and Tailscale checks on Pulsar |
|
|
10
10
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { dirname, join, relative, resolve } from "node:path";
|
|
3
3
|
import { readConfig } from "../lib/config.ts";
|
|
4
4
|
import { ServiceError, UsageError } from "../lib/errors.ts";
|
|
@@ -32,17 +32,8 @@ const SHAPE = {
|
|
|
32
32
|
gitopsBranch: "string",
|
|
33
33
|
} as const;
|
|
34
34
|
|
|
35
|
-
/**
|
|
36
|
-
export const REFUSED_PREFIXES: readonly string[] = [
|
|
37
|
-
"projects create",
|
|
38
|
-
"projects edit",
|
|
39
|
-
"projects update",
|
|
40
|
-
"projects import",
|
|
41
|
-
"config",
|
|
42
|
-
"auth",
|
|
43
|
-
"self-update",
|
|
44
|
-
"completion",
|
|
45
|
-
];
|
|
35
|
+
/** CLI paths that would alter the wrapper's own setup or the binary; everything else passes through. */
|
|
36
|
+
export const REFUSED_PREFIXES: readonly string[] = ["config", "auth", "self-update", "completion"];
|
|
46
37
|
|
|
47
38
|
/** Global arcane-cli flags that take a separate value. */
|
|
48
39
|
const VALUE_FLAGS = ["--env", "--output", "--config", "-c", "--request-timeout", "--log-level", "--limit"];
|
|
@@ -75,13 +66,15 @@ const HELP = `arcane specialist
|
|
|
75
66
|
<arcane-cli args...> run arcane-cli with a fresh federated token; JSON output, sensitive fields removed
|
|
76
67
|
gitops status branch, ahead/behind, changed files of the GitOps checkout
|
|
77
68
|
gitops pull fast-forward the checkout from origin
|
|
69
|
+
gitops ls [dir] list files in the checkout
|
|
70
|
+
gitops show <path> print a file from the checkout
|
|
78
71
|
gitops diff unstaged and staged changes
|
|
79
72
|
gitops log [n] last n commits (default 10)
|
|
80
73
|
gitops write <path> <content> write a file inside the checkout (creates folders)
|
|
81
74
|
gitops commit <message> stage everything and commit
|
|
82
75
|
gitops push push the branch to origin
|
|
83
76
|
|
|
84
|
-
Refused: ${REFUSED_PREFIXES.join(", ")}.
|
|
77
|
+
Refused: ${REFUSED_PREFIXES.join(", ")}. GitOps-managed projects (gitOpsManagedBy set) change through gitops, then sync.`;
|
|
85
78
|
|
|
86
79
|
const TIMEOUT_MS = 240_000;
|
|
87
80
|
|
|
@@ -147,6 +140,20 @@ async function gitops(config: ArcaneConfig, args: string[]): Promise<number> {
|
|
|
147
140
|
case "diff":
|
|
148
141
|
printRaw((await git(dir, ["diff", "HEAD"])) || "(no changes)");
|
|
149
142
|
return 0;
|
|
143
|
+
case "ls": {
|
|
144
|
+
if (rest.length > 1) throw new UsageError("gitops ls [dir]");
|
|
145
|
+
const target = rest[0] === undefined ? dir : safeRepoPath(dir, rest[0]);
|
|
146
|
+
if (!existsSync(target) || !statSync(target).isDirectory()) throw new UsageError(`not a directory in the checkout: ${rest[0] ?? "."}`);
|
|
147
|
+
printJson(readdirSync(target, { withFileTypes: true }).filter((e) => e.name !== ".git").map((e) => (e.isDirectory() ? `${e.name}/` : e.name)).sort());
|
|
148
|
+
return 0;
|
|
149
|
+
}
|
|
150
|
+
case "show": {
|
|
151
|
+
if (rest.length !== 1) throw new UsageError("gitops show <path>");
|
|
152
|
+
const target = safeRepoPath(dir, rest[0]);
|
|
153
|
+
if (!existsSync(target) || !statSync(target).isFile()) throw new UsageError(`not a file in the checkout: ${rest[0]}`);
|
|
154
|
+
printRaw(readFileSync(target, "utf-8"));
|
|
155
|
+
return 0;
|
|
156
|
+
}
|
|
150
157
|
case "log": {
|
|
151
158
|
const n = rest[0] === undefined ? 10 : Number(rest[0]);
|
|
152
159
|
if (!Number.isInteger(n) || n <= 0 || n > 200) throw new UsageError("gitops log [n]: n must be 1..200");
|
|
@@ -211,7 +218,7 @@ async function federatedToken(config: ArcaneConfig): Promise<string> {
|
|
|
211
218
|
|
|
212
219
|
async function passthrough(config: ArcaneConfig, args: string[]): Promise<number> {
|
|
213
220
|
const refused = refusedPrefix(args);
|
|
214
|
-
if (refused) throw new UsageError(`"${refused}" is refused
|
|
221
|
+
if (refused) throw new UsageError(`"${refused}" is refused by the wrapper`);
|
|
215
222
|
const token = await federatedToken(config);
|
|
216
223
|
const wantsJson = args.includes("--json") || args.includes("--output");
|
|
217
224
|
const cliArgs = ["--config", config.arcaneCliConfig, ...(wantsJson ? [] : ["--output", "json"]), "--no-color", ...args];
|
|
@@ -8,9 +8,11 @@ import { UsageError } from "./lib/errors.ts";
|
|
|
8
8
|
import { buildPulsarRemote, parseQuery, unifiPath, command as networkCommand } from "./network/run.ts";
|
|
9
9
|
|
|
10
10
|
describe("arcane wrapper", () => {
|
|
11
|
-
it("refuses
|
|
12
|
-
expect(refusedPrefix(["projects", "create", "x"])).
|
|
11
|
+
it("refuses only the wrapper's own setup paths", () => {
|
|
12
|
+
expect(refusedPrefix(["projects", "create", "x"])).toBeNull();
|
|
13
|
+
expect(refusedPrefix(["projects", "update", "arcane", "--flag"])).toBeNull();
|
|
13
14
|
expect(refusedPrefix(["--env", "0", "auth", "login"])).toBe("auth");
|
|
15
|
+
expect(refusedPrefix(["self-update"])).toBe("self-update");
|
|
14
16
|
expect(refusedPrefix(["config", "set", "api-key", "x"])).toBe("config");
|
|
15
17
|
expect(refusedPrefix(["projects", "list"])).toBeNull();
|
|
16
18
|
expect(refusedPrefix(["projects", "up", "my-app"])).toBeNull();
|
|
@@ -29,6 +31,7 @@ describe("arcane wrapper", () => {
|
|
|
29
31
|
const write = vi.spyOn(process.stdout, "write").mockImplementation(() => true);
|
|
30
32
|
expect(await arcaneCommand(["--help"])).toBe(0);
|
|
31
33
|
expect(String(write.mock.calls[0][0])).toContain("gitops write");
|
|
34
|
+
expect(String(write.mock.calls[0][0])).toContain("gitops show");
|
|
32
35
|
write.mockRestore();
|
|
33
36
|
});
|
|
34
37
|
});
|