specpi 0.27.0 → 0.28.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.
@@ -1,140 +0,0 @@
1
- // Integration seam for specpi-jev-guard, the Jev-scored command gate.
2
- //
3
- // The guard is pinned in the base set but ships **inert**, the way web access ships installed but
4
- // withdrawn. Its own `DEFAULT_SETTINGS.enabled` is `true`, so leaving it alone would mean a fresh
5
- // SpecPi install started gating shell and file calls through a third-party service on day one.
6
- // SpecPi writes `enabled: false` instead, and `/jev guard on` is how a human opts in.
7
- //
8
- // One key for the whole layer: `OPENROUTER_API_KEY`. Jev is published on OpenRouter, the guard
9
- // reaches it that way by default, and the advisor now does too, so the guard's backend is left
10
- // alone rather than re-pinned.
11
- //
12
- // Be precise about what this cannot do. The guard is fail-closed by design: with no key, an
13
- // unreachable endpoint, or a middle-band verdict in a session with no UI, it blocks the call and
14
- // says so. There is no setting that hands the decision back to @gotgenes/pi-permission-system
15
- // instead. So the honest posture is:
16
- //
17
- // - off (the default) -> the guard is not in the tool path at all, and the permission system
18
- // decides every call exactly as it did before this package existed;
19
- // - on -> the guard decides first, asks a human in the middle band when there is a UI, and
20
- // blocks when it cannot reach Jev. An outage stops gated work until it is switched off.
21
- //
22
- // That trade is the user's to make, which is why it ships off and why /jev status says plainly
23
- // what is in force.
24
-
25
- import fs from "node:fs";
26
- import os from "node:os";
27
- import path from "node:path";
28
- import { agentDirectory, regularFile, writeFileAtomic } from "./config.mjs";
29
-
30
- export const GUARD_PACKAGE = "specpi-jev-guard";
31
- export const FALLBACK_PACKAGE = "@gotgenes/pi-permission-system";
32
-
33
- /** Must match the pin in templates/settings.json. */
34
- export const GUARD_PIN = "npm:specpi-jev-guard@0.1.0";
35
-
36
- // The guard reads `<homedir>/.pi/jev-guard.json` globally, and a project copy under `<cwd>/.pi/`
37
- // when the project is trusted. SpecPi writes only the global file: a project-local override is the
38
- // user's to make, and writing one would put a security setting inside whatever repository happened
39
- // to be open at the time.
40
- const CONFIG_DIR_NAME = ".pi";
41
- const SETTINGS_FILE = "jev-guard.json";
42
-
43
- function guardRoot() {
44
- return path.join(agentDirectory(), "npm", "node_modules", GUARD_PACKAGE);
45
- }
46
-
47
- function guardConfigFile() {
48
- return path.join(os.homedir(), CONFIG_DIR_NAME, SETTINGS_FILE);
49
- }
50
-
51
- export function installed() {
52
- try {
53
- const manifest = path.join(guardRoot(), "package.json");
54
- if (!fs.existsSync(manifest)) {
55
- return { installed: false };
56
- }
57
-
58
- const parsed = JSON.parse(fs.readFileSync(manifest, "utf8"));
59
- if (parsed?.name !== GUARD_PACKAGE) {
60
- return { installed: false };
61
- }
62
-
63
- return { installed: true, version: typeof parsed.version === "string" ? parsed.version : "unknown" };
64
- } catch {
65
- return { installed: false };
66
- }
67
- }
68
-
69
- /**
70
- * The fields SpecPi owns, in the guard's own schema. Anything absent here keeps the package's
71
- * default, so its risk rules, protected paths and thresholds stay the package's business.
72
- */
73
- export function desiredConfig(enabled = false) {
74
- return {
75
- // Off means the guard never enters the tool path, so no key is needed and nothing is sent.
76
- enabled: enabled === true,
77
- // Left at the guard's own default. Both halves use OpenRouter, so one OPENROUTER_API_KEY
78
- // serves the guard and the advisor together.
79
- backend: "openrouter",
80
- // With a UI, a middle-band verdict asks rather than deciding on its own. Without one the
81
- // guard fails closed; that is the package's design, disclosed rather than configured away.
82
- uncertain: "ask",
83
- };
84
- }
85
-
86
- export function readConfig() {
87
- try {
88
- const file = guardConfigFile();
89
- if (!regularFile(file, "Jev guard settings")) {
90
- return undefined;
91
- }
92
-
93
- return JSON.parse(fs.readFileSync(file, "utf8"));
94
- } catch {
95
- return undefined;
96
- }
97
- }
98
-
99
- /**
100
- * Merge SpecPi's fields into whatever is already there rather than replacing the file. A user who
101
- * set their own thresholds, safe-command globs or protected paths keeps them; only the three fields
102
- * above are asserted. Idempotent, and reports what changed so a caller can say so.
103
- */
104
- export function applyConfig(enabled = false) {
105
- if (!installed().installed) {
106
- return { applied: false, reason: "not-installed" };
107
- }
108
-
109
- const desired = desiredConfig(enabled);
110
- const current = readConfig();
111
- if (current && Object.entries(desired).every(([key, value]) => current[key] === value)) {
112
- return { applied: false, reason: "already-current" };
113
- }
114
-
115
- writeFileAtomic(guardConfigFile(), `${JSON.stringify({ ...(current ?? {}), ...desired }, null, 4)}\n`);
116
-
117
- return { applied: true, reason: current ? "updated" : "created" };
118
- }
119
-
120
- /** One line for `/jev status` and `specpi doctor`. Never prints a key. */
121
- export function statusLine() {
122
- const state = installed();
123
- if (!state.installed) {
124
- return `guard: not installed (command policy stays with ${FALLBACK_PACKAGE})`;
125
- }
126
-
127
- const config = readConfig();
128
- if (config?.enabled !== true) {
129
- return `guard: ${GUARD_PACKAGE}@${state.version} installed but off (every call goes to ${FALLBACK_PACKAGE})`;
130
- }
131
-
132
- const backend = config.backend === "typesafe" ? "typesafe" : (config.backend ?? "openrouter");
133
- const variable = backend === "typesafe" ? "TYPESAFE_API_KEY" : "OPENROUTER_API_KEY";
134
-
135
- return `guard: ${GUARD_PACKAGE}@${state.version} ON via ${backend} (${variable} ${process.env[variable] ? "present" : "MISSING"}); it decides before ${FALLBACK_PACKAGE} and fails closed when Jev is unreachable`;
136
- }
137
-
138
- export function configPath() {
139
- return guardConfigFile();
140
- }