@nail00749/agent-gvozd 0.1.2 → 0.1.4
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 +12 -6
- package/defaults/agents/devops.jsonc +138 -29
- package/defaults/agents/git.jsonc +950 -36
- package/defaults/agents/verifier.jsonc +1283 -20
- package/defaults/prompts/back-deep.md +1 -1
- package/defaults/prompts/back-fast.md +1 -1
- package/defaults/prompts/devops.md +1 -1
- package/defaults/prompts/docs.md +1 -1
- package/defaults/prompts/front-deep.md +1 -1
- package/defaults/prompts/front-fast.md +1 -1
- package/defaults/prompts/git.md +3 -1
- package/defaults/prompts/master.md +10 -1
- package/defaults/prompts/researcher.md +1 -1
- package/defaults/prompts/review-deep.md +1 -1
- package/defaults/prompts/verifier.md +3 -1
- package/defaults/schema.json +11 -1
- package/dist/cli.js +475 -8
- package/dist/index.js +350 -258
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -2,6 +2,77 @@
|
|
|
2
2
|
// src/index.ts
|
|
3
3
|
import { Model, Plugin } from "@opencode/plugin";
|
|
4
4
|
|
|
5
|
+
// src/tool-permissions.ts
|
|
6
|
+
function family(command, ...variants) {
|
|
7
|
+
return [command, ...variants].map((entry) => ({
|
|
8
|
+
exact: entry,
|
|
9
|
+
wildcard: `${entry} *`
|
|
10
|
+
}));
|
|
11
|
+
}
|
|
12
|
+
function exactOnly(command, ...variants) {
|
|
13
|
+
return [command, ...variants].map((entry) => ({ exact: entry, wildcard: entry }));
|
|
14
|
+
}
|
|
15
|
+
var INSPECTION_COMMANDS = [
|
|
16
|
+
...family("pwd", "true", "test"),
|
|
17
|
+
...family("cat", "head", "tail", "wc", "sort", "uniq"),
|
|
18
|
+
...family("grep", "rg", "find", "diff", "cmp"),
|
|
19
|
+
...family("ls", "du", "df", "stat", "file", "realpath", "basename", "dirname"),
|
|
20
|
+
...family("shasum", "sha256sum", "md5sum"),
|
|
21
|
+
...family("uname", "whoami", "hostname", "date", "printenv"),
|
|
22
|
+
...family("which", "command -v"),
|
|
23
|
+
...family("mktemp"),
|
|
24
|
+
...family("tr", "cut", "paste", "column"),
|
|
25
|
+
...family("node --version", "python3 --version", "python --version", "deno --version")
|
|
26
|
+
];
|
|
27
|
+
var TOOLCHAIN_COMMANDS = [
|
|
28
|
+
...family("bun test", "bun run test", "bun --version"),
|
|
29
|
+
...family("bun run typecheck", "bun run lint", "bun run build", "bun run check"),
|
|
30
|
+
...family("tsc --noEmit", "npx tsc --noEmit"),
|
|
31
|
+
...family("eslint", "biome check", "prettier --check"),
|
|
32
|
+
...family("npm test", "npm run test", "npm run typecheck", "npm run lint", "npm run build"),
|
|
33
|
+
...family("pnpm test", "pnpm run test", "pnpm run build"),
|
|
34
|
+
...family("yarn test", "yarn build"),
|
|
35
|
+
...family("vitest run", "jest", "playwright test"),
|
|
36
|
+
...family("cargo check", "cargo test", "cargo build", "cargo clippy", "cargo fmt --check", "cargo --version"),
|
|
37
|
+
...family("go build ./...", "go test ./...", "go vet ./...", "go version"),
|
|
38
|
+
...family("pytest", "python3 -m pytest", "python -m pytest"),
|
|
39
|
+
...family("ruff check", "mypy", "pyright"),
|
|
40
|
+
...family("mvn test", "mvn verify", "gradle test", "gradle check", "./gradlew test", "./gradlew check"),
|
|
41
|
+
...family("make test", "make check", "make build", "make --version"),
|
|
42
|
+
...family("just --list")
|
|
43
|
+
];
|
|
44
|
+
var GIT_READONLY_COMMANDS = [
|
|
45
|
+
...family("git status", "git status --short", "git status --short --branch", "git status --porcelain", "git status --porcelain=v1 --branch"),
|
|
46
|
+
...family("git diff", "git diff --stat", "git diff --cached", "git diff --check"),
|
|
47
|
+
...family("git log", "git show"),
|
|
48
|
+
...family("git rev-parse", "git rev-list", "git show-ref", "git cat-file"),
|
|
49
|
+
...exactOnly("git symbolic-ref HEAD", "git symbolic-ref --short HEAD"),
|
|
50
|
+
...family("git ls-files", "git ls-remote", "git grep"),
|
|
51
|
+
...exactOnly("git branch", "git tag", "git remote", "git reflog"),
|
|
52
|
+
...family("git branch --list", "git branch -l", "git branch -a", "git branch -r", "git branch -v", "git branch -vv", "git branch --all", "git branch --remotes", "git branch --show-current", "git branch --contains"),
|
|
53
|
+
...family("git tag --list", "git tag -l", "git tag -n"),
|
|
54
|
+
...family("git remote -v", "git remote --verbose", "git remote show", "git remote get-url"),
|
|
55
|
+
...family("git reflog show"),
|
|
56
|
+
...family("git stash list", "git describe", "git worktree list"),
|
|
57
|
+
...family("git config --get", "git config --get-regexp")
|
|
58
|
+
];
|
|
59
|
+
var GIT_MUTATING_COMMANDS = [
|
|
60
|
+
...family("git add", "git rm --cached"),
|
|
61
|
+
...family("git commit", "git merge --ff-only", "git merge --no-ff"),
|
|
62
|
+
...family("git push", "git fetch", "git pull --ff-only"),
|
|
63
|
+
...family("git stash", "git cherry-pick", "git revert"),
|
|
64
|
+
...family("git switch", "git checkout -b", "git worktree add"),
|
|
65
|
+
...exactOnly("git branch *", "git tag *", "git remote *", "git symbolic-ref *", "git reflog *")
|
|
66
|
+
];
|
|
67
|
+
var GIT_ENV_PREFIXES = ["GIT_OPTIONAL_LOCKS=0"];
|
|
68
|
+
function withEnvPrefixes(rule, prefixes = GIT_ENV_PREFIXES) {
|
|
69
|
+
return prefixes.map((prefix) => ({
|
|
70
|
+
action: rule.action,
|
|
71
|
+
resource: `${prefix} ${rule.resource}`,
|
|
72
|
+
effect: rule.effect
|
|
73
|
+
}));
|
|
74
|
+
}
|
|
75
|
+
|
|
5
76
|
// src/agent-permissions.ts
|
|
6
77
|
function normalizeMcpName(name) {
|
|
7
78
|
return name.replaceAll(/[^A-Za-z0-9_-]/g, "_");
|
|
@@ -45,12 +116,21 @@ function buildAgentPermissions(agent, mcpServers) {
|
|
|
45
116
|
});
|
|
46
117
|
result.push(...agent.permissions.filter((rule) => rule.action.startsWith(prefix)));
|
|
47
118
|
}
|
|
119
|
+
for (const rule of agent.permissions) {
|
|
120
|
+
if (rule.action !== "shell")
|
|
121
|
+
continue;
|
|
122
|
+
if (!/(^|\s|["'])git(?:$|\s)/.test(rule.resource))
|
|
123
|
+
continue;
|
|
124
|
+
if (rule.resource.startsWith("GIT_"))
|
|
125
|
+
continue;
|
|
126
|
+
result.push(...withEnvPrefixes(rule));
|
|
127
|
+
}
|
|
48
128
|
return result;
|
|
49
129
|
}
|
|
50
130
|
|
|
51
131
|
// src/config.ts
|
|
52
|
-
import { existsSync as
|
|
53
|
-
import { dirname as
|
|
132
|
+
import { existsSync as existsSync3, lstatSync as lstatSync3, readdirSync as readdirSync2, readFileSync as readFileSync2, realpathSync as realpathSync3 } from "fs";
|
|
133
|
+
import { dirname as dirname3, isAbsolute as isAbsolute4, join as join3, relative as relative3, resolve as resolve4 } from "path";
|
|
54
134
|
import { fileURLToPath } from "url";
|
|
55
135
|
|
|
56
136
|
// node_modules/jsonc-parser/lib/esm/impl/scanner.js
|
|
@@ -5686,226 +5766,10 @@ function computeProjectTrustToken(projectDirectory) {
|
|
|
5686
5766
|
return `sha256:${hash.digest("hex")}`;
|
|
5687
5767
|
}
|
|
5688
5768
|
|
|
5689
|
-
// src/config.ts
|
|
5690
|
-
var permissionSchema = object({
|
|
5691
|
-
action: string2().min(1),
|
|
5692
|
-
resource: string2().min(1),
|
|
5693
|
-
effect: _enum(["allow", "ask", "deny"])
|
|
5694
|
-
}).strict();
|
|
5695
|
-
var modelRefSchema = string2().min(1).regex(/^[^/#\s]+\/[^#\s]+(?:#[^#\s]+)?$/, "Expected provider/model or provider/model#variant");
|
|
5696
|
-
var agentIdSchema = string2().regex(/^[A-Za-z0-9][A-Za-z0-9_-]*$/, "Expected a filesystem-safe agent ID");
|
|
5697
|
-
var fileLeaseRoleSchema = _enum(["coordinator", "writer", "readonly"]);
|
|
5698
|
-
var agentPatchSchema = object({
|
|
5699
|
-
description: string2().min(1).optional(),
|
|
5700
|
-
mode: _enum(["primary", "subagent", "all"]).optional(),
|
|
5701
|
-
models: array(modelRefSchema).min(1).optional(),
|
|
5702
|
-
prompt: string2().min(1).optional(),
|
|
5703
|
-
skills: array(string2().min(1)).optional(),
|
|
5704
|
-
mcp: array(string2().min(1)).optional(),
|
|
5705
|
-
permissions: array(permissionSchema).optional(),
|
|
5706
|
-
fileLease: fileLeaseRoleSchema.optional(),
|
|
5707
|
-
disabled: boolean2().optional()
|
|
5708
|
-
}).strict();
|
|
5709
|
-
var rootPatchSchema = object({
|
|
5710
|
-
$schema: string2().min(1).optional(),
|
|
5711
|
-
defaultAgent: agentIdSchema.optional(),
|
|
5712
|
-
agentsDirectory: string2().min(1).optional(),
|
|
5713
|
-
agents: record(agentIdSchema, agentPatchSchema).optional()
|
|
5714
|
-
}).strict();
|
|
5715
|
-
var resolvedAgentSchema = agentPatchSchema.extend({
|
|
5716
|
-
description: string2().min(1),
|
|
5717
|
-
mode: _enum(["primary", "subagent", "all"]),
|
|
5718
|
-
models: array(modelRefSchema).min(1),
|
|
5719
|
-
prompt: string2().min(1),
|
|
5720
|
-
skills: array(string2().min(1)),
|
|
5721
|
-
mcp: array(string2().min(1)),
|
|
5722
|
-
permissions: array(permissionSchema),
|
|
5723
|
-
fileLease: fileLeaseRoleSchema,
|
|
5724
|
-
disabled: boolean2()
|
|
5725
|
-
});
|
|
5726
|
-
var SAFE_UNTRUSTED_AGENT_FIELDS = new Set(["description"]);
|
|
5727
|
-
function assertTrustedProjectPatch(patch, sourcePath, id, trusted, knownAgents) {
|
|
5728
|
-
if (trusted)
|
|
5729
|
-
return;
|
|
5730
|
-
const fields = Object.keys(patch).filter((field) => !SAFE_UNTRUSTED_AGENT_FIELDS.has(field));
|
|
5731
|
-
if (fields.length === 0 && knownAgents.has(id))
|
|
5732
|
-
return;
|
|
5733
|
-
throw new Error(`Untrusted project config ${sourcePath} cannot override ${fields.join(", ") || `unknown agent ${id}`}. ` + `Set ${PROJECT_TRUST_ENV} to the exact token returned by computeProjectTrustToken() after reviewing these files.`);
|
|
5734
|
-
}
|
|
5735
|
-
function readJsonc2(path) {
|
|
5736
|
-
const errors = [];
|
|
5737
|
-
const value = parse2(readFileSync2(path, "utf8"), errors, {
|
|
5738
|
-
allowTrailingComma: true,
|
|
5739
|
-
disallowComments: false
|
|
5740
|
-
});
|
|
5741
|
-
if (errors.length > 0) {
|
|
5742
|
-
const details = errors.map((error) => `${printParseErrorCode(error.error)} at offset ${error.offset}`).join(", ");
|
|
5743
|
-
throw new Error(`Invalid JSONC in ${path}: ${details}`);
|
|
5744
|
-
}
|
|
5745
|
-
return value;
|
|
5746
|
-
}
|
|
5747
|
-
function assertWithin(base, target, label) {
|
|
5748
|
-
const child = relative2(base, target);
|
|
5749
|
-
if (child === "" || !child.startsWith("..") && !isAbsolute3(child))
|
|
5750
|
-
return;
|
|
5751
|
-
throw new Error(`${label} must stay inside ${base}: ${target}`);
|
|
5752
|
-
}
|
|
5753
|
-
function resolvePrompt(patch, sourcePath, layerDirectory) {
|
|
5754
|
-
if (!patch.prompt)
|
|
5755
|
-
return patch;
|
|
5756
|
-
const prompt = resolve3(dirname2(sourcePath), patch.prompt);
|
|
5757
|
-
assertWithin(layerDirectory, prompt, "Agent prompt");
|
|
5758
|
-
if (!existsSync2(prompt))
|
|
5759
|
-
throw new Error(`Agent prompt is missing: ${prompt}`);
|
|
5760
|
-
const promptStat = lstatSync2(prompt);
|
|
5761
|
-
if (promptStat.isSymbolicLink() || !promptStat.isFile())
|
|
5762
|
-
throw new Error(`Agent prompt must be a regular non-symlink file: ${prompt}`);
|
|
5763
|
-
const canonical = realpathSync2(prompt);
|
|
5764
|
-
assertWithin(realpathSync2(layerDirectory), canonical, "Agent prompt");
|
|
5765
|
-
const promptContent = readFileSync2(canonical, "utf8");
|
|
5766
|
-
return { ...patch, prompt: canonical, promptContent };
|
|
5767
|
-
}
|
|
5768
|
-
function loadLayer(directory, rootFileName, required, projectPolicy) {
|
|
5769
|
-
const rootPath = join3(directory, rootFileName);
|
|
5770
|
-
if (!existsSync2(rootPath)) {
|
|
5771
|
-
if (required)
|
|
5772
|
-
throw new Error(`Required config is missing: ${rootPath}`);
|
|
5773
|
-
return { agents: {}, sources: [] };
|
|
5774
|
-
}
|
|
5775
|
-
const root = rootPatchSchema.parse(readJsonc2(rootPath));
|
|
5776
|
-
if (projectPolicy && !projectPolicy.trusted) {
|
|
5777
|
-
const restricted = ["defaultAgent", "agentsDirectory"].filter((field) => Object.prototype.hasOwnProperty.call(root, field));
|
|
5778
|
-
if (restricted.length > 0)
|
|
5779
|
-
throw new Error(`Untrusted project config ${rootPath} cannot override ${restricted.join(", ")}`);
|
|
5780
|
-
}
|
|
5781
|
-
const agents = {};
|
|
5782
|
-
for (const [id, patch] of Object.entries(root.agents ?? {})) {
|
|
5783
|
-
if (projectPolicy)
|
|
5784
|
-
assertTrustedProjectPatch(patch, rootPath, id, projectPolicy.trusted, projectPolicy.knownAgents);
|
|
5785
|
-
agents[id] = resolvePrompt(patch, rootPath, directory);
|
|
5786
|
-
}
|
|
5787
|
-
const agentsDirectory = resolve3(directory, root.agentsDirectory ?? "agents");
|
|
5788
|
-
assertWithin(directory, agentsDirectory, "agentsDirectory");
|
|
5789
|
-
if (existsSync2(agentsDirectory)) {
|
|
5790
|
-
const directoryStat = lstatSync2(agentsDirectory);
|
|
5791
|
-
if (directoryStat.isSymbolicLink() || !directoryStat.isDirectory()) {
|
|
5792
|
-
throw new Error(`agentsDirectory must be a regular directory: ${agentsDirectory}`);
|
|
5793
|
-
}
|
|
5794
|
-
assertWithin(realpathSync2(directory), realpathSync2(agentsDirectory), "agentsDirectory");
|
|
5795
|
-
for (const entry of readdirSync2(agentsDirectory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
5796
|
-
if (!entry.isFile() || !entry.name.endsWith(".jsonc"))
|
|
5797
|
-
continue;
|
|
5798
|
-
const id = entry.name.slice(0, -".jsonc".length);
|
|
5799
|
-
agentIdSchema.parse(id);
|
|
5800
|
-
const agentPath = join3(agentsDirectory, entry.name);
|
|
5801
|
-
const patch = agentPatchSchema.parse(readJsonc2(agentPath));
|
|
5802
|
-
if (projectPolicy)
|
|
5803
|
-
assertTrustedProjectPatch(patch, agentPath, id, projectPolicy.trusted, projectPolicy.knownAgents);
|
|
5804
|
-
agents[id] = mergeAgent(agents[id], resolvePrompt(patch, agentPath, directory));
|
|
5805
|
-
}
|
|
5806
|
-
}
|
|
5807
|
-
return {
|
|
5808
|
-
defaultAgent: root.defaultAgent,
|
|
5809
|
-
agents,
|
|
5810
|
-
sources: [rootPath]
|
|
5811
|
-
};
|
|
5812
|
-
}
|
|
5813
|
-
function mergeAgent(base, override) {
|
|
5814
|
-
return { ...base ?? {}, ...override };
|
|
5815
|
-
}
|
|
5816
|
-
function resolveAgentConfig(patch) {
|
|
5817
|
-
const parsed = agentPatchSchema.extend({ promptContent: string2().optional() }).parse(patch);
|
|
5818
|
-
const { promptContent, ...agentPatch } = parsed;
|
|
5819
|
-
const resolved = resolvedAgentSchema.parse({
|
|
5820
|
-
skills: [],
|
|
5821
|
-
mcp: [],
|
|
5822
|
-
permissions: [],
|
|
5823
|
-
fileLease: "readonly",
|
|
5824
|
-
disabled: false,
|
|
5825
|
-
...agentPatch
|
|
5826
|
-
});
|
|
5827
|
-
return promptContent === undefined ? resolved : { ...resolved, promptContent };
|
|
5828
|
-
}
|
|
5829
|
-
function findPackageRoot() {
|
|
5830
|
-
let current = dirname2(fileURLToPath(import.meta.url));
|
|
5831
|
-
while (true) {
|
|
5832
|
-
if (existsSync2(join3(current, "defaults", "default.jsonc")))
|
|
5833
|
-
return current;
|
|
5834
|
-
const parent = dirname2(current);
|
|
5835
|
-
if (parent === current)
|
|
5836
|
-
break;
|
|
5837
|
-
current = parent;
|
|
5838
|
-
}
|
|
5839
|
-
throw new Error("Unable to locate agent-gvozd package defaults");
|
|
5840
|
-
}
|
|
5841
|
-
function findProjectRoot(start) {
|
|
5842
|
-
let current = resolve3(start);
|
|
5843
|
-
while (true) {
|
|
5844
|
-
const git = join3(current, ".git");
|
|
5845
|
-
if (existsSync2(git))
|
|
5846
|
-
return current;
|
|
5847
|
-
const parent = dirname2(current);
|
|
5848
|
-
if (parent === current)
|
|
5849
|
-
return resolve3(start);
|
|
5850
|
-
current = parent;
|
|
5851
|
-
}
|
|
5852
|
-
}
|
|
5853
|
-
function loadConfig(projectDirectory, options = {}) {
|
|
5854
|
-
const projectRoot = findProjectRoot(projectDirectory);
|
|
5855
|
-
const packageRoot = findPackageRoot();
|
|
5856
|
-
const projectConfigDirectory = join3(projectRoot, "docs", ".gvozd");
|
|
5857
|
-
const globalConfigDirectory = join3(resolveOpenCodeConfigRoot(options.env, options.platform, options.home, options.configRoot), "gvozd");
|
|
5858
|
-
const baseLayers = [
|
|
5859
|
-
loadLayer(join3(packageRoot, "defaults"), "default.jsonc", true),
|
|
5860
|
-
loadLayer(globalConfigDirectory, "config.jsonc", false)
|
|
5861
|
-
];
|
|
5862
|
-
const knownAgents = new Set(baseLayers.flatMap((layer) => Object.keys(layer.agents)));
|
|
5863
|
-
const includeProject = options.includeProject !== false;
|
|
5864
|
-
const suppliedToken = includeProject ? options.projectTrustToken ?? (options.env ?? process.env)[PROJECT_TRUST_ENV] : undefined;
|
|
5865
|
-
const trustProjectConfig = includeProject && suppliedToken !== undefined && suppliedToken === computeProjectTrustToken(projectRoot);
|
|
5866
|
-
const projectLayer = includeProject ? loadLayer(projectConfigDirectory, "config.jsonc", false, { trusted: trustProjectConfig, knownAgents }) : undefined;
|
|
5867
|
-
if (trustProjectConfig && suppliedToken !== computeProjectTrustToken(projectRoot)) {
|
|
5868
|
-
throw new Error("Project configuration changed while its trust token was being validated; review it and compute a new token");
|
|
5869
|
-
}
|
|
5870
|
-
const layers = [...baseLayers, ...projectLayer ? [projectLayer] : []];
|
|
5871
|
-
let defaultAgent;
|
|
5872
|
-
const agents = {};
|
|
5873
|
-
for (const layer of layers) {
|
|
5874
|
-
defaultAgent = layer.defaultAgent ?? defaultAgent;
|
|
5875
|
-
for (const [id, patch] of Object.entries(layer.agents)) {
|
|
5876
|
-
agents[id] = mergeAgent(agents[id], patch);
|
|
5877
|
-
}
|
|
5878
|
-
}
|
|
5879
|
-
if (!defaultAgent)
|
|
5880
|
-
throw new Error("defaultAgent is not configured");
|
|
5881
|
-
const resolvedAgents = Object.fromEntries(Object.entries(agents).map(([id, patch]) => [
|
|
5882
|
-
id,
|
|
5883
|
-
(() => {
|
|
5884
|
-
const agent = resolveAgentConfig(patch);
|
|
5885
|
-
if (agent.promptContent === undefined)
|
|
5886
|
-
throw new Error(`Agent prompt snapshot is missing after configuration load: ${agent.prompt}`);
|
|
5887
|
-
return agent;
|
|
5888
|
-
})()
|
|
5889
|
-
]));
|
|
5890
|
-
const defaultConfig = resolvedAgents[defaultAgent];
|
|
5891
|
-
if (!defaultConfig || defaultConfig.disabled || defaultConfig.mode === "subagent") {
|
|
5892
|
-
throw new Error(`defaultAgent must reference an enabled primary agent: ${defaultAgent}`);
|
|
5893
|
-
}
|
|
5894
|
-
return {
|
|
5895
|
-
defaultAgent,
|
|
5896
|
-
agents: resolvedAgents,
|
|
5897
|
-
packageRoot,
|
|
5898
|
-
projectRoot,
|
|
5899
|
-
projectConfigDirectory,
|
|
5900
|
-
globalConfigDirectory,
|
|
5901
|
-
sources: layers.flatMap((layer) => layer.sources)
|
|
5902
|
-
};
|
|
5903
|
-
}
|
|
5904
|
-
|
|
5905
5769
|
// src/file-leases.ts
|
|
5906
5770
|
import { randomUUID } from "crypto";
|
|
5907
|
-
import { existsSync as
|
|
5908
|
-
import { basename, dirname as
|
|
5771
|
+
import { existsSync as existsSync2, lstatSync as lstatSync2, realpathSync as realpathSync2, statSync } from "fs";
|
|
5772
|
+
import { basename, dirname as dirname2, isAbsolute as isAbsolute3, relative as relative2, resolve as resolve3, sep } from "path";
|
|
5909
5773
|
|
|
5910
5774
|
class LeaseError extends Error {
|
|
5911
5775
|
code;
|
|
@@ -5941,11 +5805,11 @@ function assertPositiveDuration(value, label) {
|
|
|
5941
5805
|
return value;
|
|
5942
5806
|
}
|
|
5943
5807
|
function isWithin(root, target) {
|
|
5944
|
-
const child =
|
|
5945
|
-
return child !== "" && !child.startsWith(`..${sep}`) && child !== ".." && !
|
|
5808
|
+
const child = relative2(root, target);
|
|
5809
|
+
return child !== "" && !child.startsWith(`..${sep}`) && child !== ".." && !isAbsolute3(child);
|
|
5946
5810
|
}
|
|
5947
5811
|
function displayPath(root, target) {
|
|
5948
|
-
return
|
|
5812
|
+
return relative2(root, target).replaceAll("\\", "/");
|
|
5949
5813
|
}
|
|
5950
5814
|
|
|
5951
5815
|
class FileLeaseManager {
|
|
@@ -5959,7 +5823,7 @@ class FileLeaseManager {
|
|
|
5959
5823
|
fileOwners = new Map;
|
|
5960
5824
|
sessionOwners = new Map;
|
|
5961
5825
|
constructor(options) {
|
|
5962
|
-
this.projectRoot =
|
|
5826
|
+
this.projectRoot = realpathSync2(options.projectRoot);
|
|
5963
5827
|
if (!statSync(this.projectRoot).isDirectory())
|
|
5964
5828
|
throw new Error(`Project root is not a directory: ${this.projectRoot}`);
|
|
5965
5829
|
this.reservationTtlMs = assertPositiveDuration(options.reservationTtlMs ?? DEFAULT_RESERVATION_TTL_MS, "reservationTtlMs");
|
|
@@ -6175,7 +6039,7 @@ class FileLeaseManager {
|
|
|
6175
6039
|
const value = input.trim();
|
|
6176
6040
|
if (value === "" || value.includes("\x00"))
|
|
6177
6041
|
throw new LeaseError("INVALID_PATH", "File path must not be empty");
|
|
6178
|
-
if (!allowAbsolute &&
|
|
6042
|
+
if (!allowAbsolute && isAbsolute3(value))
|
|
6179
6043
|
throw new LeaseError("INVALID_PATH", `Absolute lease path is not allowed: ${value}`);
|
|
6180
6044
|
if (value.includes("*") || value.includes("?")) {
|
|
6181
6045
|
throw new LeaseError("INVALID_PATH", `Lease paths must name exact files, not patterns: ${value}`);
|
|
@@ -6183,14 +6047,14 @@ class FileLeaseManager {
|
|
|
6183
6047
|
if (value.endsWith("/") || value.endsWith("\\")) {
|
|
6184
6048
|
throw new LeaseError("INVALID_PATH", `Lease paths must name files, not directories: ${value}`);
|
|
6185
6049
|
}
|
|
6186
|
-
const candidate =
|
|
6187
|
-
if ((!allowAbsolute || !
|
|
6050
|
+
const candidate = resolve3(this.projectRoot, value);
|
|
6051
|
+
if ((!allowAbsolute || !isAbsolute3(value)) && !isWithin(this.projectRoot, candidate)) {
|
|
6188
6052
|
throw new LeaseError("INVALID_PATH", `File must stay inside the project root: ${value}`);
|
|
6189
6053
|
}
|
|
6190
6054
|
const code = allowAbsolute ? "OUT_OF_SCOPE" : "INVALID_PATH";
|
|
6191
|
-
if (
|
|
6055
|
+
if (existsSync2(candidate)) {
|
|
6192
6056
|
try {
|
|
6193
|
-
const target =
|
|
6057
|
+
const target = lstatSync2(candidate);
|
|
6194
6058
|
if (!target.isSymbolicLink() && !target.isFile()) {
|
|
6195
6059
|
throw new LeaseError(code, `Lease targets must be regular files: ${value}`);
|
|
6196
6060
|
}
|
|
@@ -6203,8 +6067,8 @@ class FileLeaseManager {
|
|
|
6203
6067
|
}
|
|
6204
6068
|
let ancestor = candidate;
|
|
6205
6069
|
const suffix = [];
|
|
6206
|
-
while (!
|
|
6207
|
-
const parent =
|
|
6070
|
+
while (!existsSync2(ancestor)) {
|
|
6071
|
+
const parent = dirname2(ancestor);
|
|
6208
6072
|
if (parent === ancestor)
|
|
6209
6073
|
throw new LeaseError("INVALID_PATH", `Cannot resolve file path: ${value}`);
|
|
6210
6074
|
suffix.unshift(basename(ancestor));
|
|
@@ -6212,16 +6076,16 @@ class FileLeaseManager {
|
|
|
6212
6076
|
}
|
|
6213
6077
|
let canonicalAncestor;
|
|
6214
6078
|
try {
|
|
6215
|
-
canonicalAncestor =
|
|
6079
|
+
canonicalAncestor = realpathSync2(ancestor);
|
|
6216
6080
|
} catch {
|
|
6217
6081
|
throw new LeaseError("INVALID_PATH", `Cannot canonicalize file path: ${value}`);
|
|
6218
6082
|
}
|
|
6219
|
-
const canonical =
|
|
6083
|
+
const canonical = resolve3(canonicalAncestor, ...suffix);
|
|
6220
6084
|
if (!isWithin(this.projectRoot, canonical)) {
|
|
6221
6085
|
throw new LeaseError("INVALID_PATH", `File resolves outside the project root: ${value}`);
|
|
6222
6086
|
}
|
|
6223
6087
|
try {
|
|
6224
|
-
const target =
|
|
6088
|
+
const target = lstatSync2(canonical);
|
|
6225
6089
|
if (!target.isFile()) {
|
|
6226
6090
|
throw new LeaseError(code, `Lease targets must be regular files: ${value}`);
|
|
6227
6091
|
}
|
|
@@ -6253,6 +6117,243 @@ class FileLeaseManager {
|
|
|
6253
6117
|
}
|
|
6254
6118
|
}
|
|
6255
6119
|
|
|
6120
|
+
// src/config.ts
|
|
6121
|
+
var permissionSchema = object({
|
|
6122
|
+
action: string2().min(1),
|
|
6123
|
+
resource: string2().min(1),
|
|
6124
|
+
effect: _enum(["allow", "ask", "deny"])
|
|
6125
|
+
}).strict();
|
|
6126
|
+
var modelRefSchema = string2().min(1).regex(/^[^/#\s]+\/[^#\s]+(?:#[^#\s]+)?$/, "Expected provider/model or provider/model#variant");
|
|
6127
|
+
var agentIdSchema = string2().regex(/^[A-Za-z0-9][A-Za-z0-9_-]*$/, "Expected a filesystem-safe agent ID");
|
|
6128
|
+
var fileLeaseRoleSchema = _enum(["coordinator", "writer", "readonly"]);
|
|
6129
|
+
var agentPatchSchema = object({
|
|
6130
|
+
description: string2().min(1).optional(),
|
|
6131
|
+
mode: _enum(["primary", "subagent", "all"]).optional(),
|
|
6132
|
+
models: array(modelRefSchema).min(1).optional(),
|
|
6133
|
+
prompt: string2().min(1).optional(),
|
|
6134
|
+
skills: array(string2().min(1)).optional(),
|
|
6135
|
+
mcp: array(string2().min(1)).optional(),
|
|
6136
|
+
permissions: array(permissionSchema).optional(),
|
|
6137
|
+
fileLease: fileLeaseRoleSchema.optional(),
|
|
6138
|
+
disabled: boolean2().optional()
|
|
6139
|
+
}).strict();
|
|
6140
|
+
var leaseSchema = object({
|
|
6141
|
+
reservationTtlMinutes: number2().int().positive().max(24 * 60).optional(),
|
|
6142
|
+
activeTtlMinutes: number2().int().positive().max(24 * 60).optional()
|
|
6143
|
+
}).strict();
|
|
6144
|
+
var rootPatchSchema = object({
|
|
6145
|
+
$schema: string2().min(1).optional(),
|
|
6146
|
+
defaultAgent: agentIdSchema.optional(),
|
|
6147
|
+
agentsDirectory: string2().min(1).optional(),
|
|
6148
|
+
agents: record(agentIdSchema, agentPatchSchema).optional(),
|
|
6149
|
+
lease: leaseSchema.optional()
|
|
6150
|
+
}).strict();
|
|
6151
|
+
var resolvedAgentSchema = agentPatchSchema.extend({
|
|
6152
|
+
description: string2().min(1),
|
|
6153
|
+
mode: _enum(["primary", "subagent", "all"]),
|
|
6154
|
+
models: array(modelRefSchema).min(1),
|
|
6155
|
+
prompt: string2().min(1),
|
|
6156
|
+
skills: array(string2().min(1)),
|
|
6157
|
+
mcp: array(string2().min(1)),
|
|
6158
|
+
permissions: array(permissionSchema),
|
|
6159
|
+
fileLease: fileLeaseRoleSchema,
|
|
6160
|
+
disabled: boolean2()
|
|
6161
|
+
});
|
|
6162
|
+
var SAFE_UNTRUSTED_AGENT_FIELDS = new Set(["description"]);
|
|
6163
|
+
function assertTrustedProjectPatch(patch, sourcePath, id, trusted, knownAgents) {
|
|
6164
|
+
if (trusted)
|
|
6165
|
+
return;
|
|
6166
|
+
const fields = Object.keys(patch).filter((field) => !SAFE_UNTRUSTED_AGENT_FIELDS.has(field));
|
|
6167
|
+
if (fields.length === 0 && knownAgents.has(id))
|
|
6168
|
+
return;
|
|
6169
|
+
throw new Error(`Untrusted project config ${sourcePath} cannot override ${fields.join(", ") || `unknown agent ${id}`}. ` + `Set ${PROJECT_TRUST_ENV} to the exact token returned by computeProjectTrustToken() after reviewing these files.`);
|
|
6170
|
+
}
|
|
6171
|
+
function readJsonc2(path) {
|
|
6172
|
+
const errors = [];
|
|
6173
|
+
const value = parse2(readFileSync2(path, "utf8"), errors, {
|
|
6174
|
+
allowTrailingComma: true,
|
|
6175
|
+
disallowComments: false
|
|
6176
|
+
});
|
|
6177
|
+
if (errors.length > 0) {
|
|
6178
|
+
const details = errors.map((error) => `${printParseErrorCode(error.error)} at offset ${error.offset}`).join(", ");
|
|
6179
|
+
throw new Error(`Invalid JSONC in ${path}: ${details}`);
|
|
6180
|
+
}
|
|
6181
|
+
return value;
|
|
6182
|
+
}
|
|
6183
|
+
function assertWithin(base, target, label) {
|
|
6184
|
+
const child = relative3(base, target);
|
|
6185
|
+
if (child === "" || !child.startsWith("..") && !isAbsolute4(child))
|
|
6186
|
+
return;
|
|
6187
|
+
throw new Error(`${label} must stay inside ${base}: ${target}`);
|
|
6188
|
+
}
|
|
6189
|
+
function resolvePrompt(patch, sourcePath, layerDirectory) {
|
|
6190
|
+
if (!patch.prompt)
|
|
6191
|
+
return patch;
|
|
6192
|
+
const prompt = resolve4(dirname3(sourcePath), patch.prompt);
|
|
6193
|
+
assertWithin(layerDirectory, prompt, "Agent prompt");
|
|
6194
|
+
if (!existsSync3(prompt))
|
|
6195
|
+
throw new Error(`Agent prompt is missing: ${prompt}`);
|
|
6196
|
+
const promptStat = lstatSync3(prompt);
|
|
6197
|
+
if (promptStat.isSymbolicLink() || !promptStat.isFile())
|
|
6198
|
+
throw new Error(`Agent prompt must be a regular non-symlink file: ${prompt}`);
|
|
6199
|
+
const canonical = realpathSync3(prompt);
|
|
6200
|
+
assertWithin(realpathSync3(layerDirectory), canonical, "Agent prompt");
|
|
6201
|
+
const promptContent = readFileSync2(canonical, "utf8");
|
|
6202
|
+
return { ...patch, prompt: canonical, promptContent };
|
|
6203
|
+
}
|
|
6204
|
+
function loadLayer(directory, rootFileName, required, projectPolicy) {
|
|
6205
|
+
const rootPath = join3(directory, rootFileName);
|
|
6206
|
+
if (!existsSync3(rootPath)) {
|
|
6207
|
+
if (required)
|
|
6208
|
+
throw new Error(`Required config is missing: ${rootPath}`);
|
|
6209
|
+
return { agents: {}, sources: [] };
|
|
6210
|
+
}
|
|
6211
|
+
const root = rootPatchSchema.parse(readJsonc2(rootPath));
|
|
6212
|
+
if (projectPolicy && !projectPolicy.trusted) {
|
|
6213
|
+
const restricted = ["defaultAgent", "agentsDirectory", "lease"].filter((field) => Object.prototype.hasOwnProperty.call(root, field));
|
|
6214
|
+
if (restricted.length > 0)
|
|
6215
|
+
throw new Error(`Untrusted project config ${rootPath} cannot override ${restricted.join(", ")}`);
|
|
6216
|
+
}
|
|
6217
|
+
const agents = {};
|
|
6218
|
+
for (const [id, patch] of Object.entries(root.agents ?? {})) {
|
|
6219
|
+
if (projectPolicy)
|
|
6220
|
+
assertTrustedProjectPatch(patch, rootPath, id, projectPolicy.trusted, projectPolicy.knownAgents);
|
|
6221
|
+
agents[id] = resolvePrompt(patch, rootPath, directory);
|
|
6222
|
+
}
|
|
6223
|
+
const agentsDirectory = resolve4(directory, root.agentsDirectory ?? "agents");
|
|
6224
|
+
assertWithin(directory, agentsDirectory, "agentsDirectory");
|
|
6225
|
+
if (existsSync3(agentsDirectory)) {
|
|
6226
|
+
const directoryStat = lstatSync3(agentsDirectory);
|
|
6227
|
+
if (directoryStat.isSymbolicLink() || !directoryStat.isDirectory()) {
|
|
6228
|
+
throw new Error(`agentsDirectory must be a regular directory: ${agentsDirectory}`);
|
|
6229
|
+
}
|
|
6230
|
+
assertWithin(realpathSync3(directory), realpathSync3(agentsDirectory), "agentsDirectory");
|
|
6231
|
+
for (const entry of readdirSync2(agentsDirectory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
6232
|
+
if (!entry.isFile() || !entry.name.endsWith(".jsonc"))
|
|
6233
|
+
continue;
|
|
6234
|
+
const id = entry.name.slice(0, -".jsonc".length);
|
|
6235
|
+
agentIdSchema.parse(id);
|
|
6236
|
+
const agentPath = join3(agentsDirectory, entry.name);
|
|
6237
|
+
const patch = agentPatchSchema.parse(readJsonc2(agentPath));
|
|
6238
|
+
if (projectPolicy)
|
|
6239
|
+
assertTrustedProjectPatch(patch, agentPath, id, projectPolicy.trusted, projectPolicy.knownAgents);
|
|
6240
|
+
agents[id] = mergeAgent(agents[id], resolvePrompt(patch, agentPath, directory));
|
|
6241
|
+
}
|
|
6242
|
+
}
|
|
6243
|
+
return {
|
|
6244
|
+
defaultAgent: root.defaultAgent,
|
|
6245
|
+
agents,
|
|
6246
|
+
lease: root.lease,
|
|
6247
|
+
sources: [rootPath]
|
|
6248
|
+
};
|
|
6249
|
+
}
|
|
6250
|
+
function mergeAgent(base, override) {
|
|
6251
|
+
return { ...base ?? {}, ...override };
|
|
6252
|
+
}
|
|
6253
|
+
function resolveAgentConfig(patch) {
|
|
6254
|
+
const parsed = agentPatchSchema.extend({ promptContent: string2().optional() }).parse(patch);
|
|
6255
|
+
const { promptContent, ...agentPatch } = parsed;
|
|
6256
|
+
const resolved = resolvedAgentSchema.parse({
|
|
6257
|
+
skills: [],
|
|
6258
|
+
mcp: [],
|
|
6259
|
+
permissions: [],
|
|
6260
|
+
fileLease: "readonly",
|
|
6261
|
+
disabled: false,
|
|
6262
|
+
...agentPatch
|
|
6263
|
+
});
|
|
6264
|
+
return promptContent === undefined ? resolved : { ...resolved, promptContent };
|
|
6265
|
+
}
|
|
6266
|
+
function findPackageRoot() {
|
|
6267
|
+
let current = dirname3(fileURLToPath(import.meta.url));
|
|
6268
|
+
while (true) {
|
|
6269
|
+
if (existsSync3(join3(current, "defaults", "default.jsonc")))
|
|
6270
|
+
return current;
|
|
6271
|
+
const parent = dirname3(current);
|
|
6272
|
+
if (parent === current)
|
|
6273
|
+
break;
|
|
6274
|
+
current = parent;
|
|
6275
|
+
}
|
|
6276
|
+
throw new Error("Unable to locate agent-gvozd package defaults");
|
|
6277
|
+
}
|
|
6278
|
+
function findProjectRoot(start) {
|
|
6279
|
+
let current = resolve4(start);
|
|
6280
|
+
while (true) {
|
|
6281
|
+
const git = join3(current, ".git");
|
|
6282
|
+
if (existsSync3(git))
|
|
6283
|
+
return current;
|
|
6284
|
+
const parent = dirname3(current);
|
|
6285
|
+
if (parent === current)
|
|
6286
|
+
return resolve4(start);
|
|
6287
|
+
current = parent;
|
|
6288
|
+
}
|
|
6289
|
+
}
|
|
6290
|
+
function loadConfig(projectDirectory, options = {}) {
|
|
6291
|
+
const projectRoot = findProjectRoot(projectDirectory);
|
|
6292
|
+
const packageRoot = findPackageRoot();
|
|
6293
|
+
const projectConfigDirectory = join3(projectRoot, "docs", ".gvozd");
|
|
6294
|
+
const globalConfigDirectory = join3(resolveOpenCodeConfigRoot(options.env, options.platform, options.home, options.configRoot), "gvozd");
|
|
6295
|
+
const baseLayers = [
|
|
6296
|
+
loadLayer(join3(packageRoot, "defaults"), "default.jsonc", true),
|
|
6297
|
+
loadLayer(globalConfigDirectory, "config.jsonc", false)
|
|
6298
|
+
];
|
|
6299
|
+
const knownAgents = new Set(baseLayers.flatMap((layer) => Object.keys(layer.agents)));
|
|
6300
|
+
const includeProject = options.includeProject !== false;
|
|
6301
|
+
const suppliedToken = includeProject ? options.projectTrustToken ?? (options.env ?? process.env)[PROJECT_TRUST_ENV] : undefined;
|
|
6302
|
+
const trustProjectConfig = includeProject && suppliedToken !== undefined && suppliedToken === computeProjectTrustToken(projectRoot);
|
|
6303
|
+
const projectLayer = includeProject ? loadLayer(projectConfigDirectory, "config.jsonc", false, { trusted: trustProjectConfig, knownAgents }) : undefined;
|
|
6304
|
+
if (trustProjectConfig && suppliedToken !== computeProjectTrustToken(projectRoot)) {
|
|
6305
|
+
throw new Error("Project configuration changed while its trust token was being validated; review it and compute a new token");
|
|
6306
|
+
}
|
|
6307
|
+
const layers = [...baseLayers, ...projectLayer ? [projectLayer] : []];
|
|
6308
|
+
const lease = {
|
|
6309
|
+
reservationTtlMs: DEFAULT_RESERVATION_TTL_MS,
|
|
6310
|
+
activeTtlMs: DEFAULT_ACTIVE_TTL_MS
|
|
6311
|
+
};
|
|
6312
|
+
for (const layer of layers) {
|
|
6313
|
+
if (!layer.lease)
|
|
6314
|
+
continue;
|
|
6315
|
+
if (layer.lease.reservationTtlMinutes !== undefined) {
|
|
6316
|
+
lease.reservationTtlMs = layer.lease.reservationTtlMinutes * 60000;
|
|
6317
|
+
}
|
|
6318
|
+
if (layer.lease.activeTtlMinutes !== undefined) {
|
|
6319
|
+
lease.activeTtlMs = layer.lease.activeTtlMinutes * 60000;
|
|
6320
|
+
}
|
|
6321
|
+
}
|
|
6322
|
+
let defaultAgent;
|
|
6323
|
+
const agents = {};
|
|
6324
|
+
for (const layer of layers) {
|
|
6325
|
+
defaultAgent = layer.defaultAgent ?? defaultAgent;
|
|
6326
|
+
for (const [id, patch] of Object.entries(layer.agents)) {
|
|
6327
|
+
agents[id] = mergeAgent(agents[id], patch);
|
|
6328
|
+
}
|
|
6329
|
+
}
|
|
6330
|
+
if (!defaultAgent)
|
|
6331
|
+
throw new Error("defaultAgent is not configured");
|
|
6332
|
+
const resolvedAgents = Object.fromEntries(Object.entries(agents).map(([id, patch]) => [
|
|
6333
|
+
id,
|
|
6334
|
+
(() => {
|
|
6335
|
+
const agent = resolveAgentConfig(patch);
|
|
6336
|
+
if (agent.promptContent === undefined)
|
|
6337
|
+
throw new Error(`Agent prompt snapshot is missing after configuration load: ${agent.prompt}`);
|
|
6338
|
+
return agent;
|
|
6339
|
+
})()
|
|
6340
|
+
]));
|
|
6341
|
+
const defaultConfig = resolvedAgents[defaultAgent];
|
|
6342
|
+
if (!defaultConfig || defaultConfig.disabled || defaultConfig.mode === "subagent") {
|
|
6343
|
+
throw new Error(`defaultAgent must reference an enabled primary agent: ${defaultAgent}`);
|
|
6344
|
+
}
|
|
6345
|
+
return {
|
|
6346
|
+
defaultAgent,
|
|
6347
|
+
agents: resolvedAgents,
|
|
6348
|
+
lease,
|
|
6349
|
+
packageRoot,
|
|
6350
|
+
projectRoot,
|
|
6351
|
+
projectConfigDirectory,
|
|
6352
|
+
globalConfigDirectory,
|
|
6353
|
+
sources: layers.flatMap((layer) => layer.sources)
|
|
6354
|
+
};
|
|
6355
|
+
}
|
|
6356
|
+
|
|
6256
6357
|
// src/file-lease-plugin.ts
|
|
6257
6358
|
var GVOZD_LEASE_TOOL = "gvozd_lease";
|
|
6258
6359
|
var GVOZD_CLAIM_TOOL = "gvozd_claim";
|
|
@@ -6263,26 +6364,12 @@ var TERMINAL_SESSION_EVENTS = new Set([
|
|
|
6263
6364
|
"session.idle",
|
|
6264
6365
|
"session.deleted"
|
|
6265
6366
|
]);
|
|
6266
|
-
var SAFE_SHELL_DURING_ACTIVE_LEASES = new Set([
|
|
6267
|
-
|
|
6268
|
-
|
|
6269
|
-
|
|
6270
|
-
|
|
6271
|
-
|
|
6272
|
-
"git rev-parse --git-dir",
|
|
6273
|
-
"git remote -v",
|
|
6274
|
-
"git branch --show-current",
|
|
6275
|
-
"git stash list",
|
|
6276
|
-
"GIT_OPTIONAL_LOCKS=0 git status",
|
|
6277
|
-
"GIT_OPTIONAL_LOCKS=0 git status --short",
|
|
6278
|
-
"GIT_OPTIONAL_LOCKS=0 git status --short --branch",
|
|
6279
|
-
"GIT_OPTIONAL_LOCKS=0 git status --porcelain=v1 --branch",
|
|
6280
|
-
"GIT_OPTIONAL_LOCKS=0 git rev-parse --show-toplevel",
|
|
6281
|
-
"GIT_OPTIONAL_LOCKS=0 git rev-parse --git-dir",
|
|
6282
|
-
"GIT_OPTIONAL_LOCKS=0 git remote -v",
|
|
6283
|
-
"GIT_OPTIONAL_LOCKS=0 git branch --show-current",
|
|
6284
|
-
"GIT_OPTIONAL_LOCKS=0 git stash list"
|
|
6285
|
-
]);
|
|
6367
|
+
var SAFE_SHELL_DURING_ACTIVE_LEASES = new Set([...INSPECTION_COMMANDS, ...TOOLCHAIN_COMMANDS, ...GIT_READONLY_COMMANDS].flatMap(({ exact, wildcard }) => [
|
|
6368
|
+
exact,
|
|
6369
|
+
wildcard,
|
|
6370
|
+
...GIT_ENV_PREFIXES.map((prefix) => `${prefix} ${exact}`),
|
|
6371
|
+
...GIT_ENV_PREFIXES.map((prefix) => `${prefix} ${wildcard}`)
|
|
6372
|
+
]));
|
|
6286
6373
|
var leaseInputSchema = discriminatedUnion("operation", [
|
|
6287
6374
|
object({
|
|
6288
6375
|
operation: literal("reserve"),
|
|
@@ -6332,8 +6419,8 @@ function deny(event, message) {
|
|
|
6332
6419
|
event.message = message;
|
|
6333
6420
|
return true;
|
|
6334
6421
|
}
|
|
6335
|
-
function
|
|
6336
|
-
return agent
|
|
6422
|
+
function safeReadonlyShell(agent, resources) {
|
|
6423
|
+
return agent !== undefined && resources.length > 0 && resources.every((resource) => SAFE_SHELL_DURING_ACTIVE_LEASES.has(resource));
|
|
6337
6424
|
}
|
|
6338
6425
|
function enforceFileLeasePermission(event, config, manager) {
|
|
6339
6426
|
const role = roleOf(config, event.agent);
|
|
@@ -6356,7 +6443,7 @@ function enforceFileLeasePermission(event, config, manager) {
|
|
|
6356
6443
|
if (role === "coordinator" || role === "writer") {
|
|
6357
6444
|
return deny(event, `Agent ${event.agent} cannot use shell while file leases enforce structured mutations`);
|
|
6358
6445
|
}
|
|
6359
|
-
if (manager.hasActiveLeases() && !(role === "readonly" &&
|
|
6446
|
+
if (manager.hasActiveLeases() && !(role === "readonly" && safeReadonlyShell(event.agent, event.resources))) {
|
|
6360
6447
|
return deny(event, "Shell commands are paused until all active writer file leases are released");
|
|
6361
6448
|
}
|
|
6362
6449
|
return false;
|
|
@@ -6382,7 +6469,12 @@ function requireRole(config, agentID, allowed) {
|
|
|
6382
6469
|
}
|
|
6383
6470
|
async function installFileLeaseRuntime(ctx, config, options = {}) {
|
|
6384
6471
|
const caseInsensitive = options.caseInsensitive ?? resolveCaseInsensitiveFilesystem(options.env ?? process.env, options.platform ?? process.platform);
|
|
6385
|
-
const manager = new FileLeaseManager({
|
|
6472
|
+
const manager = new FileLeaseManager({
|
|
6473
|
+
projectRoot: config.projectRoot,
|
|
6474
|
+
caseInsensitive,
|
|
6475
|
+
reservationTtlMs: config.lease.reservationTtlMs,
|
|
6476
|
+
activeTtlMs: config.lease.activeTtlMs
|
|
6477
|
+
});
|
|
6386
6478
|
const toolTransform = await ctx.tool.transform((tools) => {
|
|
6387
6479
|
tools.namespace({
|
|
6388
6480
|
name: "gvozd",
|