@nail00749/agent-gvozd 0.1.2 → 0.1.3

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/dist/index.js CHANGED
@@ -2,6 +2,69 @@
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
+ var INSPECTION_COMMANDS = [
13
+ ...family("pwd", "true", "test"),
14
+ ...family("cat", "head", "tail", "wc", "sort", "uniq"),
15
+ ...family("grep", "rg", "find", "diff", "cmp"),
16
+ ...family("ls", "du", "df", "stat", "file", "realpath", "basename", "dirname"),
17
+ ...family("shasum", "sha256sum", "md5sum"),
18
+ ...family("uname", "whoami", "hostname", "date", "printenv"),
19
+ ...family("which", "command -v"),
20
+ ...family("mktemp"),
21
+ ...family("tr", "cut", "paste", "column"),
22
+ ...family("node --version", "python3 --version", "python --version", "deno --version")
23
+ ];
24
+ var TOOLCHAIN_COMMANDS = [
25
+ ...family("bun test", "bun run test", "bun --version"),
26
+ ...family("bun run typecheck", "bun run lint", "bun run build", "bun run check"),
27
+ ...family("tsc --noEmit", "npx tsc --noEmit"),
28
+ ...family("eslint", "biome check", "prettier --check"),
29
+ ...family("npm test", "npm run test", "npm run typecheck", "npm run lint", "npm run build"),
30
+ ...family("pnpm test", "pnpm run test", "pnpm run build"),
31
+ ...family("yarn test", "yarn build"),
32
+ ...family("vitest run", "jest", "playwright test"),
33
+ ...family("cargo check", "cargo test", "cargo build", "cargo clippy", "cargo fmt --check", "cargo --version"),
34
+ ...family("go build ./...", "go test ./...", "go vet ./...", "go version"),
35
+ ...family("pytest", "python3 -m pytest", "python -m pytest"),
36
+ ...family("ruff check", "mypy", "pyright"),
37
+ ...family("mvn test", "mvn verify", "gradle test", "gradle check", "./gradlew test", "./gradlew check"),
38
+ ...family("make test", "make check", "make build", "make --version"),
39
+ ...family("just --list")
40
+ ];
41
+ var GIT_READONLY_COMMANDS = [
42
+ ...family("git status", "git status --short", "git status --short --branch", "git status --porcelain", "git status --porcelain=v1 --branch"),
43
+ ...family("git diff", "git diff --stat", "git diff --cached", "git diff --check"),
44
+ ...family("git log", "git show", "git reflog"),
45
+ ...family("git rev-parse", "git rev-list", "git show-ref", "git cat-file", "git symbolic-ref"),
46
+ ...family("git ls-files", "git ls-remote", "git grep"),
47
+ ...family("git branch", "git remote", "git stash list", "git tag", "git describe"),
48
+ ...family("git config --get", "git config --get-regexp"),
49
+ ...family("git -C")
50
+ ];
51
+ var GIT_MUTATING_COMMANDS = [
52
+ ...family("git add", "git rm --cached"),
53
+ ...family("git commit", "git merge --ff-only", "git merge --no-ff"),
54
+ ...family("git push", "git fetch", "git pull --ff-only"),
55
+ ...family("git tag -a", "git tag -v", "git tag --list"),
56
+ ...family("git stash", "git cherry-pick", "git revert"),
57
+ ...family("git switch", "git checkout -b", "git worktree list", "git worktree add")
58
+ ];
59
+ var GIT_ENV_PREFIXES = ["GIT_OPTIONAL_LOCKS=0"];
60
+ function withEnvPrefixes(rule, prefixes = GIT_ENV_PREFIXES) {
61
+ return prefixes.map((prefix) => ({
62
+ action: rule.action,
63
+ resource: `${prefix} ${rule.resource}`,
64
+ effect: rule.effect
65
+ }));
66
+ }
67
+
5
68
  // src/agent-permissions.ts
6
69
  function normalizeMcpName(name) {
7
70
  return name.replaceAll(/[^A-Za-z0-9_-]/g, "_");
@@ -45,12 +108,21 @@ function buildAgentPermissions(agent, mcpServers) {
45
108
  });
