@pablozrrrr/jevguard 0.2.0 → 0.2.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/README.md +29 -3
- package/cli/main.js +351 -21
- package/index.js +299 -25
- package/package.json +1 -1
- package/skills/jev-init/SKILL.md +4 -3
- package/skills/jev-init/validate-init.js +13 -3
- package/skills/jevguard-rules/validate-rules.js +13 -3
package/README.md
CHANGED
|
@@ -166,13 +166,18 @@ proposal request to a hidden `jevguard-proposer` subagent:
|
|
|
166
166
|
`jevguard-proposer` subagent has wildcard-deny permissions and wildcard-disabled
|
|
167
167
|
tools, so it cannot call a tool, edit a file, or produce a patch. It returns a
|
|
168
168
|
strategy and one manual apply instruction only.
|
|
169
|
+
- `remediation.model` is optional. When set in `.jev/config.yaml`, it selects the
|
|
170
|
+
proposer model; when it is absent, the proposer inherits the host's model.
|
|
169
171
|
- A generic toast reports that the proposal is ready in the child session. It carries
|
|
170
172
|
no rule, task, diff, finding, or credential.
|
|
171
173
|
|
|
172
174
|
The plugin creates at most one proposal per evaluated turn, and the child session is
|
|
173
175
|
excluded from review for the plugin lifetime, so a proposal can never recurse or
|
|
174
176
|
trigger a second one. A host, model, or toast failure is contained and never changes
|
|
175
|
-
the review.
|
|
177
|
+
the review. A contained proposal failure — an invalid model specifier, a child-session
|
|
178
|
+
failure, or a prompt failure — shows a generic error toast and writes one structured
|
|
179
|
+
`error` log entry carrying only a typed reason code, never any task, diff, finding,
|
|
180
|
+
path, or credential.
|
|
176
181
|
|
|
177
182
|
The proposer is a strategy step, not an apply step: it never edits code, and nothing
|
|
178
183
|
is applied automatically. You read the proposal, copy its manual apply instruction
|
|
@@ -187,8 +192,29 @@ or secret is logged.
|
|
|
187
192
|
|
|
188
193
|
## Install
|
|
189
194
|
|
|
190
|
-
|
|
191
|
-
`opencode.json
|
|
195
|
+
Installing `@pablozrrrr/jevguard` does **not** register it with OpenCode by itself.
|
|
196
|
+
OpenCode only loads plugins listed in `plugin: [...]` in `opencode.json`, so register
|
|
197
|
+
the plugin from the installed CLI:
|
|
198
|
+
|
|
199
|
+
```sh
|
|
200
|
+
# global config: ~/.config/opencode/opencode.json
|
|
201
|
+
jevguard install
|
|
202
|
+
|
|
203
|
+
# project config: ./opencode.json
|
|
204
|
+
jevguard install --project
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
The installer asks for confirmation, edits only the `plugin` array, and is
|
|
208
|
+
idempotent. It only edits a `plugin` array of plain strings, so OpenCode's
|
|
209
|
+
`[["pkg", { "options": {} }]]` tuple form is a known limitation: the installer leaves
|
|
210
|
+
the file unchanged and prints the manual snippet instead. If `jevguard` is not on
|
|
211
|
+
`PATH`, run it through `bunx`:
|
|
212
|
+
|
|
213
|
+
```sh
|
|
214
|
+
bunx --package @pablozrrrr/jevguard jevguard install
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
The manual alternative is one entry in `opencode.json`:
|
|
192
218
|
|
|
193
219
|
```json
|
|
194
220
|
{ "plugin": ["@pablozrrrr/jevguard"] }
|
package/cli/main.js
CHANGED
|
@@ -1259,7 +1259,7 @@ var REMEDIATION_MAX_TASK_LENGTH = 16384;
|
|
|
1259
1259
|
var DEFAULT_REMEDIATION_CONFIG = {
|
|
1260
1260
|
autoPropose: true,
|
|
1261
1261
|
proposeOn: ["FAIL"],
|
|
1262
|
-
model:
|
|
1262
|
+
model: null
|
|
1263
1263
|
};
|
|
1264
1264
|
|
|
1265
1265
|
// packages/core/src/remediation/request.ts
|
|
@@ -1349,6 +1349,7 @@ function snapshotRule(rule) {
|
|
|
1349
1349
|
// packages/core/src/remediation/config.ts
|
|
1350
1350
|
var MODEL_PATTERN = /^[^\s/]+\/[^\s/]+$/;
|
|
1351
1351
|
var KNOWN_KEYS = new Set(["auto_propose", "propose_on", "model"]);
|
|
1352
|
+
var INVALID_MODEL = Symbol("invalid-model");
|
|
1352
1353
|
function resolveRemediationConfig(value) {
|
|
1353
1354
|
if (value === null || value === undefined) {
|
|
1354
1355
|
return valid(DEFAULT_REMEDIATION_CONFIG);
|
|
@@ -1371,8 +1372,8 @@ function resolveRemediationConfig(value) {
|
|
|
1371
1372
|
if (proposeOn === null) {
|
|
1372
1373
|
return invalid2();
|
|
1373
1374
|
}
|
|
1374
|
-
const model = remediation.model
|
|
1375
|
-
if (
|
|
1375
|
+
const model = resolveModel(remediation.model);
|
|
1376
|
+
if (model === INVALID_MODEL) {
|
|
1376
1377
|
return invalid2();
|
|
1377
1378
|
}
|
|
1378
1379
|
return valid({ autoPropose, proposeOn, model });
|
|
@@ -1387,6 +1388,15 @@ function parseModelSpecifier(model) {
|
|
|
1387
1388
|
modelID: model.slice(separator + 1)
|
|
1388
1389
|
};
|
|
1389
1390
|
}
|
|
1391
|
+
function resolveModel(value) {
|
|
1392
|
+
if (value === undefined) {
|
|
1393
|
+
return null;
|
|
1394
|
+
}
|
|
1395
|
+
if (typeof value !== "string" || !MODEL_PATTERN.test(value)) {
|
|
1396
|
+
return INVALID_MODEL;
|
|
1397
|
+
}
|
|
1398
|
+
return value;
|
|
1399
|
+
}
|
|
1390
1400
|
function readProposeOn(value) {
|
|
1391
1401
|
if (value === undefined) {
|
|
1392
1402
|
return DEFAULT_REMEDIATION_CONFIG.proposeOn;
|
|
@@ -1695,6 +1705,217 @@ function batchFailed(reason) {
|
|
|
1695
1705
|
function failedFor(request, reason) {
|
|
1696
1706
|
return request.kind === "RULE" ? ruleFailed(reason) : batchFailed(reason);
|
|
1697
1707
|
}
|
|
1708
|
+
// packages/opencode-adapter/src/opencode-config/confirmation.ts
|
|
1709
|
+
import { createInterface } from "readline/promises";
|
|
1710
|
+
function createConfirmationPrompt() {
|
|
1711
|
+
return {
|
|
1712
|
+
confirm: askForConfirmation
|
|
1713
|
+
};
|
|
1714
|
+
}
|
|
1715
|
+
async function askForConfirmation(message) {
|
|
1716
|
+
if (process.stdin.isTTY !== true) {
|
|
1717
|
+
return { status: "FAILED", reason: "NOT_A_TTY" };
|
|
1718
|
+
}
|
|
1719
|
+
try {
|
|
1720
|
+
const readline = createInterface({ input: process.stdin, output: process.stdout });
|
|
1721
|
+
try {
|
|
1722
|
+
const answer = await readline.question(`${message} [y/N] `);
|
|
1723
|
+
return isAffirmative(answer) ? { status: "CONFIRMED" } : { status: "DECLINED" };
|
|
1724
|
+
} finally {
|
|
1725
|
+
readline.close();
|
|
1726
|
+
}
|
|
1727
|
+
} catch {
|
|
1728
|
+
return { status: "DECLINED" };
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
function isAffirmative(answer) {
|
|
1732
|
+
const normalized = answer.trim().toLowerCase();
|
|
1733
|
+
return normalized === "y" || normalized === "yes";
|
|
1734
|
+
}
|
|
1735
|
+
// packages/opencode-adapter/src/opencode-config/entry.ts
|
|
1736
|
+
var JEVGUARD_PLUGIN_ENTRY = "@pablozrrrr/jevguard";
|
|
1737
|
+
var CONFIG_SCHEMA_URL = "https://opencode.ai/config.json";
|
|
1738
|
+
function createDefaultConfigDocument() {
|
|
1739
|
+
return { $schema: CONFIG_SCHEMA_URL };
|
|
1740
|
+
}
|
|
1741
|
+
function addPluginEntry(document, entry) {
|
|
1742
|
+
if (!isPlainObject(document)) {
|
|
1743
|
+
return { status: "INVALID_SHAPE" };
|
|
1744
|
+
}
|
|
1745
|
+
const existing = document.plugin;
|
|
1746
|
+
if (existing === undefined) {
|
|
1747
|
+
return { status: "ADDED", document: { ...document, plugin: [entry] } };
|
|
1748
|
+
}
|
|
1749
|
+
if (!isStringArray(existing)) {
|
|
1750
|
+
return { status: "INVALID_SHAPE" };
|
|
1751
|
+
}
|
|
1752
|
+
if (existing.some((value) => isPluginEntryFor(value, entry))) {
|
|
1753
|
+
return { status: "PRESENT" };
|
|
1754
|
+
}
|
|
1755
|
+
return { status: "ADDED", document: { ...document, plugin: [...existing, entry] } };
|
|
1756
|
+
}
|
|
1757
|
+
function isPlainObject(value) {
|
|
1758
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1759
|
+
}
|
|
1760
|
+
function isStringArray(value) {
|
|
1761
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
1762
|
+
}
|
|
1763
|
+
function isPluginEntryFor(value, entry) {
|
|
1764
|
+
return value === entry || value.startsWith(`${entry}@`);
|
|
1765
|
+
}
|
|
1766
|
+
// packages/opencode-adapter/src/opencode-config/file-system.ts
|
|
1767
|
+
import { randomUUID } from "crypto";
|
|
1768
|
+
import { access, mkdir, readFile, rename, rm, writeFile } from "fs/promises";
|
|
1769
|
+
import { dirname, join } from "path";
|
|
1770
|
+
function createNodeConfigFileSystem() {
|
|
1771
|
+
return {
|
|
1772
|
+
readFile: readOptionalFile,
|
|
1773
|
+
fileExists,
|
|
1774
|
+
writeFile: writeConfigFile,
|
|
1775
|
+
ensureParentDirectory
|
|
1776
|
+
};
|
|
1777
|
+
}
|
|
1778
|
+
async function readOptionalFile(path) {
|
|
1779
|
+
try {
|
|
1780
|
+
return await readFile(path, "utf8");
|
|
1781
|
+
} catch (error) {
|
|
1782
|
+
if (isMissingFileError(error)) {
|
|
1783
|
+
return null;
|
|
1784
|
+
}
|
|
1785
|
+
throw error;
|
|
1786
|
+
}
|
|
1787
|
+
}
|
|
1788
|
+
async function fileExists(path) {
|
|
1789
|
+
try {
|
|
1790
|
+
await access(path);
|
|
1791
|
+
return true;
|
|
1792
|
+
} catch {
|
|
1793
|
+
return false;
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
async function writeConfigFile(path, contents) {
|
|
1797
|
+
const temporaryPath = join(dirname(path), `.opencode.json.jevguard-tmp-${randomUUID()}`);
|
|
1798
|
+
try {
|
|
1799
|
+
await writeFile(temporaryPath, contents, "utf8");
|
|
1800
|
+
await rename(temporaryPath, path);
|
|
1801
|
+
} catch (error) {
|
|
1802
|
+
await rm(temporaryPath, { force: true }).catch(() => {
|
|
1803
|
+
return;
|
|
1804
|
+
});
|
|
1805
|
+
throw error;
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
async function ensureParentDirectory(path) {
|
|
1809
|
+
await mkdir(dirname(path), { recursive: true });
|
|
1810
|
+
}
|
|
1811
|
+
function isMissingFileError(error) {
|
|
1812
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
1813
|
+
}
|
|
1814
|
+
// packages/opencode-adapter/src/opencode-config/install.ts
|
|
1815
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
1816
|
+
|
|
1817
|
+
// packages/opencode-adapter/src/opencode-config/paths.ts
|
|
1818
|
+
import { isAbsolute, join as join2 } from "path";
|
|
1819
|
+
var PROJECT_CONFIG_FILENAME = "opencode.json";
|
|
1820
|
+
function resolveConfigPath(target, context) {
|
|
1821
|
+
if (target === "project") {
|
|
1822
|
+
return join2(context.cwd, PROJECT_CONFIG_FILENAME);
|
|
1823
|
+
}
|
|
1824
|
+
return join2(resolveConfigHome(context), "opencode", PROJECT_CONFIG_FILENAME);
|
|
1825
|
+
}
|
|
1826
|
+
function resolveConfigHome(context) {
|
|
1827
|
+
const configured = context.environment.XDG_CONFIG_HOME;
|
|
1828
|
+
if (configured !== undefined && configured !== "" && isAbsolute(configured)) {
|
|
1829
|
+
return configured;
|
|
1830
|
+
}
|
|
1831
|
+
return join2(context.homedir, ".config");
|
|
1832
|
+
}
|
|
1833
|
+
|
|
1834
|
+
// packages/opencode-adapter/src/opencode-config/install.ts
|
|
1835
|
+
var CONFIRMATION_MESSAGE = "Add JevGuard to the opencode.json plugin list?";
|
|
1836
|
+
async function install(dependencies) {
|
|
1837
|
+
const path = resolveConfigPath(dependencies.target, dependencies);
|
|
1838
|
+
const read = await readConfigDocument(dependencies.fileSystem, path);
|
|
1839
|
+
if (read.status === "FAILED") {
|
|
1840
|
+
return failed(dependencies.target, path, read.reason);
|
|
1841
|
+
}
|
|
1842
|
+
const mutation = addPluginEntry(read.document, JEVGUARD_PLUGIN_ENTRY);
|
|
1843
|
+
if (mutation.status === "INVALID_SHAPE") {
|
|
1844
|
+
return failed(dependencies.target, path, "INVALID_CONFIG_SHAPE");
|
|
1845
|
+
}
|
|
1846
|
+
if (mutation.status === "PRESENT") {
|
|
1847
|
+
return { status: "ALREADY_INSTALLED", target: dependencies.target, path };
|
|
1848
|
+
}
|
|
1849
|
+
const confirmation = await requestConfirmation(dependencies.confirmation);
|
|
1850
|
+
if (confirmation.status === "FAILED") {
|
|
1851
|
+
return failed(dependencies.target, path, confirmation.reason);
|
|
1852
|
+
}
|
|
1853
|
+
if (confirmation.status === "DECLINED") {
|
|
1854
|
+
return { status: "ABORTED", target: dependencies.target, path };
|
|
1855
|
+
}
|
|
1856
|
+
const written = await writeConfigDocument(dependencies.fileSystem, path, mutation.document);
|
|
1857
|
+
if (!written) {
|
|
1858
|
+
return failed(dependencies.target, path, "WRITE_FAILURE");
|
|
1859
|
+
}
|
|
1860
|
+
return { status: "INSTALLED", target: dependencies.target, path };
|
|
1861
|
+
}
|
|
1862
|
+
async function readConfigDocument(fileSystem, path) {
|
|
1863
|
+
const contents = await readOptionalContents(fileSystem, path);
|
|
1864
|
+
if (contents.status === "FAILED") {
|
|
1865
|
+
return contents;
|
|
1866
|
+
}
|
|
1867
|
+
if (contents.value === null) {
|
|
1868
|
+
return readMissingConfigDocument(fileSystem, path);
|
|
1869
|
+
}
|
|
1870
|
+
try {
|
|
1871
|
+
return { status: "READ", document: JSON.parse(contents.value) };
|
|
1872
|
+
} catch {
|
|
1873
|
+
return { status: "FAILED", reason: "INVALID_JSON" };
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
async function readOptionalContents(fileSystem, path) {
|
|
1877
|
+
try {
|
|
1878
|
+
return { status: "READ", value: await fileSystem.readFile(path) };
|
|
1879
|
+
} catch {
|
|
1880
|
+
return { status: "FAILED", reason: "READ_FAILURE" };
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
async function readMissingConfigDocument(fileSystem, path) {
|
|
1884
|
+
try {
|
|
1885
|
+
const hasJsonc = await fileSystem.fileExists(join3(dirname2(path), "opencode.jsonc"));
|
|
1886
|
+
if (hasJsonc) {
|
|
1887
|
+
return { status: "FAILED", reason: "UNSUPPORTED_JSONC" };
|
|
1888
|
+
}
|
|
1889
|
+
return { status: "READ", document: createDefaultConfigDocument() };
|
|
1890
|
+
} catch {
|
|
1891
|
+
return { status: "FAILED", reason: "READ_FAILURE" };
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
async function requestConfirmation(confirmation) {
|
|
1895
|
+
try {
|
|
1896
|
+
const result = await confirmation.confirm(CONFIRMATION_MESSAGE);
|
|
1897
|
+
if (result.status === "FAILED") {
|
|
1898
|
+
return { status: "FAILED", reason: result.reason };
|
|
1899
|
+
}
|
|
1900
|
+
return { status: result.status };
|
|
1901
|
+
} catch {
|
|
1902
|
+
return { status: "FAILED", reason: "NOT_A_TTY" };
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
async function writeConfigDocument(fileSystem, path, document) {
|
|
1906
|
+
const serialized = `${JSON.stringify(document, null, 2)}
|
|
1907
|
+
`;
|
|
1908
|
+
try {
|
|
1909
|
+
await fileSystem.ensureParentDirectory(path);
|
|
1910
|
+
await fileSystem.writeFile(path, serialized);
|
|
1911
|
+
return true;
|
|
1912
|
+
} catch {
|
|
1913
|
+
return false;
|
|
1914
|
+
}
|
|
1915
|
+
}
|
|
1916
|
+
function failed(target, path, reason) {
|
|
1917
|
+
return { status: "FAILED", target, path, reason };
|
|
1918
|
+
}
|
|
1698
1919
|
// packages/opencode-adapter/src/policy/config-yaml.ts
|
|
1699
1920
|
import { parseDocument, visit } from "yaml";
|
|
1700
1921
|
function parsePolicyConfig(text) {
|
|
@@ -1731,16 +1952,16 @@ function invalid3() {
|
|
|
1731
1952
|
return { status: "INVALID", reason: "INVALID_CONFIG" };
|
|
1732
1953
|
}
|
|
1733
1954
|
// packages/opencode-adapter/src/policy/file-source.ts
|
|
1734
|
-
import { readFile } from "fs/promises";
|
|
1735
|
-
import { join } from "path";
|
|
1955
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
1956
|
+
import { join as join4 } from "path";
|
|
1736
1957
|
function createPolicyFileLoader(dependencies) {
|
|
1737
1958
|
return {
|
|
1738
1959
|
async load() {
|
|
1739
|
-
const rules = await
|
|
1960
|
+
const rules = await readOptionalFile2(dependencies.fileSystem, join4(dependencies.root, ".jev", "rules.md"));
|
|
1740
1961
|
if (rules.status === "FAILED") {
|
|
1741
1962
|
return { status: "FAILED", reason: "RULES_READ_FAILURE" };
|
|
1742
1963
|
}
|
|
1743
|
-
const config = await
|
|
1964
|
+
const config = await readOptionalFile2(dependencies.fileSystem, join4(dependencies.root, ".jev", "config.yaml"));
|
|
1744
1965
|
if (config.status === "FAILED") {
|
|
1745
1966
|
return { status: "FAILED", reason: "CONFIG_READ_FAILURE" };
|
|
1746
1967
|
}
|
|
@@ -1753,20 +1974,20 @@ function createPolicyFileLoader(dependencies) {
|
|
|
1753
1974
|
}
|
|
1754
1975
|
function createNodePolicyFileSystem() {
|
|
1755
1976
|
return {
|
|
1756
|
-
readFile: (path) =>
|
|
1977
|
+
readFile: (path) => readFile2(path, "utf8")
|
|
1757
1978
|
};
|
|
1758
1979
|
}
|
|
1759
|
-
async function
|
|
1980
|
+
async function readOptionalFile2(fileSystem, path) {
|
|
1760
1981
|
try {
|
|
1761
1982
|
return { status: "READ", value: await fileSystem.readFile(path) };
|
|
1762
1983
|
} catch (error) {
|
|
1763
|
-
if (
|
|
1984
|
+
if (isMissingFileError2(error)) {
|
|
1764
1985
|
return { status: "READ", value: null };
|
|
1765
1986
|
}
|
|
1766
1987
|
return { status: "FAILED" };
|
|
1767
1988
|
}
|
|
1768
1989
|
}
|
|
1769
|
-
function
|
|
1990
|
+
function isMissingFileError2(error) {
|
|
1770
1991
|
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
1771
1992
|
}
|
|
1772
1993
|
// packages/opencode-adapter/src/presentation/display.ts
|
|
@@ -1996,25 +2217,50 @@ function toOutcomeExtra(result) {
|
|
|
1996
2217
|
}
|
|
1997
2218
|
// packages/opencode-adapter/src/presentation/remediation.ts
|
|
1998
2219
|
var TITLE = "JevGuard";
|
|
2220
|
+
var SERVICE_NAME2 = "jevguard";
|
|
1999
2221
|
var PREPARING_MESSAGE = "JevGuard is preparing a remediation proposal in the child session.";
|
|
2000
2222
|
var READY_MESSAGE = "JevGuard remediation proposal ready.";
|
|
2223
|
+
var FAILED_MESSAGE = "JevGuard could not prepare a remediation proposal.";
|
|
2224
|
+
var FAILURE_LOG_MESSAGE = "JevGuard remediation proposal failed";
|
|
2001
2225
|
function createRemediationNotifier(toast) {
|
|
2002
2226
|
return {
|
|
2003
2227
|
async proposalPreparing() {
|
|
2004
|
-
return showRemediationToast(toast, PREPARING_MESSAGE);
|
|
2228
|
+
return showRemediationToast(toast, PREPARING_MESSAGE, "info");
|
|
2005
2229
|
},
|
|
2006
2230
|
async proposalReady() {
|
|
2007
|
-
return showRemediationToast(toast, READY_MESSAGE);
|
|
2231
|
+
return showRemediationToast(toast, READY_MESSAGE, "info");
|
|
2232
|
+
},
|
|
2233
|
+
async proposalFailed() {
|
|
2234
|
+
return showRemediationToast(toast, FAILED_MESSAGE, "error");
|
|
2008
2235
|
}
|
|
2009
2236
|
};
|
|
2010
2237
|
}
|
|
2011
|
-
async function showRemediationToast(toast, message) {
|
|
2238
|
+
async function showRemediationToast(toast, message, variant) {
|
|
2012
2239
|
try {
|
|
2013
|
-
return await toast.show({ title: TITLE, message, variant
|
|
2240
|
+
return await toast.show({ title: TITLE, message, variant });
|
|
2014
2241
|
} catch {
|
|
2015
2242
|
return "FAILED";
|
|
2016
2243
|
}
|
|
2017
2244
|
}
|
|
2245
|
+
function createRemediationFailureLogSink(client) {
|
|
2246
|
+
return {
|
|
2247
|
+
async write(reason) {
|
|
2248
|
+
try {
|
|
2249
|
+
const result = await client.app.log({
|
|
2250
|
+
body: {
|
|
2251
|
+
service: SERVICE_NAME2,
|
|
2252
|
+
level: "error",
|
|
2253
|
+
message: FAILURE_LOG_MESSAGE,
|
|
2254
|
+
extra: { reason }
|
|
2255
|
+
}
|
|
2256
|
+
});
|
|
2257
|
+
return result.error === undefined ? "DELIVERED" : "FAILED";
|
|
2258
|
+
} catch {
|
|
2259
|
+
return "FAILED";
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
};
|
|
2263
|
+
}
|
|
2018
2264
|
function createOpenCodeSessionNavigator(client) {
|
|
2019
2265
|
return {
|
|
2020
2266
|
async navigate(sessionID) {
|
|
@@ -2065,11 +2311,12 @@ function createOpenCodeProposalSessionFacade(client) {
|
|
|
2065
2311
|
return result.data.id;
|
|
2066
2312
|
},
|
|
2067
2313
|
async prompt(input) {
|
|
2314
|
+
const model = input.model;
|
|
2068
2315
|
const result = await client.session.prompt({
|
|
2069
2316
|
path: { id: input.sessionID },
|
|
2070
2317
|
body: {
|
|
2071
2318
|
agent: input.agent,
|
|
2072
|
-
model: { providerID:
|
|
2319
|
+
...model === null ? {} : { model: { providerID: model.providerID, modelID: model.modelID } },
|
|
2073
2320
|
system: input.system,
|
|
2074
2321
|
tools: { ...input.tools },
|
|
2075
2322
|
parts: [{ type: "text", text: input.text }]
|
|
@@ -2081,15 +2328,37 @@ function createOpenCodeProposalSessionFacade(client) {
|
|
|
2081
2328
|
}
|
|
2082
2329
|
};
|
|
2083
2330
|
}
|
|
2331
|
+
// packages/plugin/src/cli/install.ts
|
|
2332
|
+
import { homedir } from "os";
|
|
2333
|
+
function performInstall(target) {
|
|
2334
|
+
return install({
|
|
2335
|
+
confirmation: createConfirmationPrompt(),
|
|
2336
|
+
fileSystem: createNodeConfigFileSystem(),
|
|
2337
|
+
target,
|
|
2338
|
+
cwd: process.cwd(),
|
|
2339
|
+
environment: process.env,
|
|
2340
|
+
homedir: homedir()
|
|
2341
|
+
});
|
|
2342
|
+
}
|
|
2343
|
+
|
|
2084
2344
|
// packages/plugin/src/cli/login.ts
|
|
2085
2345
|
function performLogin() {
|
|
2086
2346
|
return login({ prompt: createSecretPrompt(), store: createKeyringCredentialStore() });
|
|
2087
2347
|
}
|
|
2088
2348
|
|
|
2089
2349
|
// packages/plugin/src/cli/messages.ts
|
|
2090
|
-
var USAGE =
|
|
2350
|
+
var USAGE = [
|
|
2351
|
+
"Usage: jevguard login",
|
|
2352
|
+
" jevguard install [--project]",
|
|
2353
|
+
"",
|
|
2354
|
+
"`jev-init` and `jevguard-rules` are OpenCode skills, not CLI commands. Register",
|
|
2355
|
+
"the plugin in opencode.json (or run `jevguard install`) and invoke them from",
|
|
2356
|
+
"OpenCode."
|
|
2357
|
+
].join(`
|
|
2358
|
+
`);
|
|
2091
2359
|
var LOGIN_SAVED_MESSAGE = "Credential saved to the OS credential store.";
|
|
2092
2360
|
var LOGIN_ABORTED_MESSAGE = "Login cancelled. No credential was saved.";
|
|
2361
|
+
var MANUAL_INSTALL_SNIPPET = `{ "plugin": ["${JEVGUARD_PLUGIN_ENTRY}"] }`;
|
|
2093
2362
|
function describeLoginFailure(reason) {
|
|
2094
2363
|
switch (reason) {
|
|
2095
2364
|
case "NOT_A_TTY":
|
|
@@ -2102,13 +2371,51 @@ function describeLoginFailure(reason) {
|
|
|
2102
2371
|
return "Could not save the credential to the OS credential store. Check that a credential store is available and try again.";
|
|
2103
2372
|
}
|
|
2104
2373
|
}
|
|
2374
|
+
function describeInstallInstalled(target, path) {
|
|
2375
|
+
return `Registered JevGuard for the ${target} target in ${path}. Restart OpenCode to load it.`;
|
|
2376
|
+
}
|
|
2377
|
+
function describeInstallAlreadyInstalled(target, path) {
|
|
2378
|
+
return `JevGuard is already registered for the ${target} target in ${path}.`;
|
|
2379
|
+
}
|
|
2380
|
+
function describeInstallAborted(path) {
|
|
2381
|
+
return `Install cancelled. ${path} was not changed.`;
|
|
2382
|
+
}
|
|
2383
|
+
function describeInstallFailure(reason, path) {
|
|
2384
|
+
switch (reason) {
|
|
2385
|
+
case "NOT_A_TTY":
|
|
2386
|
+
return manualSnippetFailure(`${path} cannot be confirmed without an interactive terminal.`);
|
|
2387
|
+
case "UNSUPPORTED_JSONC":
|
|
2388
|
+
return manualSnippetFailure(`${path} is absent, but a sibling opencode.jsonc exists. JevGuard never edits .jsonc files.`);
|
|
2389
|
+
case "INVALID_JSON":
|
|
2390
|
+
return manualSnippetFailure(`Could not parse ${path} as JSON.`);
|
|
2391
|
+
case "INVALID_CONFIG_SHAPE":
|
|
2392
|
+
return manualSnippetFailure(`The plugin entry in ${path} is not an array of strings, so JevGuard did not change it.`);
|
|
2393
|
+
case "READ_FAILURE":
|
|
2394
|
+
return `Could not read ${path}. Check the file permissions and try again.`;
|
|
2395
|
+
case "WRITE_FAILURE":
|
|
2396
|
+
return `Could not write ${path}. Check the file permissions and try again.`;
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
function manualSnippetFailure(message) {
|
|
2400
|
+
return [message, "Add this to the plugin array manually:", MANUAL_INSTALL_SNIPPET].join(`
|
|
2401
|
+
`);
|
|
2402
|
+
}
|
|
2105
2403
|
|
|
2106
2404
|
// packages/plugin/src/cli/run-cli.ts
|
|
2107
2405
|
async function runCli(argv, io, dependencies) {
|
|
2108
|
-
if (argv.length
|
|
2109
|
-
io
|
|
2110
|
-
|
|
2406
|
+
if (argv.length === 1 && argv[0] === "login") {
|
|
2407
|
+
return runLogin(io, dependencies);
|
|
2408
|
+
}
|
|
2409
|
+
if (argv.length === 1 && argv[0] === "install") {
|
|
2410
|
+
return runInstall(io, dependencies, "global");
|
|
2411
|
+
}
|
|
2412
|
+
if (argv.length === 2 && argv[0] === "install" && argv[1] === "--project") {
|
|
2413
|
+
return runInstall(io, dependencies, "project");
|
|
2111
2414
|
}
|
|
2415
|
+
io.writeErr(USAGE);
|
|
2416
|
+
return 1;
|
|
2417
|
+
}
|
|
2418
|
+
async function runLogin(io, dependencies) {
|
|
2112
2419
|
const outcome = await dependencies.login();
|
|
2113
2420
|
if (outcome.status === "SAVED") {
|
|
2114
2421
|
io.writeOut(LOGIN_SAVED_MESSAGE);
|
|
@@ -2121,6 +2428,26 @@ async function runCli(argv, io, dependencies) {
|
|
|
2121
2428
|
io.writeErr(describeLoginFailure(outcome.reason));
|
|
2122
2429
|
return 1;
|
|
2123
2430
|
}
|
|
2431
|
+
async function runInstall(io, dependencies, target) {
|
|
2432
|
+
const outcome = await dependencies.install(target);
|
|
2433
|
+
return reportInstall(io, outcome);
|
|
2434
|
+
}
|
|
2435
|
+
function reportInstall(io, outcome) {
|
|
2436
|
+
if (outcome.status === "INSTALLED") {
|
|
2437
|
+
io.writeOut(describeInstallInstalled(outcome.target, outcome.path));
|
|
2438
|
+
return 0;
|
|
2439
|
+
}
|
|
2440
|
+
if (outcome.status === "ALREADY_INSTALLED") {
|
|
2441
|
+
io.writeOut(describeInstallAlreadyInstalled(outcome.target, outcome.path));
|
|
2442
|
+
return 0;
|
|
2443
|
+
}
|
|
2444
|
+
if (outcome.status === "ABORTED") {
|
|
2445
|
+
io.writeErr(describeInstallAborted(outcome.path));
|
|
2446
|
+
return 1;
|
|
2447
|
+
}
|
|
2448
|
+
io.writeErr(describeInstallFailure(outcome.reason, outcome.path));
|
|
2449
|
+
return 1;
|
|
2450
|
+
}
|
|
2124
2451
|
|
|
2125
2452
|
// packages/plugin/src/cli/main.ts
|
|
2126
2453
|
var io = {
|
|
@@ -2129,5 +2456,8 @@ var io = {
|
|
|
2129
2456
|
writeErr: (message) => process.stderr.write(`${message}
|
|
2130
2457
|
`)
|
|
2131
2458
|
};
|
|
2132
|
-
var exitCode = await runCli(process.argv.slice(2), io, {
|
|
2459
|
+
var exitCode = await runCli(process.argv.slice(2), io, {
|
|
2460
|
+
login: performLogin,
|
|
2461
|
+
install: performInstall
|
|
2462
|
+
});
|
|
2133
2463
|
process.exitCode = exitCode;
|
package/index.js
CHANGED
|
@@ -906,7 +906,7 @@ var REMEDIATION_MAX_TASK_LENGTH = 16384;
|
|
|
906
906
|
var DEFAULT_REMEDIATION_CONFIG = {
|
|
907
907
|
autoPropose: true,
|
|
908
908
|
proposeOn: ["FAIL"],
|
|
909
|
-
model:
|
|
909
|
+
model: null
|
|
910
910
|
};
|
|
911
911
|
|
|
912
912
|
// packages/core/src/remediation/request.ts
|
|
@@ -996,6 +996,7 @@ function snapshotRule(rule) {
|
|
|
996
996
|
// packages/core/src/remediation/config.ts
|
|
997
997
|
var MODEL_PATTERN = /^[^\s/]+\/[^\s/]+$/;
|
|
998
998
|
var KNOWN_KEYS = new Set(["auto_propose", "propose_on", "model"]);
|
|
999
|
+
var INVALID_MODEL = Symbol("invalid-model");
|
|
999
1000
|
function resolveRemediationConfig(value) {
|
|
1000
1001
|
if (value === null || value === undefined) {
|
|
1001
1002
|
return valid(DEFAULT_REMEDIATION_CONFIG);
|
|
@@ -1018,8 +1019,8 @@ function resolveRemediationConfig(value) {
|
|
|
1018
1019
|
if (proposeOn === null) {
|
|
1019
1020
|
return invalid2();
|
|
1020
1021
|
}
|
|
1021
|
-
const model = remediation.model
|
|
1022
|
-
if (
|
|
1022
|
+
const model = resolveModel(remediation.model);
|
|
1023
|
+
if (model === INVALID_MODEL) {
|
|
1023
1024
|
return invalid2();
|
|
1024
1025
|
}
|
|
1025
1026
|
return valid({ autoPropose, proposeOn, model });
|
|
@@ -1034,6 +1035,15 @@ function parseModelSpecifier(model) {
|
|
|
1034
1035
|
modelID: model.slice(separator + 1)
|
|
1035
1036
|
};
|
|
1036
1037
|
}
|
|
1038
|
+
function resolveModel(value) {
|
|
1039
|
+
if (value === undefined) {
|
|
1040
|
+
return null;
|
|
1041
|
+
}
|
|
1042
|
+
if (typeof value !== "string" || !MODEL_PATTERN.test(value)) {
|
|
1043
|
+
return INVALID_MODEL;
|
|
1044
|
+
}
|
|
1045
|
+
return value;
|
|
1046
|
+
}
|
|
1037
1047
|
function readProposeOn(value) {
|
|
1038
1048
|
if (value === undefined) {
|
|
1039
1049
|
return DEFAULT_REMEDIATION_CONFIG.proposeOn;
|
|
@@ -1693,6 +1703,217 @@ function batchFailed(reason) {
|
|
|
1693
1703
|
function failedFor(request, reason) {
|
|
1694
1704
|
return request.kind === "RULE" ? ruleFailed(reason) : batchFailed(reason);
|
|
1695
1705
|
}
|
|
1706
|
+
// packages/opencode-adapter/src/opencode-config/confirmation.ts
|
|
1707
|
+
import { createInterface } from "readline/promises";
|
|
1708
|
+
function createConfirmationPrompt() {
|
|
1709
|
+
return {
|
|
1710
|
+
confirm: askForConfirmation
|
|
1711
|
+
};
|
|
1712
|
+
}
|
|
1713
|
+
async function askForConfirmation(message) {
|
|
1714
|
+
if (process.stdin.isTTY !== true) {
|
|
1715
|
+
return { status: "FAILED", reason: "NOT_A_TTY" };
|
|
1716
|
+
}
|
|
1717
|
+
try {
|
|
1718
|
+
const readline = createInterface({ input: process.stdin, output: process.stdout });
|
|
1719
|
+
try {
|
|
1720
|
+
const answer = await readline.question(`${message} [y/N] `);
|
|
1721
|
+
return isAffirmative(answer) ? { status: "CONFIRMED" } : { status: "DECLINED" };
|
|
1722
|
+
} finally {
|
|
1723
|
+
readline.close();
|
|
1724
|
+
}
|
|
1725
|
+
} catch {
|
|
1726
|
+
return { status: "DECLINED" };
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
function isAffirmative(answer) {
|
|
1730
|
+
const normalized = answer.trim().toLowerCase();
|
|
1731
|
+
return normalized === "y" || normalized === "yes";
|
|
1732
|
+
}
|
|
1733
|
+
// packages/opencode-adapter/src/opencode-config/entry.ts
|
|
1734
|
+
var JEVGUARD_PLUGIN_ENTRY = "@pablozrrrr/jevguard";
|
|
1735
|
+
var CONFIG_SCHEMA_URL = "https://opencode.ai/config.json";
|
|
1736
|
+
function createDefaultConfigDocument() {
|
|
1737
|
+
return { $schema: CONFIG_SCHEMA_URL };
|
|
1738
|
+
}
|
|
1739
|
+
function addPluginEntry(document, entry) {
|
|
1740
|
+
if (!isPlainObject(document)) {
|
|
1741
|
+
return { status: "INVALID_SHAPE" };
|
|
1742
|
+
}
|
|
1743
|
+
const existing = document.plugin;
|
|
1744
|
+
if (existing === undefined) {
|
|
1745
|
+
return { status: "ADDED", document: { ...document, plugin: [entry] } };
|
|
1746
|
+
}
|
|
1747
|
+
if (!isStringArray(existing)) {
|
|
1748
|
+
return { status: "INVALID_SHAPE" };
|
|
1749
|
+
}
|
|
1750
|
+
if (existing.some((value) => isPluginEntryFor(value, entry))) {
|
|
1751
|
+
return { status: "PRESENT" };
|
|
1752
|
+
}
|
|
1753
|
+
return { status: "ADDED", document: { ...document, plugin: [...existing, entry] } };
|
|
1754
|
+
}
|
|
1755
|
+
function isPlainObject(value) {
|
|
1756
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
1757
|
+
}
|
|
1758
|
+
function isStringArray(value) {
|
|
1759
|
+
return Array.isArray(value) && value.every((item) => typeof item === "string");
|
|
1760
|
+
}
|
|
1761
|
+
function isPluginEntryFor(value, entry) {
|
|
1762
|
+
return value === entry || value.startsWith(`${entry}@`);
|
|
1763
|
+
}
|
|
1764
|
+
// packages/opencode-adapter/src/opencode-config/file-system.ts
|
|
1765
|
+
import { randomUUID } from "crypto";
|
|
1766
|
+
import { access, mkdir, readFile, rename, rm, writeFile } from "fs/promises";
|
|
1767
|
+
import { dirname, join } from "path";
|
|
1768
|
+
function createNodeConfigFileSystem() {
|
|
1769
|
+
return {
|
|
1770
|
+
readFile: readOptionalFile,
|
|
1771
|
+
fileExists,
|
|
1772
|
+
writeFile: writeConfigFile,
|
|
1773
|
+
ensureParentDirectory
|
|
1774
|
+
};
|
|
1775
|
+
}
|
|
1776
|
+
async function readOptionalFile(path) {
|
|
1777
|
+
try {
|
|
1778
|
+
return await readFile(path, "utf8");
|
|
1779
|
+
} catch (error) {
|
|
1780
|
+
if (isMissingFileError(error)) {
|
|
1781
|
+
return null;
|
|
1782
|
+
}
|
|
1783
|
+
throw error;
|
|
1784
|
+
}
|
|
1785
|
+
}
|
|
1786
|
+
async function fileExists(path) {
|
|
1787
|
+
try {
|
|
1788
|
+
await access(path);
|
|
1789
|
+
return true;
|
|
1790
|
+
} catch {
|
|
1791
|
+
return false;
|
|
1792
|
+
}
|
|
1793
|
+
}
|
|
1794
|
+
async function writeConfigFile(path, contents) {
|
|
1795
|
+
const temporaryPath = join(dirname(path), `.opencode.json.jevguard-tmp-${randomUUID()}`);
|
|
1796
|
+
try {
|
|
1797
|
+
await writeFile(temporaryPath, contents, "utf8");
|
|
1798
|
+
await rename(temporaryPath, path);
|
|
1799
|
+
} catch (error) {
|
|
1800
|
+
await rm(temporaryPath, { force: true }).catch(() => {
|
|
1801
|
+
return;
|
|
1802
|
+
});
|
|
1803
|
+
throw error;
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
async function ensureParentDirectory(path) {
|
|
1807
|
+
await mkdir(dirname(path), { recursive: true });
|
|
1808
|
+
}
|
|
1809
|
+
function isMissingFileError(error) {
|
|
1810
|
+
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
1811
|
+
}
|
|
1812
|
+
// packages/opencode-adapter/src/opencode-config/install.ts
|
|
1813
|
+
import { dirname as dirname2, join as join3 } from "path";
|
|
1814
|
+
|
|
1815
|
+
// packages/opencode-adapter/src/opencode-config/paths.ts
|
|
1816
|
+
import { isAbsolute, join as join2 } from "path";
|
|
1817
|
+
var PROJECT_CONFIG_FILENAME = "opencode.json";
|
|
1818
|
+
function resolveConfigPath(target, context) {
|
|
1819
|
+
if (target === "project") {
|
|
1820
|
+
return join2(context.cwd, PROJECT_CONFIG_FILENAME);
|
|
1821
|
+
}
|
|
1822
|
+
return join2(resolveConfigHome(context), "opencode", PROJECT_CONFIG_FILENAME);
|
|
1823
|
+
}
|
|
1824
|
+
function resolveConfigHome(context) {
|
|
1825
|
+
const configured = context.environment.XDG_CONFIG_HOME;
|
|
1826
|
+
if (configured !== undefined && configured !== "" && isAbsolute(configured)) {
|
|
1827
|
+
return configured;
|
|
1828
|
+
}
|
|
1829
|
+
return join2(context.homedir, ".config");
|
|
1830
|
+
}
|
|
1831
|
+
|
|
1832
|
+
// packages/opencode-adapter/src/opencode-config/install.ts
|
|
1833
|
+
var CONFIRMATION_MESSAGE = "Add JevGuard to the opencode.json plugin list?";
|
|
1834
|
+
async function install(dependencies) {
|
|
1835
|
+
const path = resolveConfigPath(dependencies.target, dependencies);
|
|
1836
|
+
const read = await readConfigDocument(dependencies.fileSystem, path);
|
|
1837
|
+
if (read.status === "FAILED") {
|
|
1838
|
+
return failed(dependencies.target, path, read.reason);
|
|
1839
|
+
}
|
|
1840
|
+
const mutation = addPluginEntry(read.document, JEVGUARD_PLUGIN_ENTRY);
|
|
1841
|
+
if (mutation.status === "INVALID_SHAPE") {
|
|
1842
|
+
return failed(dependencies.target, path, "INVALID_CONFIG_SHAPE");
|
|
1843
|
+
}
|
|
1844
|
+
if (mutation.status === "PRESENT") {
|
|
1845
|
+
return { status: "ALREADY_INSTALLED", target: dependencies.target, path };
|
|
1846
|
+
}
|
|
1847
|
+
const confirmation = await requestConfirmation(dependencies.confirmation);
|
|
1848
|
+
if (confirmation.status === "FAILED") {
|
|
1849
|
+
return failed(dependencies.target, path, confirmation.reason);
|
|
1850
|
+
}
|
|
1851
|
+
if (confirmation.status === "DECLINED") {
|
|
1852
|
+
return { status: "ABORTED", target: dependencies.target, path };
|
|
1853
|
+
}
|
|
1854
|
+
const written = await writeConfigDocument(dependencies.fileSystem, path, mutation.document);
|
|
1855
|
+
if (!written) {
|
|
1856
|
+
return failed(dependencies.target, path, "WRITE_FAILURE");
|
|
1857
|
+
}
|
|
1858
|
+
return { status: "INSTALLED", target: dependencies.target, path };
|
|
1859
|
+
}
|
|
1860
|
+
async function readConfigDocument(fileSystem, path) {
|
|
1861
|
+
const contents = await readOptionalContents(fileSystem, path);
|
|
1862
|
+
if (contents.status === "FAILED") {
|
|
1863
|
+
return contents;
|
|
1864
|
+
}
|
|
1865
|
+
if (contents.value === null) {
|
|
1866
|
+
return readMissingConfigDocument(fileSystem, path);
|
|
1867
|
+
}
|
|
1868
|
+
try {
|
|
1869
|
+
return { status: "READ", document: JSON.parse(contents.value) };
|
|
1870
|
+
} catch {
|
|
1871
|
+
return { status: "FAILED", reason: "INVALID_JSON" };
|
|
1872
|
+
}
|
|
1873
|
+
}
|
|
1874
|
+
async function readOptionalContents(fileSystem, path) {
|
|
1875
|
+
try {
|
|
1876
|
+
return { status: "READ", value: await fileSystem.readFile(path) };
|
|
1877
|
+
} catch {
|
|
1878
|
+
return { status: "FAILED", reason: "READ_FAILURE" };
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
async function readMissingConfigDocument(fileSystem, path) {
|
|
1882
|
+
try {
|
|
1883
|
+
const hasJsonc = await fileSystem.fileExists(join3(dirname2(path), "opencode.jsonc"));
|
|
1884
|
+
if (hasJsonc) {
|
|
1885
|
+
return { status: "FAILED", reason: "UNSUPPORTED_JSONC" };
|
|
1886
|
+
}
|
|
1887
|
+
return { status: "READ", document: createDefaultConfigDocument() };
|
|
1888
|
+
} catch {
|
|
1889
|
+
return { status: "FAILED", reason: "READ_FAILURE" };
|
|
1890
|
+
}
|
|
1891
|
+
}
|
|
1892
|
+
async function requestConfirmation(confirmation) {
|
|
1893
|
+
try {
|
|
1894
|
+
const result = await confirmation.confirm(CONFIRMATION_MESSAGE);
|
|
1895
|
+
if (result.status === "FAILED") {
|
|
1896
|
+
return { status: "FAILED", reason: result.reason };
|
|
1897
|
+
}
|
|
1898
|
+
return { status: result.status };
|
|
1899
|
+
} catch {
|
|
1900
|
+
return { status: "FAILED", reason: "NOT_A_TTY" };
|
|
1901
|
+
}
|
|
1902
|
+
}
|
|
1903
|
+
async function writeConfigDocument(fileSystem, path, document) {
|
|
1904
|
+
const serialized = `${JSON.stringify(document, null, 2)}
|
|
1905
|
+
`;
|
|
1906
|
+
try {
|
|
1907
|
+
await fileSystem.ensureParentDirectory(path);
|
|
1908
|
+
await fileSystem.writeFile(path, serialized);
|
|
1909
|
+
return true;
|
|
1910
|
+
} catch {
|
|
1911
|
+
return false;
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
function failed(target, path, reason) {
|
|
1915
|
+
return { status: "FAILED", target, path, reason };
|
|
1916
|
+
}
|
|
1696
1917
|
// packages/opencode-adapter/src/policy/config-yaml.ts
|
|
1697
1918
|
import { parseDocument, visit } from "yaml";
|
|
1698
1919
|
function parsePolicyConfig(text) {
|
|
@@ -1729,16 +1950,16 @@ function invalid3() {
|
|
|
1729
1950
|
return { status: "INVALID", reason: "INVALID_CONFIG" };
|
|
1730
1951
|
}
|
|
1731
1952
|
// packages/opencode-adapter/src/policy/file-source.ts
|
|
1732
|
-
import { readFile } from "fs/promises";
|
|
1733
|
-
import { join } from "path";
|
|
1953
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
1954
|
+
import { join as join4 } from "path";
|
|
1734
1955
|
function createPolicyFileLoader(dependencies) {
|
|
1735
1956
|
return {
|
|
1736
1957
|
async load() {
|
|
1737
|
-
const rules = await
|
|
1958
|
+
const rules = await readOptionalFile2(dependencies.fileSystem, join4(dependencies.root, ".jev", "rules.md"));
|
|
1738
1959
|
if (rules.status === "FAILED") {
|
|
1739
1960
|
return { status: "FAILED", reason: "RULES_READ_FAILURE" };
|
|
1740
1961
|
}
|
|
1741
|
-
const config = await
|
|
1962
|
+
const config = await readOptionalFile2(dependencies.fileSystem, join4(dependencies.root, ".jev", "config.yaml"));
|
|
1742
1963
|
if (config.status === "FAILED") {
|
|
1743
1964
|
return { status: "FAILED", reason: "CONFIG_READ_FAILURE" };
|
|
1744
1965
|
}
|
|
@@ -1751,20 +1972,20 @@ function createPolicyFileLoader(dependencies) {
|
|
|
1751
1972
|
}
|
|
1752
1973
|
function createNodePolicyFileSystem() {
|
|
1753
1974
|
return {
|
|
1754
|
-
readFile: (path) =>
|
|
1975
|
+
readFile: (path) => readFile2(path, "utf8")
|
|
1755
1976
|
};
|
|
1756
1977
|
}
|
|
1757
|
-
async function
|
|
1978
|
+
async function readOptionalFile2(fileSystem, path) {
|
|
1758
1979
|
try {
|
|
1759
1980
|
return { status: "READ", value: await fileSystem.readFile(path) };
|
|
1760
1981
|
} catch (error) {
|
|
1761
|
-
if (
|
|
1982
|
+
if (isMissingFileError2(error)) {
|
|
1762
1983
|
return { status: "READ", value: null };
|
|
1763
1984
|
}
|
|
1764
1985
|
return { status: "FAILED" };
|
|
1765
1986
|
}
|
|
1766
1987
|
}
|
|
1767
|
-
function
|
|
1988
|
+
function isMissingFileError2(error) {
|
|
1768
1989
|
return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
|
|
1769
1990
|
}
|
|
1770
1991
|
// packages/opencode-adapter/src/presentation/display.ts
|
|
@@ -1994,25 +2215,50 @@ function toOutcomeExtra(result) {
|
|
|
1994
2215
|
}
|
|
1995
2216
|
// packages/opencode-adapter/src/presentation/remediation.ts
|
|
1996
2217
|
var TITLE = "JevGuard";
|
|
2218
|
+
var SERVICE_NAME2 = "jevguard";
|
|
1997
2219
|
var PREPARING_MESSAGE = "JevGuard is preparing a remediation proposal in the child session.";
|
|
1998
2220
|
var READY_MESSAGE = "JevGuard remediation proposal ready.";
|
|
2221
|
+
var FAILED_MESSAGE = "JevGuard could not prepare a remediation proposal.";
|
|
2222
|
+
var FAILURE_LOG_MESSAGE = "JevGuard remediation proposal failed";
|
|
1999
2223
|
function createRemediationNotifier(toast) {
|
|
2000
2224
|
return {
|
|
2001
2225
|
async proposalPreparing() {
|
|
2002
|
-
return showRemediationToast(toast, PREPARING_MESSAGE);
|
|
2226
|
+
return showRemediationToast(toast, PREPARING_MESSAGE, "info");
|
|
2003
2227
|
},
|
|
2004
2228
|
async proposalReady() {
|
|
2005
|
-
return showRemediationToast(toast, READY_MESSAGE);
|
|
2229
|
+
return showRemediationToast(toast, READY_MESSAGE, "info");
|
|
2230
|
+
},
|
|
2231
|
+
async proposalFailed() {
|
|
2232
|
+
return showRemediationToast(toast, FAILED_MESSAGE, "error");
|
|
2006
2233
|
}
|
|
2007
2234
|
};
|
|
2008
2235
|
}
|
|
2009
|
-
async function showRemediationToast(toast, message) {
|
|
2236
|
+
async function showRemediationToast(toast, message, variant) {
|
|
2010
2237
|
try {
|
|
2011
|
-
return await toast.show({ title: TITLE, message, variant
|
|
2238
|
+
return await toast.show({ title: TITLE, message, variant });
|
|
2012
2239
|
} catch {
|
|
2013
2240
|
return "FAILED";
|
|
2014
2241
|
}
|
|
2015
2242
|
}
|
|
2243
|
+
function createRemediationFailureLogSink(client) {
|
|
2244
|
+
return {
|
|
2245
|
+
async write(reason) {
|
|
2246
|
+
try {
|
|
2247
|
+
const result = await client.app.log({
|
|
2248
|
+
body: {
|
|
2249
|
+
service: SERVICE_NAME2,
|
|
2250
|
+
level: "error",
|
|
2251
|
+
message: FAILURE_LOG_MESSAGE,
|
|
2252
|
+
extra: { reason }
|
|
2253
|
+
}
|
|
2254
|
+
});
|
|
2255
|
+
return result.error === undefined ? "DELIVERED" : "FAILED";
|
|
2256
|
+
} catch {
|
|
2257
|
+
return "FAILED";
|
|
2258
|
+
}
|
|
2259
|
+
}
|
|
2260
|
+
};
|
|
2261
|
+
}
|
|
2016
2262
|
function createOpenCodeSessionNavigator(client) {
|
|
2017
2263
|
return {
|
|
2018
2264
|
async navigate(sessionID) {
|
|
@@ -2063,11 +2309,12 @@ function createOpenCodeProposalSessionFacade(client) {
|
|
|
2063
2309
|
return result.data.id;
|
|
2064
2310
|
},
|
|
2065
2311
|
async prompt(input) {
|
|
2312
|
+
const model = input.model;
|
|
2066
2313
|
const result = await client.session.prompt({
|
|
2067
2314
|
path: { id: input.sessionID },
|
|
2068
2315
|
body: {
|
|
2069
2316
|
agent: input.agent,
|
|
2070
|
-
model: { providerID:
|
|
2317
|
+
...model === null ? {} : { model: { providerID: model.providerID, modelID: model.modelID } },
|
|
2071
2318
|
system: input.system,
|
|
2072
2319
|
tools: { ...input.tools },
|
|
2073
2320
|
parts: [{ type: "text", text: input.text }]
|
|
@@ -2146,21 +2393,28 @@ async function proposeForTurn(request, dependencies) {
|
|
|
2146
2393
|
if (request === null) {
|
|
2147
2394
|
return;
|
|
2148
2395
|
}
|
|
2149
|
-
const model = parseModelSpecifier(request.model);
|
|
2150
|
-
if (model === null) {
|
|
2396
|
+
const model = request.model === null ? null : parseModelSpecifier(request.model);
|
|
2397
|
+
if (request.model !== null && model === null) {
|
|
2398
|
+
await reportProposalFailure("MODEL_SPECIFIER_INVALID", dependencies);
|
|
2151
2399
|
return;
|
|
2152
2400
|
}
|
|
2153
2401
|
if (!dependencies.proposals.claim(request.evaluationId)) {
|
|
2154
2402
|
return;
|
|
2155
2403
|
}
|
|
2404
|
+
let sessionID;
|
|
2156
2405
|
try {
|
|
2157
|
-
|
|
2406
|
+
sessionID = await dependencies.proposalFacade.createChildSession({
|
|
2158
2407
|
parentID: request.sessionID,
|
|
2159
2408
|
title: PROPOSAL_SESSION_TITLE
|
|
2160
2409
|
});
|
|
2161
|
-
|
|
2162
|
-
await
|
|
2163
|
-
|
|
2410
|
+
} catch {
|
|
2411
|
+
await reportProposalFailure("CHILD_SESSION_FAILED", dependencies);
|
|
2412
|
+
return;
|
|
2413
|
+
}
|
|
2414
|
+
dependencies.proposals.registerChildSession(sessionID);
|
|
2415
|
+
await navigateToProposal(dependencies.navigator, sessionID);
|
|
2416
|
+
await notifyProposalPreparing(dependencies.notifier);
|
|
2417
|
+
try {
|
|
2164
2418
|
await dependencies.proposalFacade.prompt({
|
|
2165
2419
|
sessionID,
|
|
2166
2420
|
agent: PROPOSER_AGENT_NAME,
|
|
@@ -2169,7 +2423,26 @@ async function proposeForTurn(request, dependencies) {
|
|
|
2169
2423
|
tools: PROPOSER_TOOLS,
|
|
2170
2424
|
text: buildProposalText(request)
|
|
2171
2425
|
});
|
|
2172
|
-
|
|
2426
|
+
} catch {
|
|
2427
|
+
await reportProposalFailure("PROPOSAL_PROMPT_FAILED", dependencies);
|
|
2428
|
+
return;
|
|
2429
|
+
}
|
|
2430
|
+
await notifyProposalReady(dependencies.notifier);
|
|
2431
|
+
}
|
|
2432
|
+
async function reportProposalFailure(reason, dependencies) {
|
|
2433
|
+
await writeFailureLog(dependencies.failures, reason);
|
|
2434
|
+
await notifyProposalFailed(dependencies.notifier);
|
|
2435
|
+
}
|
|
2436
|
+
async function writeFailureLog(sink, reason) {
|
|
2437
|
+
try {
|
|
2438
|
+
await sink.write(reason);
|
|
2439
|
+
} catch {
|
|
2440
|
+
return;
|
|
2441
|
+
}
|
|
2442
|
+
}
|
|
2443
|
+
async function notifyProposalFailed(notifier) {
|
|
2444
|
+
try {
|
|
2445
|
+
await notifier.proposalFailed();
|
|
2173
2446
|
} catch {
|
|
2174
2447
|
return;
|
|
2175
2448
|
}
|
|
@@ -2416,7 +2689,7 @@ function createConfigHook(options) {
|
|
|
2416
2689
|
|
|
2417
2690
|
// packages/plugin/src/skills/skill-directory.ts
|
|
2418
2691
|
import { existsSync } from "fs";
|
|
2419
|
-
import { join as
|
|
2692
|
+
import { join as join5 } from "path";
|
|
2420
2693
|
import { fileURLToPath } from "url";
|
|
2421
2694
|
var BUNDLED_SKILL_NAMES = ["jevguard-rules", "jev-init"];
|
|
2422
2695
|
var SKILL_ENTRY_FILE = "SKILL.md";
|
|
@@ -2424,7 +2697,7 @@ var SKILL_DIRECTORY_PREFIXES = ["./skills/", "../skills/"];
|
|
|
2424
2697
|
function resolveSkillDirectory(moduleUrl, skillName, exists) {
|
|
2425
2698
|
for (const prefix of SKILL_DIRECTORY_PREFIXES) {
|
|
2426
2699
|
const directory = fileURLToPath(new URL(`${prefix}${skillName}`, moduleUrl));
|
|
2427
|
-
if (exists(
|
|
2700
|
+
if (exists(join5(directory, SKILL_ENTRY_FILE))) {
|
|
2428
2701
|
return directory;
|
|
2429
2702
|
}
|
|
2430
2703
|
}
|
|
@@ -2461,6 +2734,7 @@ var JevGuardPlugin = async (input) => {
|
|
|
2461
2734
|
proposalFacade: createOpenCodeProposalSessionFacade(input.client),
|
|
2462
2735
|
navigator: createOpenCodeSessionNavigator(presentationClient),
|
|
2463
2736
|
notifier: createRemediationNotifier(toast),
|
|
2737
|
+
failures: createRemediationFailureLogSink(presentationClient),
|
|
2464
2738
|
jev: createFifoJevPort(createTypeSafeJevTransport(createCredentialProvider({
|
|
2465
2739
|
environment: createProcessEnvironment(),
|
|
2466
2740
|
store: createKeyringCredentialStore()
|
package/package.json
CHANGED
package/skills/jev-init/SKILL.md
CHANGED
|
@@ -165,11 +165,12 @@ remediation:
|
|
|
165
165
|
auto_propose: true
|
|
166
166
|
propose_on:
|
|
167
167
|
- FAIL
|
|
168
|
-
model: opencode/gpt-5.6-luna
|
|
169
168
|
```
|
|
170
169
|
|
|
171
|
-
Do not invent thresholds
|
|
172
|
-
|
|
170
|
+
Do not invent thresholds or add keys. The default configuration is valid and uses the
|
|
171
|
+
documented defaults. Never pin `remediation.model`: when it is absent the proposer
|
|
172
|
+
inherits the host model, which is the only portable choice. A hard-coded provider
|
|
173
|
+
model can be absent on the user's machine and would make every proposal fail.
|
|
173
174
|
|
|
174
175
|
### 7. Preview both files and require explicit confirmation
|
|
175
176
|
|
|
@@ -910,7 +910,7 @@ var REMEDIATION_MAX_TASK_LENGTH = 16384;
|
|
|
910
910
|
var DEFAULT_REMEDIATION_CONFIG = {
|
|
911
911
|
autoPropose: true,
|
|
912
912
|
proposeOn: ["FAIL"],
|
|
913
|
-
model:
|
|
913
|
+
model: null
|
|
914
914
|
};
|
|
915
915
|
|
|
916
916
|
// packages/core/src/remediation/request.ts
|
|
@@ -1000,6 +1000,7 @@ function snapshotRule(rule) {
|
|
|
1000
1000
|
// packages/core/src/remediation/config.ts
|
|
1001
1001
|
var MODEL_PATTERN = /^[^\s/]+\/[^\s/]+$/;
|
|
1002
1002
|
var KNOWN_KEYS = new Set(["auto_propose", "propose_on", "model"]);
|
|
1003
|
+
var INVALID_MODEL = Symbol("invalid-model");
|
|
1003
1004
|
function resolveRemediationConfig(value) {
|
|
1004
1005
|
if (value === null || value === undefined) {
|
|
1005
1006
|
return valid(DEFAULT_REMEDIATION_CONFIG);
|
|
@@ -1022,8 +1023,8 @@ function resolveRemediationConfig(value) {
|
|
|
1022
1023
|
if (proposeOn === null) {
|
|
1023
1024
|
return invalid2();
|
|
1024
1025
|
}
|
|
1025
|
-
const model = remediation.model
|
|
1026
|
-
if (
|
|
1026
|
+
const model = resolveModel(remediation.model);
|
|
1027
|
+
if (model === INVALID_MODEL) {
|
|
1027
1028
|
return invalid2();
|
|
1028
1029
|
}
|
|
1029
1030
|
return valid({ autoPropose, proposeOn, model });
|
|
@@ -1038,6 +1039,15 @@ function parseModelSpecifier(model) {
|
|
|
1038
1039
|
modelID: model.slice(separator + 1)
|
|
1039
1040
|
};
|
|
1040
1041
|
}
|
|
1042
|
+
function resolveModel(value) {
|
|
1043
|
+
if (value === undefined) {
|
|
1044
|
+
return null;
|
|
1045
|
+
}
|
|
1046
|
+
if (typeof value !== "string" || !MODEL_PATTERN.test(value)) {
|
|
1047
|
+
return INVALID_MODEL;
|
|
1048
|
+
}
|
|
1049
|
+
return value;
|
|
1050
|
+
}
|
|
1041
1051
|
function readProposeOn(value) {
|
|
1042
1052
|
if (value === undefined) {
|
|
1043
1053
|
return DEFAULT_REMEDIATION_CONFIG.proposeOn;
|
|
@@ -910,7 +910,7 @@ var REMEDIATION_MAX_TASK_LENGTH = 16384;
|
|
|
910
910
|
var DEFAULT_REMEDIATION_CONFIG = {
|
|
911
911
|
autoPropose: true,
|
|
912
912
|
proposeOn: ["FAIL"],
|
|
913
|
-
model:
|
|
913
|
+
model: null
|
|
914
914
|
};
|
|
915
915
|
|
|
916
916
|
// packages/core/src/remediation/request.ts
|
|
@@ -1000,6 +1000,7 @@ function snapshotRule(rule) {
|
|
|
1000
1000
|
// packages/core/src/remediation/config.ts
|
|
1001
1001
|
var MODEL_PATTERN = /^[^\s/]+\/[^\s/]+$/;
|
|
1002
1002
|
var KNOWN_KEYS = new Set(["auto_propose", "propose_on", "model"]);
|
|
1003
|
+
var INVALID_MODEL = Symbol("invalid-model");
|
|
1003
1004
|
function resolveRemediationConfig(value) {
|
|
1004
1005
|
if (value === null || value === undefined) {
|
|
1005
1006
|
return valid(DEFAULT_REMEDIATION_CONFIG);
|
|
@@ -1022,8 +1023,8 @@ function resolveRemediationConfig(value) {
|
|
|
1022
1023
|
if (proposeOn === null) {
|
|
1023
1024
|
return invalid2();
|
|
1024
1025
|
}
|
|
1025
|
-
const model = remediation.model
|
|
1026
|
-
if (
|
|
1026
|
+
const model = resolveModel(remediation.model);
|
|
1027
|
+
if (model === INVALID_MODEL) {
|
|
1027
1028
|
return invalid2();
|
|
1028
1029
|
}
|
|
1029
1030
|
return valid({ autoPropose, proposeOn, model });
|
|
@@ -1038,6 +1039,15 @@ function parseModelSpecifier(model) {
|
|
|
1038
1039
|
modelID: model.slice(separator + 1)
|
|
1039
1040
|
};
|
|
1040
1041
|
}
|
|
1042
|
+
function resolveModel(value) {
|
|
1043
|
+
if (value === undefined) {
|
|
1044
|
+
return null;
|
|
1045
|
+
}
|
|
1046
|
+
if (typeof value !== "string" || !MODEL_PATTERN.test(value)) {
|
|
1047
|
+
return INVALID_MODEL;
|
|
1048
|
+
}
|
|
1049
|
+
return value;
|
|
1050
|
+
}
|
|
1041
1051
|
function readProposeOn(value) {
|
|
1042
1052
|
if (value === undefined) {
|
|
1043
1053
|
return DEFAULT_REMEDIATION_CONFIG.proposeOn;
|