46
109
  result.push(...agent.permissions.filter((rule) => rule.action.startsWith(prefix)));
47
110
  }
111
+ for (const rule of agent.permissions) {
112
+ if (rule.action !== "shell")
113
+ continue;
114
+ if (!/(^|\s|["'])git(?:$|\s)/.test(rule.resource))
115
+ continue;
116
+ if (rule.resource.startsWith("GIT_"))
117
+ continue;
118
+ result.push(...withEnvPrefixes(rule));
119
+ }
48
120
  return result;
49
121
  }
50
122
 
51
123
  // src/config.ts
52
- import { existsSync as existsSync2, lstatSync as lstatSync2, readdirSync as readdirSync2, readFileSync as readFileSync2, realpathSync as realpathSync2 } from "fs";
53
- import { dirname as dirname2, isAbsolute as isAbsolute3, join as join3, relative as relative2, resolve as resolve3 } from "path";
124
+ import { existsSync as existsSync3, lstatSync as lstatSync3, readdirSync as readdirSync2, readFileSync as readFileSync2, realpathSync as realpathSync3 } from "fs";
125
+ import { dirname as dirname3, isAbsolute as isAbsolute4, join as join3, relative as relative3, resolve as resolve4 } from "path";
54
126
  import { fileURLToPath } from "url";
55
127
 
56
128
  // node_modules/jsonc-parser/lib/esm/impl/scanner.js
@@ -5686,226 +5758,10 @@ function computeProjectTrustToken(projectDirectory) {
5686
5758
  return `sha256:${hash.digest("hex")}`;
5687
5759
  }
5688
5760
 
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
5761
  // src/file-leases.ts
5906
5762
  import { randomUUID } from "crypto";
5907
- import { existsSync as existsSync3, lstatSync as lstatSync3, realpathSync as realpathSync3, statSync } from "fs";
5908
- import { basename, dirname as dirname3, isAbsolute as isAbsolute4, relative as relative3, resolve as resolve4, sep } from "path";
5763
+ import { existsSync as existsSync2, lstatSync as lstatSync2, realpathSync as realpathSync2, statSync } from "fs";
5764
+ import { basename, dirname as dirname2, isAbsolute as isAbsolute3, relative as relative2, resolve as resolve3, sep } from "path";
5909
5765
 
5910
5766
  class LeaseError extends Error {
5911
5767
  code;
@@ -5941,11 +5797,11 @@ function assertPositiveDuration(value, label) {
5941
5797
  return value;
5942
5798
  }
5943
5799
  function isWithin(root, target) {
5944
- const child = relative3(root, target);
5945
- return child !== "" && !child.startsWith(`..${sep}`) && child !== ".." && !isAbsolute4(child);
5800
+ const child = relative2(root, target);
5801
+ return child !== "" && !child.startsWith(`..${sep}`) && child !== ".." && !isAbsolute3(child);
5946
5802
  }
5947
5803
  function displayPath(root, target) {
5948
- return relative3(root, target).replaceAll("\\", "/");
5804
+ return relative2(root, target).replaceAll("\\", "/");
5949
5805
  }
5950
5806
 
5951
5807
  class FileLeaseManager {
@@ -5959,7 +5815,7 @@ class FileLeaseManager {
5959
5815
  fileOwners = new Map;
5960
5816
  sessionOwners = new Map;
5961
5817
  constructor(options) {
5962
- this.projectRoot = realpathSync3(options.projectRoot);
5818
+ this.projectRoot = realpathSync2(options.projectRoot);
5963
5819
  if (!statSync(this.projectRoot).isDirectory())
5964
5820
  throw new Error(`Project root is not a directory: ${this.projectRoot}`);
5965
5821
  this.reservationTtlMs = assertPositiveDuration(options.reservationTtlMs ?? DEFAULT_RESERVATION_TTL_MS, "reservationTtlMs");
@@ -6175,7 +6031,7 @@ class FileLeaseManager {
6175
6031
  const value = input.trim();
6176
6032
  if (value === "" || value.includes("\x00"))
6177
6033
  throw new LeaseError("INVALID_PATH", "File path must not be empty");
6178
- if (!allowAbsolute && isAbsolute4(value))
6034
+ if (!allowAbsolute && isAbsolute3(value))
6179
6035
  throw new LeaseError("INVALID_PATH", `Absolute lease path is not allowed: ${value}`);
6180
6036
  if (value.includes("*") || value.includes("?")) {
6181
6037
  throw new LeaseError("INVALID_PATH", `Lease paths must name exact files, not patterns: ${value}`);
@@ -6183,14 +6039,14 @@ class FileLeaseManager {
6183
6039
  if (value.endsWith("/") || value.endsWith("\\")) {
6184
6040
  throw new LeaseError("INVALID_PATH", `Lease paths must name files, not directories: ${value}`);
6185
6041
  }
6186
- const candidate = resolve4(this.projectRoot, value);
6187
- if ((!allowAbsolute || !isAbsolute4(value)) && !isWithin(this.projectRoot, candidate)) {
6042
+ const candidate = resolve3(this.projectRoot, value);
6043
+ if ((!allowAbsolute || !isAbsolute3(value)) && !isWithin(this.projectRoot, candidate)) {
6188
6044
  throw new LeaseError("INVALID_PATH", `File must stay inside the project root: ${value}`);
6189
6045
  }
6190
6046
  const code = allowAbsolute ? "OUT_OF_SCOPE" : "INVALID_PATH";
6191
- if (existsSync3(candidate)) {
6047
+ if (existsSync2(candidate)) {
6192
6048
  try {
6193
- const target = lstatSync3(candidate);
6049
+ const target = lstatSync2(candidate);
6194
6050
  if (!target.isSymbolicLink() && !target.isFile()) {
6195
6051
  throw new LeaseError(code, `Lease targets must be regular files: ${value}`);
6196
6052
  }
@@ -6203,8 +6059,8 @@ class FileLeaseManager {
6203
6059
  }
6204
6060
  let ancestor = candidate;
6205
6061
  const suffix = [];
6206
- while (!existsSync3(ancestor)) {
6207
- const parent = dirname3(ancestor);
6062
+ while (!existsSync2(ancestor)) {
6063
+ const parent = dirname2(ancestor);
6208
6064
  if (parent === ancestor)
6209
6065
  throw new LeaseError("INVALID_PATH", `Cannot resolve file path: ${value}`);
6210
6066
  suffix.unshift(basename(ancestor));
@@ -6212,16 +6068,16 @@ class FileLeaseManager {
6212
6068
  }
6213
6069
  let canonicalAncestor;
6214
6070
  try {
6215
- canonicalAncestor = realpathSync3(ancestor);
6071
+ canonicalAncestor = realpathSync2(ancestor);
6216
6072
  } catch {
6217
6073
  throw new LeaseError("INVALID_PATH", `Cannot canonicalize file path: ${value}`);
6218
6074
  }
6219
- const canonical = resolve4(canonicalAncestor, ...suffix);
6075
+ const canonical = resolve3(canonicalAncestor, ...suffix);
6220
6076
  if (!isWithin(this.projectRoot, canonical)) {
6221
6077
  throw new LeaseError("INVALID_PATH", `File resolves outside the project root: ${value}`);
6222
6078
  }
6223
6079
  try {
6224
- const target = lstatSync3(canonical);
6080
+ const target = lstatSync2(canonical);
6225
6081
  if (!target.isFile()) {
6226
6082
  throw new LeaseError(code, `Lease targets must be regular files: ${value}`);
6227
6083
  }
@@ -6253,6 +6109,243 @@ class FileLeaseManager {
6253
6109
  }
6254
6110
  }
6255
6111
 
6112
+ // src/config.ts
6113
+ var permissionSchema = object({
6114
+ action: string2().min(1),
6115
+ resource: string2().min(1),
6116
+ effect: _enum(["allow", "ask", "deny"])
6117
+ }).strict();
6118
+ var modelRefSchema = string2().min(1).regex(/^[^/#\s]+\/[^#\s]+(?:#[^#\s]+)?$/, "Expected provider/model or provider/model#variant");
6119
+ var agentIdSchema = string2().regex(/^[A-Za-z0-9][A-Za-z0-9_-]*$/, "Expected a filesystem-safe agent ID");
6120
+ var fileLeaseRoleSchema = _enum(["coordinator", "writer", "readonly"]);
6121
+ var agentPatchSchema = object({
6122
+ description: string2().min(1).optional(),
6123
+ mode: _enum(["primary", "subagent", "all"]).optional(),
6124
+ models: array(modelRefSchema).min(1).optional(),
6125
+ prompt: string2().min(1).optional(),
6126
+ skills: array(string2().min(1)).optional(),
6127
+ mcp: array(string2().min(1)).optional(),
6128
+ permissions: array(permissionSchema).optional(),
6129
+ fileLease: fileLeaseRoleSchema.optional(),
6130
+ disabled: boolean2().optional()
6131
+ }).strict();
6132
+ var leaseSchema = object({
6133
+ reservationTtlMinutes: number2().int().positive().max(24 * 60).optional(),
6134
+ activeTtlMinutes: number2().int().positive().max(24 * 60).optional()
6135
+ }).strict();
6136
+ var rootPatchSchema = object({
6137
+ $schema: string2().min(1).optional(),
6138
+ defaultAgent: agentIdSchema.optional(),
6139
+ agentsDirectory: string2().min(1).optional(),
6140
+ agents: record(agentIdSchema, agentPatchSchema).optional(),
6141
+ lease: leaseSchema.optional()
6142
+ }).strict();
6143
+ var resolvedAgentSchema = agentPatchSchema.extend({
6144
+ description: string2().min(1),
6145
+ mode: _enum(["primary", "subagent", "all"]),
6146
+ models: array(modelRefSchema).min(1),
6147
+ prompt: string2().min(1),
6148
+ skills: array(string2().min(1)),
6149
+ mcp: array(string2().min(1)),
6150
+ permissions: array(permissionSchema),
6151
+ fileLease: fileLeaseRoleSchema,
6152
+ disabled: boolean2()
6153
+ });
6154
+ var SAFE_UNTRUSTED_AGENT_FIELDS = new Set(["description"]);
6155
+ function assertTrustedProjectPatch(patch, sourcePath, id, trusted, knownAgents) {
6156
+ if (trusted)
6157
+ return;
6158
+ const fields = Object.keys(patch).filter((field) => !SAFE_UNTRUSTED_AGENT_FIELDS.has(field));
6159
+ if (fields.length === 0 && knownAgents.has(id))
6160
+ return;
6161
+ 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.`);
6162
+ }
6163
+ function readJsonc2(path) {
6164
+ const errors = [];
6165
+ const value = parse2(readFileSync2(path, "utf8"), errors, {
6166
+ allowTrailingComma: true,
6167
+ disallowComments: false
6168
+ });
6169
+ if (errors.length > 0) {
6170
+ const details = errors.map((error) => `${printParseErrorCode(error.error)} at offset ${error.offset}`).join(", ");
6171
+ throw new Error(`Invalid JSONC in ${path}: ${details}`);
6172
+ }
6173
+ return value;
6174
+ }
6175
+ function assertWithin(base, target, label) {
6176
+ const child = relative3(base, target);
6177
+ if (child === "" || !child.startsWith("..") && !isAbsolute4(child))
6178
+ return;
6179
+ throw new Error(`${label} must stay inside ${base}: ${target}`);
6180
+ }
6181
+ function resolvePrompt(patch, sourcePath, layerDirectory) {
6182
+ if (!patch.prompt)
6183
+ return patch;
6184
+ const prompt = resolve4(dirname3(sourcePath), patch.prompt);
6185
+ assertWithin(layerDirectory, prompt, "Agent prompt");
6186
+ if (!existsSync3(prompt))
6187
+ throw new Error(`Agent prompt is missing: ${prompt}`);
6188
+ const promptStat = lstatSync3(prompt);
6189
+ if (promptStat.isSymbolicLink() || !promptStat.isFile())
6190
+ throw new Error(`Agent prompt must be a regular non-symlink file: ${prompt}`);
6191
+ const canonical = realpathSync3(prompt);
6192
+ assertWithin(realpathSync3(layerDirectory), canonical, "Agent prompt");
6193
+ const promptContent = readFileSync2(canonical, "utf8");
6194
+ return { ...patch, prompt: canonical, promptContent };
6195
+ }
6196
+ function loadLayer(directory, rootFileName, required, projectPolicy) {
6197
+ const rootPath = join3(directory, rootFileName);
6198
+ if (!existsSync3(rootPath)) {
6199
+ if (required)
6200
+ throw new Error(`Required config is missing: ${rootPath}`);
6201
+ return { agents: {}, sources: [] };
6202
+ }
6203
+ const root = rootPatchSchema.parse(readJsonc2(rootPath));
6204
+ if (projectPolicy && !projectPolicy.trusted) {
6205
+ const restricted = ["defaultAgent", "agentsDirectory", "lease"].filter((field) => Object.prototype.hasOwnProperty.call(root, field));
6206
+ if (restricted.length > 0)
6207
+ throw new Error(`Untrusted project config ${rootPath} cannot override ${restricted.join(", ")}`);
6208
+ }
6209
+ const agents = {};
6210
+ for (const [id, patch] of Object.entries(root.agents ?? {})) {
6211
+ if (projectPolicy)
6212
+ assertTrustedProjectPatch(patch, rootPath, id, projectPolicy.trusted, projectPolicy.knownAgents);
6213
+ agents[id] = resolvePrompt(patch, rootPath, directory);
6214
+ }
6215
+ const agentsDirectory = resolve4(directory, root.agentsDirectory ?? "agents");
6216
+ assertWithin(directory, agentsDirectory, "agentsDirectory");
6217
+ if (existsSync3(agentsDirectory)) {
6218
+ const directoryStat = lstatSync3(agentsDirectory);
6219
+ if (directoryStat.isSymbolicLink() || !directoryStat.isDirectory()) {
6220
+ throw new Error(`agentsDirectory must be a regular directory: ${agentsDirectory}`);
6221
+ }
6222
+ assertWithin(realpathSync3(directory), realpathSync3(agentsDirectory), "agentsDirectory");
6223
+ for (const entry of readdirSync2(agentsDirectory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
6224
+ if (!entry.isFile() || !entry.name.endsWith(".jsonc"))
6225
+ continue;
6226
+ const id = entry.name.slice(0, -".jsonc".length);
6227
+ agentIdSchema.parse(id);
6228
+ const agentPath = join3(agentsDirectory, entry.name);
6229
+ const patch = agentPatchSchema.parse(readJsonc2(agentPath));
6230
+ if (projectPolicy)
6231
+ assertTrustedProjectPatch(patch, agentPath, id, projectPolicy.trusted, projectPolicy.knownAgents);
6232
+ agents[id] = mergeAgent(agents[id], resolvePrompt(patch, agentPath, directory));
6233
+ }
6234
+ }
6235
+ return {
6236
+ defaultAgent: root.defaultAgent,
6237
+ agents,
6238
+ lease: root.lease,
6239
+ sources: [rootPath]
6240
+ };
6241
+ }
6242
+ function mergeAgent(base, override) {
6243
+ return { ...base ?? {}, ...override };
6244
+ }
6245
+ function resolveAgentConfig(patch) {
6246
+ const parsed = agentPatchSchema.extend({ promptContent: string2().optional() }).parse(patch);
6247
+ const { promptContent, ...agentPatch } = parsed;
6248
+ const resolved = resolvedAgentSchema.parse({
6249
+ skills: [],
6250
+ mcp: [],
6251
+ permissions: [],
6252
+ fileLease: "readonly",
6253
+ disabled: false,
6254
+ ...agentPatch
6255
+ });
6256
+ return promptContent === undefined ? resolved : { ...resolved, promptContent };
6257
+ }
6258
+ function findPackageRoot() {
6259
+ let current = dirname3(fileURLToPath(import.meta.url));
6260
+ while (true) {
6261
+ if (existsSync3(join3(current, "defaults", "default.jsonc")))
6262
+ return current;
6263
+ const parent = dirname3(current);
6264
+ if (parent === current)
6265
+ break;
6266
+ current = parent;
6267
+ }
6268
+ throw new Error("Unable to locate agent-gvozd package defaults");
6269
+ }
6270
+ function findProjectRoot(start) {
6271
+ let current = resolve4(start);
6272
+ while (true) {
6273
+ const git = join3(current, ".git");
6274
+ if (existsSync3(git))
6275
+ return current;
6276
+ const parent = dirname3(current);
6277
+ if (parent === current)
6278
+ return resolve4(start);
6279
+ current = parent;
6280
+ }
6281
+ }
6282
+ function loadConfig(projectDirectory, options = {}) {
6283
+ const projectRoot = findProjectRoot(projectDirectory);
6284
+ const packageRoot = findPackageRoot();
6285
+ const projectConfigDirectory = join3(projectRoot, "docs", ".gvozd");
6286
+ const globalConfigDirectory = join3(resolveOpenCodeConfigRoot(options.env, options.platform, options.home, options.configRoot), "gvozd");
6287
+ const baseLayers = [
6288
+ loadLayer(join3(packageRoot, "defaults"), "default.jsonc", true),
6289
+ loadLayer(globalConfigDirectory, "config.jsonc", false)
6290
+ ];
6291
+ const knownAgents = new Set(baseLayers.flatMap((layer) => Object.keys(layer.agents)));
6292
+ const includeProject = options.includeProject !== false;
6293
+ const suppliedToken = includeProject ? options.projectTrustToken ?? (options.env ?? process.env)[PROJECT_TRUST_ENV] : undefined;
6294
+ const trustProjectConfig = includeProject && suppliedToken !== undefined && suppliedToken === computeProjectTrustToken(projectRoot);
6295
+ const projectLayer = includeProject ? loadLayer(projectConfigDirectory, "config.jsonc", false, { trusted: trustProjectConfig, knownAgents }) : undefined;
6296
+ if (trustProjectConfig && suppliedToken !== computeProjectTrustToken(projectRoot)) {
6297
+ throw new Error("Project configuration changed while its trust token was being validated; review it and compute a new token");
6298
+ }
6299
+ const layers = [...baseLayers, ...projectLayer ? [projectLayer] : []];
6300
+ const lease = {
6301
+ reservationTtlMs: DEFAULT_RESERVATION_TTL_MS,
6302
+ activeTtlMs: DEFAULT_ACTIVE_TTL_MS
6303
+ };
6304
+ for (const layer of layers) {
6305
+ if (!layer.lease)
6306
+ continue;
6307
+ if (layer.lease.reservationTtlMinutes !== undefined) {
6308
+ lease.reservationTtlMs = layer.lease.reservationTtlMinutes * 60000;
6309
+ }
6310
+ if (layer.lease.activeTtlMinutes !== undefined) {
6311
+ lease.activeTtlMs = layer.lease.activeTtlMinutes * 60000;
6312
+ }
6313
+ }
6314
+ let defaultAgent;
6315
+ const agents = {};
6316
+ for (const layer of layers) {
6317
+ defaultAgent = layer.defaultAgent ?? defaultAgent;
6318
+ for (const [id, patch] of Object.entries(layer.agents)) {
6319
+ agents[id] = mergeAgent(agents[id], patch);
6320
+ }
6321
+ }
6322
+ if (!defaultAgent)
6323
+ throw new Error("defaultAgent is not configured");
6324
+ const resolvedAgents = Object.fromEntries(Object.entries(agents).map(([id, patch]) => [
6325
+ id,
6326
+ (() => {
6327
+ const agent = resolveAgentConfig(patch);
6328
+ if (agent.promptContent === undefined)
6329
+ throw new Error(`Agent prompt snapshot is missing after configuration load: ${agent.prompt}`);
6330
+ return agent;
6331
+ })()
6332
+ ]));
6333
+ const defaultConfig = resolvedAgents[defaultAgent];
6334
+ if (!defaultConfig || defaultConfig.disabled || defaultConfig.mode === "subagent") {
6335
+ throw new Error(`defaultAgent must reference an enabled primary agent: ${defaultAgent}`);
6336
+ }
6337
+ return {
6338
+ defaultAgent,
6339
+ agents: resolvedAgents,
6340
+ lease,
6341
+ packageRoot,
6342
+ projectRoot,
6343
+ projectConfigDirectory,
6344
+ globalConfigDirectory,
6345
+ sources: layers.flatMap((layer) => layer.sources)
6346
+ };
6347
+ }
6348
+
6256
6349
  // src/file-lease-plugin.ts
6257
6350
  var GVOZD_LEASE_TOOL = "gvozd_lease";
6258
6351
  var GVOZD_CLAIM_TOOL = "gvozd_claim";
@@ -6263,26 +6356,12 @@ var TERMINAL_SESSION_EVENTS = new Set([
6263
6356
  "session.idle",
6264
6357
  "session.deleted"
6265
6358
  ]);
6266
- var SAFE_SHELL_DURING_ACTIVE_LEASES = new Set([
6267
- "git status",
6268
- "git status --short",
6269
- "git status --short --branch",
6270
- "git status --porcelain=v1 --branch",
6271
- "git rev-parse --show-toplevel",
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
- ]);
6359
+ var SAFE_SHELL_DURING_ACTIVE_LEASES = new Set([...INSPECTION_COMMANDS, ...TOOLCHAIN_COMMANDS, ...GIT_READONLY_COMMANDS].flatMap(({ exact, wildcard }) => [
6360
+ exact,
6361
+ wildcard,
6362
+ ...GIT_ENV_PREFIXES.map((prefix) => `${prefix} ${exact}`),
6363
+ ...GIT_ENV_PREFIXES.map((prefix) => `${prefix} ${wildcard}`)
6364
+ ]));
6286
6365
  var leaseInputSchema = discriminatedUnion("operation", [
6287
6366
  object({
6288
6367
  operation: literal("reserve"),
@@ -6332,8 +6411,8 @@ function deny(event, message) {
6332
6411
  event.message = message;
6333
6412
  return true;
6334
6413
  }
6335
- function safeGitInspection(agent, resources) {
6336
- return agent === "git" && resources.length > 0 && resources.every((resource) => SAFE_SHELL_DURING_ACTIVE_LEASES.has(resource));
6414
+ function safeReadonlyShell(agent, resources) {
6415
+ return agent !== undefined && resources.length > 0 && resources.every((resource) => SAFE_SHELL_DURING_ACTIVE_LEASES.has(resource));
6337
6416
  }
6338
6417
  function enforceFileLeasePermission(event, config, manager) {
6339
6418
  const role = roleOf(config, event.agent);
@@ -6356,7 +6435,7 @@ function enforceFileLeasePermission(event, config, manager) {
6356
6435
  if (role === "coordinator" || role === "writer") {
6357
6436
  return deny(event, `Agent ${event.agent} cannot use shell while file leases enforce structured mutations`);
6358
6437
  }
6359
- if (manager.hasActiveLeases() && !(role === "readonly" && safeGitInspection(event.agent, event.resources))) {
6438
+ if (manager.hasActiveLeases() && !(role === "readonly" && safeReadonlyShell(event.agent, event.resources))) {
6360
6439
  return deny(event, "Shell commands are paused until all active writer file leases are released");
6361
6440
  }
6362
6441
  return false;
@@ -6382,7 +6461,12 @@ function requireRole(config, agentID, allowed) {
6382
6461
  }
6383
6462
  async function installFileLeaseRuntime(ctx, config, options = {}) {
6384
6463
  const caseInsensitive = options.caseInsensitive ?? resolveCaseInsensitiveFilesystem(options.env ?? process.env, options.platform ?? process.platform);
6385
- const manager = new FileLeaseManager({ projectRoot: config.projectRoot, caseInsensitive });
6464
+ const manager = new FileLeaseManager({
6465
+ projectRoot: config.projectRoot,
6466
+ caseInsensitive,
6467
+ reservationTtlMs: config.lease.reservationTtlMs,
6468
+ activeTtlMs: config.lease.activeTtlMs
6469
+ });
6386
6470
  const toolTransform = await ctx.tool.transform((tools) => {
6387
6471
  tools.namespace({
6388
6472
  name: "gvozd",