@nail00749/agent-gvozd 0.1.1 → 0.1.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/dist/index.js CHANGED
@@ -1,12 +1,56 @@
1
1
  // @bun
2
2
  // src/index.ts
3
- import { readFileSync as readFileSync2 } from "fs";
4
3
  import { Model, Plugin } from "@opencode/plugin";
5
4
 
5
+ // src/agent-permissions.ts
6
+ function normalizeMcpName(name) {
7
+ return name.replaceAll(/[^A-Za-z0-9_-]/g, "_");
8
+ }
9
+ function wildcardMatch(pattern, value) {
10
+ let source = "^";
11
+ for (const character of pattern) {
12
+ if (character === "*")
13
+ source += ".*";
14
+ else if (character === "?")
15
+ source += ".";
16
+ else
17
+ source += character.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
18
+ }
19
+ return new RegExp(`${source}$`).test(value);
20
+ }
21
+ function explicitMcpAccess(agent, action, resources) {
22
+ if (resources.length === 0)
23
+ return false;
24
+ return resources.every((resource) => {
25
+ const matching = agent.permissions.filter((rule) => wildcardMatch(rule.action, action) && wildcardMatch(rule.resource, resource));
26
+ const effect = matching.at(-1)?.effect;
27
+ return effect === "allow" || effect === "ask";
28
+ });
29
+ }
30
+ function matchingMcpServers(action, mcpServers) {
31
+ return mcpServers.filter((server) => action.startsWith(`${normalizeMcpName(server)}_`));
32
+ }
33
+ function buildAgentPermissions(agent, mcpServers) {
34
+ const result = [
35
+ ...agent.permissions,
36
+ { action: "skill", resource: "*", effect: "deny" },
37
+ ...agent.skills.map((skill) => ({ action: "skill", resource: skill, effect: "allow" }))
38
+ ];
39
+ for (const server of mcpServers) {
40
+ const prefix = `${normalizeMcpName(server)}_`;
41
+ result.push({
42
+ action: `${prefix}*`,
43
+ resource: "*",
44
+ effect: agent.mcp.includes(server) ? "allow" : "deny"
45
+ });
46
+ result.push(...agent.permissions.filter((rule) => rule.action.startsWith(prefix)));
47
+ }
48
+ return result;
49
+ }
50
+
6
51
  // src/config.ts
7
- import { existsSync, readdirSync, readFileSync, realpathSync } from "fs";
8
- import { homedir } from "os";
9
- import { dirname, isAbsolute, join, relative, resolve } from "path";
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";
10
54
  import { fileURLToPath } from "url";
11
55
 
12
56
  // node_modules/jsonc-parser/lib/esm/impl/scanner.js
@@ -5474,12 +5518,180 @@ function refine(fn, _params = {}) {
5474
5518
  function superRefine(fn, params) {
5475
5519
  return _superRefine(fn, params);
5476
5520
  }
5521
+ // src/config-root.ts
5522
+ import { homedir } from "os";
5523
+ import { isAbsolute, join, resolve } from "path";
5524
+ var GVOZD_CONFIG_ROOT_ENV = "GVOZD_OPENCODE_CONFIG_ROOT";
5525
+ function absolute(path, label) {
5526
+ if (!isAbsolute(path))
5527
+ throw new Error(`${label} must be an absolute path`);
5528
+ return resolve(path);
5529
+ }
5530
+ function resolveOpenCodeConfigRootContract(env = process.env, platform = process.platform, home = homedir(), explicitRoot) {
5531
+ if (explicitRoot)
5532
+ return { path: absolute(explicitRoot, "OpenCode config root"), source: "explicit" };
5533
+ const override = env[GVOZD_CONFIG_ROOT_ENV];
5534
+ if (override) {
5535
+ return { path: absolute(override, GVOZD_CONFIG_ROOT_ENV), source: GVOZD_CONFIG_ROOT_ENV };
5536
+ }
5537
+ if (env.XDG_CONFIG_HOME) {
5538
+ return { path: join(absolute(env.XDG_CONFIG_HOME, "XDG_CONFIG_HOME"), "opencode"), source: "XDG_CONFIG_HOME" };
5539
+ }
5540
+ if (platform === "win32" && env.APPDATA) {
5541
+ return { path: join(env.APPDATA, "opencode"), source: "APPDATA" };
5542
+ }
5543
+ return { path: join(home, ".config", "opencode"), source: "platform-default" };
5544
+ }
5545
+ function resolveOpenCodeConfigRoot(env = process.env, platform = process.platform, home = homedir(), explicitRoot) {
5546
+ return resolveOpenCodeConfigRootContract(env, platform, home, explicitRoot).path;
5547
+ }
5548
+
5549
+ // src/project-trust.ts
5550
+ import { createHash } from "crypto";
5551
+ import { existsSync, lstatSync, readdirSync, readFileSync, realpathSync } from "fs";
5552
+ import { dirname, isAbsolute as isAbsolute2, join as join2, relative, resolve as resolve2 } from "path";
5553
+ var PROJECT_TRUST_ENV = "GVOZD_TRUST_PROJECT_CONFIG";
5554
+ function findRoot(start) {
5555
+ let current = resolve2(start);
5556
+ while (true) {
5557
+ if (existsSync(join2(current, ".git")))
5558
+ return realpathSync(current);
5559
+ const parent = dirname(current);
5560
+ if (parent === current)
5561
+ return realpathSync(resolve2(start));
5562
+ current = parent;
5563
+ }
5564
+ }
5565
+ function within(base, target, label) {
5566
+ const child = relative(base, target);
5567
+ if (child === "" || !child.startsWith("..") && !isAbsolute2(child))
5568
+ return;
5569
+ throw new Error(`${label} must stay inside ${base}: ${target}`);
5570
+ }
5571
+ function rejectSymlinkComponents(base, target, label) {
5572
+ within(base, target, label);
5573
+ const segments = relative(base, target).split(/[\\/]/).filter(Boolean);
5574
+ let current = base;
5575
+ for (const segment of segments) {
5576
+ current = join2(current, segment);
5577
+ if (!existsSync(current))
5578
+ continue;
5579
+ if (lstatSync(current).isSymbolicLink())
5580
+ throw new Error(`${label} must not contain symlink components: ${current}`);
5581
+ }
5582
+ }
5583
+ function readJsonc(path) {
5584
+ const bytes = readFileSync(path);
5585
+ const errors = [];
5586
+ const value = parse2(bytes.toString("utf8"), errors, { allowTrailingComma: true, disallowComments: false });
5587
+ if (errors.length > 0 || !value || typeof value !== "object" || Array.isArray(value)) {
5588
+ const details = errors.map((error) => `${printParseErrorCode(error.error)} at offset ${error.offset}`).join(", ");
5589
+ throw new Error(`Invalid JSONC in ${path}${details ? `: ${details}` : ""}`);
5590
+ }
5591
+ return { value, bytes };
5592
+ }
5593
+ function promptFromPatch(patch) {
5594
+ if (!patch || typeof patch !== "object" || Array.isArray(patch))
5595
+ return;
5596
+ const prompt = patch.prompt;
5597
+ return typeof prompt === "string" && prompt.length > 0 ? prompt : undefined;
5598
+ }
5599
+ function collectProjectTrustInputs(projectDirectory) {
5600
+ const canonicalRoot = findRoot(projectDirectory);
5601
+ const layer = join2(canonicalRoot, "docs", ".gvozd");
5602
+ const rootPath = join2(layer, "config.jsonc");
5603
+ if (!existsSync(rootPath))
5604
+ return {
5605
+ canonicalRoot,
5606
+ inputs: [{ identity: "docs/.gvozd/config.jsonc:<missing>", bytes: Buffer.alloc(0) }]
5607
+ };
5608
+ rejectSymlinkComponents(canonicalRoot, rootPath, "Project config path");
5609
+ const rootStat = lstatSync(rootPath);
5610
+ if (!rootStat.isFile() || rootStat.isSymbolicLink())
5611
+ throw new Error(`Project config must be a regular non-symlink file: ${rootPath}`);
5612
+ const root = readJsonc(rootPath);
5613
+ const files = new Map([[relative(canonicalRoot, rootPath).replaceAll("\\", "/"), root.bytes]]);
5614
+ const prompts = [];
5615
+ const inlineAgents = root.value.agents;
5616
+ if (inlineAgents && typeof inlineAgents === "object" && !Array.isArray(inlineAgents)) {
5617
+ for (const patch of Object.values(inlineAgents)) {
5618
+ const prompt = promptFromPatch(patch);
5619
+ if (prompt)
5620
+ prompts.push({ source: rootPath, value: prompt });
5621
+ }
5622
+ }
5623
+ const configuredDirectory = root.value.agentsDirectory;
5624
+ if (configuredDirectory !== undefined && (typeof configuredDirectory !== "string" || configuredDirectory.length === 0)) {
5625
+ throw new Error(`Invalid agentsDirectory in ${rootPath}`);
5626
+ }
5627
+ const agentsDirectory = resolve2(layer, configuredDirectory ?? "agents");
5628
+ within(layer, agentsDirectory, "agentsDirectory");
5629
+ if (existsSync(agentsDirectory)) {
5630
+ rejectSymlinkComponents(canonicalRoot, agentsDirectory, "agentsDirectory");
5631
+ const stat = lstatSync(agentsDirectory);
5632
+ if (!stat.isDirectory() || stat.isSymbolicLink())
5633
+ throw new Error(`agentsDirectory must be a regular directory: ${agentsDirectory}`);
5634
+ within(realpathSync(layer), realpathSync(agentsDirectory), "agentsDirectory");
5635
+ for (const entry of readdirSync(agentsDirectory, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
5636
+ if (!entry.name.endsWith(".jsonc"))
5637
+ continue;
5638
+ const path = join2(agentsDirectory, entry.name);
5639
+ if (!entry.isFile())
5640
+ throw new Error(`Agent fragment must be a regular non-symlink file: ${path}`);
5641
+ rejectSymlinkComponents(canonicalRoot, path, "Agent fragment path");
5642
+ const stat = lstatSync(path);
5643
+ if (!stat.isFile() || stat.isSymbolicLink())
5644
+ throw new Error(`Agent fragment must be a regular non-symlink file: ${path}`);
5645
+ const fragment = readJsonc(path);
5646
+ files.set(relative(canonicalRoot, path).replaceAll("\\", "/"), fragment.bytes);
5647
+ const prompt = promptFromPatch(fragment.value);
5648
+ if (prompt)
5649
+ prompts.push({ source: path, value: prompt });
5650
+ }
5651
+ }
5652
+ for (const prompt of prompts) {
5653
+ const path = resolve2(dirname(prompt.source), prompt.value);
5654
+ within(layer, path, "Agent prompt");
5655
+ rejectSymlinkComponents(canonicalRoot, path, "Agent prompt");
5656
+ if (!existsSync(path))
5657
+ throw new Error(`Agent prompt is missing: ${path}`);
5658
+ const stat = lstatSync(path);
5659
+ if (!stat.isFile() || stat.isSymbolicLink())
5660
+ throw new Error(`Agent prompt must be a regular non-symlink file: ${path}`);
5661
+ const canonical = realpathSync(path);
5662
+ within(realpathSync(layer), canonical, "Agent prompt");
5663
+ files.set(relative(canonicalRoot, path).replaceAll("\\", "/"), readFileSync(path));
5664
+ }
5665
+ return {
5666
+ canonicalRoot,
5667
+ inputs: [...files].map(([identity, bytes]) => ({ identity, bytes })).sort((left, right) => left.identity.localeCompare(right.identity))
5668
+ };
5669
+ }
5670
+ function computeProjectTrustToken(projectDirectory) {
5671
+ const collected = collectProjectTrustInputs(projectDirectory);
5672
+ const hash = createHash("sha256");
5673
+ hash.update("agent-gvozd-project-trust-v1\x00");
5674
+ hash.update(collected.canonicalRoot);
5675
+ hash.update("\x00");
5676
+ for (const input of collected.inputs) {
5677
+ hash.update(String(Buffer.byteLength(input.identity)));
5678
+ hash.update(":");
5679
+ hash.update(input.identity);
5680
+ hash.update(":");
5681
+ hash.update(String(input.bytes.length));
5682
+ hash.update(":");
5683
+ hash.update(input.bytes);
5684
+ hash.update("\x00");
5685
+ }
5686
+ return `sha256:${hash.digest("hex")}`;
5687
+ }
5688
+
5477
5689
  // src/config.ts
5478
5690
  var permissionSchema = object({
5479
5691
  action: string2().min(1),
5480
5692
  resource: string2().min(1),
5481
5693
  effect: _enum(["allow", "ask", "deny"])
5482
- });
5694
+ }).strict();
5483
5695
  var modelRefSchema = string2().min(1).regex(/^[^/#\s]+\/[^#\s]+(?:#[^#\s]+)?$/, "Expected provider/model or provider/model#variant");
5484
5696
  var agentIdSchema = string2().regex(/^[A-Za-z0-9][A-Za-z0-9_-]*$/, "Expected a filesystem-safe agent ID");
5485
5697
  var fileLeaseRoleSchema = _enum(["coordinator", "writer", "readonly"]);
@@ -5493,12 +5705,13 @@ var agentPatchSchema = object({
5493
5705
  permissions: array(permissionSchema).optional(),
5494
5706
  fileLease: fileLeaseRoleSchema.optional(),
5495
5707
  disabled: boolean2().optional()
5496
- });
5708
+ }).strict();
5497
5709
  var rootPatchSchema = object({
5710
+ $schema: string2().min(1).optional(),
5498
5711
  defaultAgent: agentIdSchema.optional(),
5499
5712
  agentsDirectory: string2().min(1).optional(),
5500
5713
  agents: record(agentIdSchema, agentPatchSchema).optional()
5501
- });
5714
+ }).strict();
5502
5715
  var resolvedAgentSchema = agentPatchSchema.extend({
5503
5716
  description: string2().min(1),
5504
5717
  mode: _enum(["primary", "subagent", "all"]),
@@ -5510,9 +5723,18 @@ var resolvedAgentSchema = agentPatchSchema.extend({
5510
5723
  fileLease: fileLeaseRoleSchema,
5511
5724
  disabled: boolean2()
5512
5725
  });
5513
- function readJsonc(path) {
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) {
5514
5736
  const errors = [];
5515
- const value = parse2(readFileSync(path, "utf8"), errors, {
5737
+ const value = parse2(readFileSync2(path, "utf8"), errors, {
5516
5738
  allowTrailingComma: true,
5517
5739
  disallowComments: false
5518
5740
  });
@@ -5523,45 +5745,62 @@ function readJsonc(path) {
5523
5745
  return value;
5524
5746
  }
5525
5747
  function assertWithin(base, target, label) {
5526
- const child = relative(base, target);
5527
- if (child === "" || !child.startsWith("..") && !isAbsolute(child))
5748
+ const child = relative2(base, target);
5749
+ if (child === "" || !child.startsWith("..") && !isAbsolute3(child))
5528
5750
  return;
5529
5751
  throw new Error(`${label} must stay inside ${base}: ${target}`);
5530
5752
  }
5531
5753
  function resolvePrompt(patch, sourcePath, layerDirectory) {
5532
5754
  if (!patch.prompt)
5533
5755
  return patch;
5534
- const prompt = resolve(dirname(sourcePath), patch.prompt);
5756
+ const prompt = resolve3(dirname2(sourcePath), patch.prompt);
5535
5757
  assertWithin(layerDirectory, prompt, "Agent prompt");
5536
- if (!existsSync(prompt))
5758
+ if (!existsSync2(prompt))
5537
5759
  throw new Error(`Agent prompt is missing: ${prompt}`);
5538
- const canonical = realpathSync(prompt);
5539
- assertWithin(realpathSync(layerDirectory), canonical, "Agent prompt");
5540
- return { ...patch, prompt: canonical };
5541
- }
5542
- function loadLayer(directory, rootFileName, required) {
5543
- const rootPath = join(directory, rootFileName);
5544
- if (!existsSync(rootPath)) {
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)) {
5545
5771
  if (required)
5546
5772
  throw new Error(`Required config is missing: ${rootPath}`);
5547
5773
  return { agents: {}, sources: [] };
5548
5774
  }
5549
- const root = rootPatchSchema.parse(readJsonc(rootPath));
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
+ }
5550
5781
  const agents = {};
5551
5782
  for (const [id, patch] of Object.entries(root.agents ?? {})) {
5783
+ if (projectPolicy)
5784
+ assertTrustedProjectPatch(patch, rootPath, id, projectPolicy.trusted, projectPolicy.knownAgents);
5552
5785
  agents[id] = resolvePrompt(patch, rootPath, directory);
5553
5786
  }
5554
- const agentsDirectory = resolve(directory, root.agentsDirectory ?? "agents");
5787
+ const agentsDirectory = resolve3(directory, root.agentsDirectory ?? "agents");
5555
5788
  assertWithin(directory, agentsDirectory, "agentsDirectory");
5556
- if (existsSync(agentsDirectory)) {
5557
- assertWithin(realpathSync(directory), realpathSync(agentsDirectory), "agentsDirectory");
5558
- for (const entry of readdirSync(agentsDirectory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
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))) {
5559
5796
  if (!entry.isFile() || !entry.name.endsWith(".jsonc"))
5560
5797
  continue;
5561
5798
  const id = entry.name.slice(0, -".jsonc".length);
5562
5799
  agentIdSchema.parse(id);
5563
- const agentPath = join(agentsDirectory, entry.name);
5564
- const patch = agentPatchSchema.parse(readJsonc(agentPath));
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);
5565
5804
  agents[id] = mergeAgent(agents[id], resolvePrompt(patch, agentPath, directory));
5566
5805
  }
5567
5806
  }
@@ -5575,22 +5814,24 @@ function mergeAgent(base, override) {
5575
5814
  return { ...base ?? {}, ...override };
5576
5815
  }
5577
5816
  function resolveAgentConfig(patch) {
5578
- const parsed = agentPatchSchema.parse(patch);
5579
- return resolvedAgentSchema.parse({
5817
+ const parsed = agentPatchSchema.extend({ promptContent: string2().optional() }).parse(patch);
5818
+ const { promptContent, ...agentPatch } = parsed;
5819
+ const resolved = resolvedAgentSchema.parse({
5580
5820
  skills: [],
5581
5821
  mcp: [],
5582
5822
  permissions: [],
5583
5823
  fileLease: "readonly",
5584
5824
  disabled: false,
5585
- ...parsed
5825
+ ...agentPatch
5586
5826
  });
5827
+ return promptContent === undefined ? resolved : { ...resolved, promptContent };
5587
5828
  }
5588
5829
  function findPackageRoot() {
5589
- let current = dirname(fileURLToPath(import.meta.url));
5830
+ let current = dirname2(fileURLToPath(import.meta.url));
5590
5831
  while (true) {
5591
- if (existsSync(join(current, "defaults", "default.jsonc")))
5832
+ if (existsSync2(join3(current, "defaults", "default.jsonc")))
5592
5833
  return current;
5593
- const parent = dirname(current);
5834
+ const parent = dirname2(current);
5594
5835
  if (parent === current)
5595
5836
  break;
5596
5837
  current = parent;
@@ -5598,34 +5839,35 @@ function findPackageRoot() {
5598
5839
  throw new Error("Unable to locate agent-gvozd package defaults");
5599
5840
  }
5600
5841
  function findProjectRoot(start) {
5601
- let current = resolve(start);
5842
+ let current = resolve3(start);
5602
5843
  while (true) {
5603
- const git = join(current, ".git");
5604
- if (existsSync(git))
5844
+ const git = join3(current, ".git");
5845
+ if (existsSync2(git))
5605
5846
  return current;
5606
- const parent = dirname(current);
5847
+ const parent = dirname2(current);
5607
5848
  if (parent === current)
5608
- return resolve(start);
5849
+ return resolve3(start);
5609
5850
  current = parent;
5610
5851
  }
5611
5852
  }
5612
- function resolveOpenCodeConfigRoot(env = process.env, platform = process.platform, home = homedir()) {
5613
- if (env.XDG_CONFIG_HOME)
5614
- return join(env.XDG_CONFIG_HOME, "opencode");
5615
- if (platform === "win32" && env.APPDATA)
5616
- return join(env.APPDATA, "opencode");
5617
- return join(home, ".config", "opencode");
5618
- }
5619
5853
  function loadConfig(projectDirectory, options = {}) {
5620
5854
  const projectRoot = findProjectRoot(projectDirectory);
5621
5855
  const packageRoot = findPackageRoot();
5622
- const projectConfigDirectory = join(projectRoot, "docs", ".gvozd");
5623
- const globalConfigDirectory = join(options.configRoot ?? resolveOpenCodeConfigRoot(options.env, options.platform, options.home), "gvozd");
5624
- const layers = [
5625
- loadLayer(join(packageRoot, "defaults"), "default.jsonc", true),
5626
- loadLayer(globalConfigDirectory, "config.jsonc", false),
5627
- ...options.includeProject === false ? [] : [loadLayer(projectConfigDirectory, "config.jsonc", false)]
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)
5628
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] : []];
5629
5871
  let defaultAgent;
5630
5872
  const agents = {};
5631
5873
  for (const layer of layers) {
@@ -5638,7 +5880,12 @@ function loadConfig(projectDirectory, options = {}) {
5638
5880
  throw new Error("defaultAgent is not configured");
5639
5881
  const resolvedAgents = Object.fromEntries(Object.entries(agents).map(([id, patch]) => [
5640
5882
  id,
5641
- resolveAgentConfig(patch)
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
+ })()
5642
5889
  ]));
5643
5890
  const defaultConfig = resolvedAgents[defaultAgent];
5644
5891
  if (!defaultConfig || defaultConfig.disabled || defaultConfig.mode === "subagent") {
@@ -5657,8 +5904,8 @@ function loadConfig(projectDirectory, options = {}) {
5657
5904
 
5658
5905
  // src/file-leases.ts
5659
5906
  import { randomUUID } from "crypto";
5660
- import { existsSync as existsSync2, lstatSync, realpathSync as realpathSync2, statSync } from "fs";
5661
- import { basename, dirname as dirname2, isAbsolute as isAbsolute2, relative as relative2, resolve as resolve2, sep } from "path";
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";
5662
5909
 
5663
5910
  class LeaseError extends Error {
5664
5911
  code;
@@ -5670,6 +5917,18 @@ class LeaseError extends Error {
5670
5917
  }
5671
5918
  var DEFAULT_RESERVATION_TTL_MS = 5 * 60 * 1000;
5672
5919
  var DEFAULT_ACTIVE_TTL_MS = 30 * 60 * 1000;
5920
+ var GVOZD_CASE_INSENSITIVE_FILESYSTEM = "GVOZD_CASE_INSENSITIVE_FILESYSTEM";
5921
+ function resolveCaseInsensitiveFilesystem(env = process.env, platform = process.platform) {
5922
+ const override = env[GVOZD_CASE_INSENSITIVE_FILESYSTEM];
5923
+ if (override === "1")
5924
+ return true;
5925
+ if (override === "0")
5926
+ return false;
5927
+ if (override !== undefined) {
5928
+ throw new Error(`${GVOZD_CASE_INSENSITIVE_FILESYSTEM} must be exactly 1 or 0`);
5929
+ }
5930
+ return platform === "darwin" || platform === "win32";
5931
+ }
5673
5932
  function nonEmpty(value, label) {
5674
5933
  const normalized = value.trim();
5675
5934
  if (normalized === "")
@@ -5682,11 +5941,11 @@ function assertPositiveDuration(value, label) {
5682
5941
  return value;
5683
5942
  }
5684
5943
  function isWithin(root, target) {
5685
- const child = relative2(root, target);
5686
- return child !== "" && !child.startsWith(`..${sep}`) && child !== ".." && !isAbsolute2(child);
5944
+ const child = relative3(root, target);
5945
+ return child !== "" && !child.startsWith(`..${sep}`) && child !== ".." && !isAbsolute4(child);
5687
5946
  }
5688
5947
  function displayPath(root, target) {
5689
- return relative2(root, target).replaceAll("\\", "/");
5948
+ return relative3(root, target).replaceAll("\\", "/");
5690
5949
  }
5691
5950
 
5692
5951
  class FileLeaseManager {
@@ -5695,17 +5954,19 @@ class FileLeaseManager {
5695
5954
  activeTtlMs;
5696
5955
  now;
5697
5956
  createID;
5957
+ caseInsensitive;
5698
5958
  leases = new Map;
5699
5959
  fileOwners = new Map;
5700
5960
  sessionOwners = new Map;
5701
5961
  constructor(options) {
5702
- this.projectRoot = realpathSync2(options.projectRoot);
5962
+ this.projectRoot = realpathSync3(options.projectRoot);
5703
5963
  if (!statSync(this.projectRoot).isDirectory())
5704
5964
  throw new Error(`Project root is not a directory: ${this.projectRoot}`);
5705
5965
  this.reservationTtlMs = assertPositiveDuration(options.reservationTtlMs ?? DEFAULT_RESERVATION_TTL_MS, "reservationTtlMs");
5706
5966
  this.activeTtlMs = assertPositiveDuration(options.activeTtlMs ?? DEFAULT_ACTIVE_TTL_MS, "activeTtlMs");
5707
5967
  this.now = options.now ?? Date.now;
5708
5968
  this.createID = options.createID ?? randomUUID;
5969
+ this.caseInsensitive = options.caseInsensitive ?? resolveCaseInsensitiveFilesystem({}, options.platform ?? process.platform);
5709
5970
  }
5710
5971
  reserve(input) {
5711
5972
  this.sweep();
@@ -5724,14 +5985,14 @@ class FileLeaseManager {
5724
5985
  agent,
5725
5986
  label,
5726
5987
  state: "reserved",
5727
- files: new Set(files),
5988
+ files: new Map(files.map((file) => [file.key, file.canonicalPath])),
5728
5989
  createdAt: now,
5729
5990
  lastActivityAt: now,
5730
5991
  expiresAt: now + this.reservationTtlMs
5731
5992
  };
5732
5993
  this.leases.set(leaseId, lease);
5733
5994
  for (const file of files)
5734
- this.fileOwners.set(file, leaseId);
5995
+ this.fileOwners.set(file.key, leaseId);
5735
5996
  return this.toStatus(lease);
5736
5997
  }
5737
5998
  extend(input) {
@@ -5743,8 +6004,9 @@ class FileLeaseManager {
5743
6004
  const files = this.normalizeLeaseFiles(input.files);
5744
6005
  this.assertFilesAvailable(files, lease.leaseId);
5745
6006
  for (const file of files) {
5746
- lease.files.add(file);
5747
- this.fileOwners.set(file, lease.leaseId);
6007
+ if (!lease.files.has(file.key))
6008
+ lease.files.set(file.key, file.canonicalPath);
6009
+ this.fileOwners.set(file.key, lease.leaseId);
5748
6010
  }
5749
6011
  this.refresh(lease);
5750
6012
  return this.toStatus(lease);
@@ -5790,10 +6052,11 @@ class FileLeaseManager {
5790
6052
  if (!leaseId)
5791
6053
  throw new LeaseError("NO_ACTIVE_LEASE", `Session ${sessionID} has no active file lease`);
5792
6054
  const lease = this.requireLease(leaseId);
5793
- const files = resources.map((resource) => this.canonicalize(resource, true));
5794
- const outside = files.find((file) => !lease.files.has(file));
6055
+ const files = resources.map((resource) => this.toLeaseFile(this.canonicalize(resource, true)));
6056
+ const canonicalPaths = new Set(lease.files.values());
6057
+ const outside = files.find((file) => !canonicalPaths.has(file.canonicalPath));
5795
6058
  if (outside) {
5796
- throw new LeaseError("OUT_OF_SCOPE", `File ${displayPath(this.projectRoot, outside)} is outside lease ${lease.leaseId} (${lease.label}); ask Master to extend the scope`);
6059
+ throw new LeaseError("OUT_OF_SCOPE", `File ${displayPath(this.projectRoot, outside.canonicalPath)} is outside lease ${lease.leaseId} (${lease.label}); ask Master to extend the scope`);
5797
6060
  }
5798
6061
  this.refresh(lease);
5799
6062
  return this.toStatus(lease);
@@ -5872,18 +6135,18 @@ class FileLeaseManager {
5872
6135
  this.leases.delete(lease.leaseId);
5873
6136
  if (lease.sessionID)
5874
6137
  this.sessionOwners.delete(lease.sessionID);
5875
- for (const file of lease.files) {
5876
- if (this.fileOwners.get(file) === lease.leaseId)
5877
- this.fileOwners.delete(file);
6138
+ for (const key of lease.files.keys()) {
6139
+ if (this.fileOwners.get(key) === lease.leaseId)
6140
+ this.fileOwners.delete(key);
5878
6141
  }
5879
6142
  }
5880
6143
  assertFilesAvailable(files, currentLeaseId) {
5881
6144
  for (const file of files) {
5882
- const ownerId = this.fileOwners.get(file);
6145
+ const ownerId = this.fileOwners.get(file.key);
5883
6146
  if (!ownerId || ownerId === currentLeaseId)
5884
6147
  continue;
5885
6148
  const owner = this.leases.get(ownerId);
5886
- const relativeFile = displayPath(this.projectRoot, file);
6149
+ const relativeFile = displayPath(this.projectRoot, file.canonicalPath);
5887
6150
  if (!owner)
5888
6151
  throw new LeaseError("FILE_CONFLICT", `File ${relativeFile} is already reserved`);
5889
6152
  throw new LeaseError("FILE_CONFLICT", `File ${relativeFile} is ${owner.state} by ${owner.agent} for ${owner.label}; change the split or serialize the work`);
@@ -5892,13 +6155,27 @@ class FileLeaseManager {
5892
6155
  normalizeLeaseFiles(files) {
5893
6156
  if (files.length === 0)
5894
6157
  throw new LeaseError("INVALID_PATH", "A lease requires at least one exact file");
5895
- return [...new Set(files.map((file) => this.canonicalize(file, false)))].sort();
6158
+ const normalized = new Map;
6159
+ for (const file of files) {
6160
+ const canonical = this.toLeaseFile(this.canonicalize(file, false));
6161
+ const existing = normalized.get(canonical.key);
6162
+ if (!existing)
6163
+ normalized.set(canonical.key, canonical);
6164
+ else if (existing.canonicalPath !== canonical.canonicalPath) {
6165
+ throw new LeaseError("INVALID_PATH", `Lease request contains ambiguous file aliases ${displayPath(this.projectRoot, existing.canonicalPath)} and ${displayPath(this.projectRoot, canonical.canonicalPath)}`);
6166
+ }
6167
+ }
6168
+ return [...normalized.values()].sort((left, right) => left.canonicalPath.localeCompare(right.canonicalPath));
6169
+ }
6170
+ toLeaseFile(canonicalPath) {
6171
+ const key = this.caseInsensitive ? canonicalPath.toLowerCase().normalize("NFC") : canonicalPath;
6172
+ return { canonicalPath, key };
5896
6173
  }
5897
6174
  canonicalize(input, allowAbsolute) {
5898
6175
  const value = input.trim();
5899
6176
  if (value === "" || value.includes("\x00"))
5900
6177
  throw new LeaseError("INVALID_PATH", "File path must not be empty");
5901
- if (!allowAbsolute && isAbsolute2(value))
6178
+ if (!allowAbsolute && isAbsolute4(value))
5902
6179
  throw new LeaseError("INVALID_PATH", `Absolute lease path is not allowed: ${value}`);
5903
6180
  if (value.includes("*") || value.includes("?")) {
5904
6181
  throw new LeaseError("INVALID_PATH", `Lease paths must name exact files, not patterns: ${value}`);
@@ -5906,14 +6183,28 @@ class FileLeaseManager {
5906
6183
  if (value.endsWith("/") || value.endsWith("\\")) {
5907
6184
  throw new LeaseError("INVALID_PATH", `Lease paths must name files, not directories: ${value}`);
5908
6185
  }
5909
- const candidate = resolve2(this.projectRoot, value);
5910
- if ((!allowAbsolute || !isAbsolute2(value)) && !isWithin(this.projectRoot, candidate)) {
6186
+ const candidate = resolve4(this.projectRoot, value);
6187
+ if ((!allowAbsolute || !isAbsolute4(value)) && !isWithin(this.projectRoot, candidate)) {
5911
6188
  throw new LeaseError("INVALID_PATH", `File must stay inside the project root: ${value}`);
5912
6189
  }
6190
+ const code = allowAbsolute ? "OUT_OF_SCOPE" : "INVALID_PATH";
6191
+ if (existsSync3(candidate)) {
6192
+ try {
6193
+ const target = lstatSync3(candidate);
6194
+ if (!target.isSymbolicLink() && !target.isFile()) {
6195
+ throw new LeaseError(code, `Lease targets must be regular files: ${value}`);
6196
+ }
6197
+ } catch (error) {
6198
+ if (error instanceof LeaseError)
6199
+ throw error;
6200
+ if (error.code !== "ENOENT")
6201
+ throw error;
6202
+ }
6203
+ }
5913
6204
  let ancestor = candidate;
5914
6205
  const suffix = [];
5915
- while (!existsSync2(ancestor)) {
5916
- const parent = dirname2(ancestor);
6206
+ while (!existsSync3(ancestor)) {
6207
+ const parent = dirname3(ancestor);
5917
6208
  if (parent === ancestor)
5918
6209
  throw new LeaseError("INVALID_PATH", `Cannot resolve file path: ${value}`);
5919
6210
  suffix.unshift(basename(ancestor));
@@ -5921,17 +6212,21 @@ class FileLeaseManager {
5921
6212
  }
5922
6213
  let canonicalAncestor;
5923
6214
  try {
5924
- canonicalAncestor = realpathSync2(ancestor);
6215
+ canonicalAncestor = realpathSync3(ancestor);
5925
6216
  } catch {
5926
6217
  throw new LeaseError("INVALID_PATH", `Cannot canonicalize file path: ${value}`);
5927
6218
  }
5928
- const canonical = resolve2(canonicalAncestor, ...suffix);
6219
+ const canonical = resolve4(canonicalAncestor, ...suffix);
5929
6220
  if (!isWithin(this.projectRoot, canonical)) {
5930
6221
  throw new LeaseError("INVALID_PATH", `File resolves outside the project root: ${value}`);
5931
6222
  }
5932
6223
  try {
5933
- if (lstatSync(canonical).isDirectory()) {
5934
- throw new LeaseError("INVALID_PATH", `Lease paths must name files, not directories: ${value}`);
6224
+ const target = lstatSync3(canonical);
6225
+ if (!target.isFile()) {
6226
+ throw new LeaseError(code, `Lease targets must be regular files: ${value}`);
6227
+ }
6228
+ if (target.nlink > 1) {
6229
+ throw new LeaseError(code, `Hard-linked files cannot be used as lease targets: ${value}`);
5935
6230
  }
5936
6231
  } catch (error) {
5937
6232
  if (error instanceof LeaseError)
@@ -5949,7 +6244,7 @@ class FileLeaseManager {
5949
6244
  agent: lease.agent,
5950
6245
  label: lease.label,
5951
6246
  state: lease.state,
5952
- files: [...lease.files].map((file) => displayPath(this.projectRoot, file)).sort(),
6247
+ files: [...lease.files.values()].map((file) => displayPath(this.projectRoot, file)).sort(),
5953
6248
  createdAt: lease.createdAt,
5954
6249
  ...lease.claimedAt === undefined ? {} : { claimedAt: lease.claimedAt },
5955
6250
  lastActivityAt: lease.lastActivityAt,
@@ -6085,8 +6380,9 @@ function requireRole(config, agentID, allowed) {
6085
6380
  }
6086
6381
  return role;
6087
6382
  }
6088
- async function installFileLeaseRuntime(ctx, config) {
6089
- const manager = new FileLeaseManager({ projectRoot: config.projectRoot });
6383
+ async function installFileLeaseRuntime(ctx, config, options = {}) {
6384
+ const caseInsensitive = options.caseInsensitive ?? resolveCaseInsensitiveFilesystem(options.env ?? process.env, options.platform ?? process.platform);
6385
+ const manager = new FileLeaseManager({ projectRoot: config.projectRoot, caseInsensitive });
6090
6386
  const toolTransform = await ctx.tool.transform((tools) => {
6091
6387
  tools.namespace({
6092
6388
  name: "gvozd",
@@ -6176,43 +6472,103 @@ async function installFileLeaseRuntime(ctx, config) {
6176
6472
  };
6177
6473
  }
6178
6474
 
6179
- // src/index.ts
6180
- function normalizeMcpName(name) {
6181
- return name.replaceAll(/[^A-Za-z0-9_-]/g, "_");
6182
- }
6183
- function wildcardMatch(pattern, value) {
6184
- let source = "^";
6185
- for (const character of pattern) {
6186
- if (character === "*")
6187
- source += ".*";
6188
- else if (character === "?")
6189
- source += ".";
6190
- else
6191
- source += character.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
6475
+ // src/runtime-events.ts
6476
+ function redactDiagnostic(error) {
6477
+ const message = error instanceof Error ? error.message : String(error);
6478
+ return message.replace(/-----BEGIN [^-\r\n]*PRIVATE KEY-----[\s\S]*?-----END [^-\r\n]*PRIVATE KEY-----/gi, "[redacted private key]").replace(/\b([a-z][a-z0-9+.-]*:\/\/)[^\s/@]+:[^\s/@]*@/gi, "$1[redacted]@").replace(/([?&](?:access_?token|auth(?:orization)?|api_?key|cookie|credential|password|secret|token)=)[^&#\s]*/gi, "$1[redacted]").replace(/\bBearer\s+[^\s,;]+/gi, "Bearer [redacted]").replace(/\b([A-Za-z0-9_]*(?:token|password|authorization|api_?key|secret|credential|cookie)[A-Za-z0-9_]*)\s*[:=]\s*["']?[^\s,;}"']+/gi, "$1=[redacted]").replace(/\s+/g, " ").slice(0, 300);
6479
+ }
6480
+ async function disposeResources(resources, diagnostic) {
6481
+ const failures = [];
6482
+ for (const resource of [...resources].reverse()) {
6483
+ try {
6484
+ await resource.dispose();
6485
+ } catch (error) {
6486
+ failures.push(error);
6487
+ diagnostic?.(`agent-gvozd cleanup failed: ${redactDiagnostic(error)}`);
6488
+ }
6192
6489
  }
6193
- return new RegExp(`${source}$`).test(value);
6490
+ if (failures.length > 0)
6491
+ throw new AggregateError(failures, "agent-gvozd cleanup failed");
6194
6492
  }
6195
- function explicitMcpAccess(agent, action, resources) {
6196
- if (resources.length === 0)
6197
- return false;
6198
- return resources.every((resource) => {
6199
- const matching = agent.permissions.filter((rule) => wildcardMatch(rule.action, action) && wildcardMatch(rule.resource, resource));
6200
- const effect = matching.at(-1)?.effect;
6201
- return effect === "allow" || effect === "ask";
6493
+ var DEFAULT_RETRY_BASE_DELAY_MS = 25;
6494
+ var DEFAULT_RETRY_MAX_DELAY_MS = 30000;
6495
+ function defaultDelay(milliseconds, signal) {
6496
+ if (signal.aborted)
6497
+ return Promise.resolve();
6498
+ return new Promise((resolve) => {
6499
+ const timeout = setTimeout(finish, milliseconds);
6500
+ function finish() {
6501
+ clearTimeout(timeout);
6502
+ signal.removeEventListener("abort", finish);
6503
+ resolve();
6504
+ }
6505
+ signal.addEventListener("abort", finish, { once: true });
6202
6506
  });
6203
6507
  }
6204
- function mcpPermissions(agent, mcpServers) {
6205
- return mcpServers.flatMap((server) => {
6206
- const prefix = `${normalizeMcpName(server)}_`;
6207
- const baseline = {
6208
- action: `${prefix}*`,
6209
- resource: "*",
6210
- effect: agent.mcp.includes(server) ? "allow" : "deny"
6211
- };
6212
- const explicit = agent.permissions.filter((rule) => rule.action.startsWith(prefix));
6213
- return [baseline, ...explicit];
6508
+ function retryDelay(attempt, baseDelayMs, maxDelayMs, random) {
6509
+ const capped = Math.min(baseDelayMs * 2 ** attempt, maxDelayMs);
6510
+ const boundedRandom = Math.max(0, Math.min(1, random()));
6511
+ return Math.round(capped * (0.5 + boundedRandom * 0.5));
6512
+ }
6513
+ async function waitForRetry(delay, milliseconds, signal) {
6514
+ if (signal.aborted)
6515
+ return false;
6516
+ let onAbort;
6517
+ const aborted = new Promise((resolve) => {
6518
+ onAbort = () => resolve();
6519
+ signal.addEventListener("abort", onAbort, { once: true });
6520
+ if (signal.aborted)
6521
+ onAbort();
6214
6522
  });
6523
+ try {
6524
+ await Promise.race([delay(milliseconds, signal), aborted]);
6525
+ } finally {
6526
+ signal.removeEventListener("abort", onAbort);
6527
+ }
6528
+ return !signal.aborted;
6529
+ }
6530
+ function startRuntimeEventLoop(input) {
6531
+ const controller = new AbortController;
6532
+ const delay = input.delay ?? defaultDelay;
6533
+ const random = input.random ?? Math.random;
6534
+ const baseDelayMs = Math.max(1, input.retryBaseDelayMs ?? DEFAULT_RETRY_BASE_DELAY_MS);
6535
+ const maxDelayMs = Math.max(baseDelayMs, input.retryMaxDelayMs ?? DEFAULT_RETRY_MAX_DELAY_MS);
6536
+ const done = (async () => {
6537
+ let retryAttempt = 0;
6538
+ while (!controller.signal.aborted) {
6539
+ try {
6540
+ for await (const event of input.subscribe(controller.signal)) {
6541
+ retryAttempt = 0;
6542
+ try {
6543
+ await input.handle(event);
6544
+ } catch (error) {
6545
+ input.diagnostic(`agent-gvozd runtime refresh failed: ${redactDiagnostic(error)}`);
6546
+ }
6547
+ }
6548
+ if (controller.signal.aborted)
6549
+ return;
6550
+ input.diagnostic("agent-gvozd event subscription ended unexpectedly");
6551
+ } catch (error) {
6552
+ if (controller.signal.aborted)
6553
+ return;
6554
+ input.diagnostic(`agent-gvozd event subscription failed: ${redactDiagnostic(error)}`);
6555
+ }
6556
+ const milliseconds = retryDelay(retryAttempt, baseDelayMs, maxDelayMs, random);
6557
+ retryAttempt += 1;
6558
+ if (!await waitForRetry(delay, milliseconds, controller.signal))
6559
+ return;
6560
+ }
6561
+ })();
6562
+ return {
6563
+ done,
6564
+ async dispose() {
6565
+ controller.abort();
6566
+ await done;
6567
+ }
6568
+ };
6215
6569
  }
6570
+
6571
+ // src/index.ts
6216
6572
  function selectModel(models, available) {
6217
6573
  const configured = models.map((model) => Model.Ref.parse(model));
6218
6574
  const selected = configured.find((candidate) => {
@@ -6232,11 +6588,14 @@ function applyAgentConfiguration(agents, config, models, mcpServers) {
6232
6588
  if (!agents.get(id))
6233
6589
  continue;
6234
6590
  agents.update(id, (agent) => {
6591
+ if (configured.promptContent === undefined) {
6592
+ throw new Error(`Agent ${id} is missing its immutable prompt snapshot (${configured.prompt})`);
6593
+ }
6235
6594
  agent.description = configured.description;
6236
6595
  agent.mode = configured.mode;
6237
- agent.system = readFileSync2(configured.prompt, "utf8").trim();
6596
+ agent.system = configured.promptContent.trim();
6238
6597
  agent.model = selectModel(configured.models, models);
6239
- agent.permissions.push(...mcpPermissions(configured, mcpServers));
6598
+ agent.permissions = buildAgentPermissions(configured, mcpServers);
6240
6599
  });
6241
6600
  }
6242
6601
  if (agents.get(config.defaultAgent))
@@ -6245,74 +6604,82 @@ function applyAgentConfiguration(agents, config, models, mcpServers) {
6245
6604
  var src_default = Plugin.define({
6246
6605
  id: "agent-gvozd",
6247
6606
  async setup(ctx) {
6248
- const config = loadConfig(ctx.location.project.directory);
6607
+ const caseInsensitive = resolveCaseInsensitiveFilesystem(process.env);
6608
+ const config = loadConfig(ctx.location.project.directory, { env: process.env });
6249
6609
  const mcp = await ctx.mcp.list();
6250
6610
  let mcpServers = mcp.data.map((server) => server.name);
6251
6611
  let models = await ctx.catalog.model.list();
6252
- const fileLeases = await installFileLeaseRuntime(ctx, config);
6253
- const agentTransform = await ctx.agent.transform((agents) => {
6254
- applyAgentConfiguration(agents, config, models.data, mcpServers);
6255
- });
6256
- const permissionHook = await ctx.permission.hook("evaluate", async (event) => {
6257
- if (fileLeases.enforcePermission(event))
6258
- return;
6259
- if (!event.agent)
6260
- return;
6261
- const configured = config.agents[event.agent];
6262
- if (!configured || configured.disabled)
6263
- return;
6264
- if (event.action === "skill") {
6265
- const allowed = new Set(configured.skills);
6266
- if (event.resources.some((resource) => !allowed.has(resource))) {
6267
- event.effect = "deny";
6268
- event.message = `Agent ${event.agent} cannot use this skill`;
6612
+ const diagnostic = (message) => console.error(message);
6613
+ const resources = [];
6614
+ try {
6615
+ const fileLeases = await installFileLeaseRuntime(ctx, config, { caseInsensitive });
6616
+ resources.push(fileLeases);
6617
+ const agentTransform = await ctx.agent.transform((agents) => {
6618
+ applyAgentConfiguration(agents, config, models.data, mcpServers);
6619
+ });
6620
+ resources.push(agentTransform);
6621
+ const permissionHook = await ctx.permission.hook("evaluate", async (event) => {
6622
+ if (fileLeases.enforcePermission(event))
6623
+ return;
6624
+ if (!event.agent)
6625
+ return;
6626
+ const configured = config.agents[event.agent];
6627
+ if (!configured || configured.disabled)
6628
+ return;
6629
+ if (event.action === "skill") {
6630
+ const allowed = new Set(configured.skills);
6631
+ if (event.resources.some((resource) => !allowed.has(resource))) {
6632
+ event.effect = "deny";
6633
+ event.message = `Agent ${event.agent} cannot use this skill`;
6634
+ }
6635
+ return;
6269
6636
  }
6270
- return;
6271
- }
6272
- let matchingServers = mcpServers.filter((server) => event.action.startsWith(`${normalizeMcpName(server)}_`));
6273
- if (matchingServers.length === 0 && event.action.includes("_")) {
6274
- const mcp = await ctx.mcp.list();
6275
- mcpServers = mcp.data.map((server) => server.name);
6276
- matchingServers = mcpServers.filter((server) => event.action.startsWith(`${normalizeMcpName(server)}_`));
6277
- }
6278
- if (matchingServers.length === 0)
6279
- return;
6280
- if (matchingServers.length > 1) {
6281
- event.effect = "deny";
6282
- event.message = `MCP action has an ambiguous server prefix: ${matchingServers.join(", ")}`;
6283
- return;
6284
- }
6285
- if (!matchingServers.some((server) => configured.mcp.includes(server)) && !explicitMcpAccess(configured, event.action, event.resources)) {
6286
- event.effect = "deny";
6287
- event.message = `Agent ${event.agent} cannot use this MCP server`;
6288
- }
6289
- });
6290
- const events = new AbortController;
6291
- const refreshMcp = (async () => {
6292
- for await (const event of ctx.event.subscribe({ signal: events.signal })) {
6293
- fileLeases.handleEvent(event);
6294
- if (event.type === "mcp.status.changed") {
6637
+ let matchingServers = matchingMcpServers(event.action, mcpServers);
6638
+ if (matchingServers.length === 0 && event.action.includes("_")) {
6295
6639
  const mcp = await ctx.mcp.list();
6296
- const next = mcp.data.map((server) => server.name);
6297
- if (next.length === mcpServers.length && next.every((server, index) => server === mcpServers[index]))
6298
- continue;
6299
- mcpServers = next;
6300
- await ctx.agent.reload();
6640
+ mcpServers = mcp.data.map((server) => server.name);
6641
+ matchingServers = matchingMcpServers(event.action, mcpServers);
6301
6642
  }
6302
- if (event.type === "catalog.updated") {
6303
- models = await ctx.catalog.model.list();
6304
- await ctx.agent.reload();
6643
+ if (matchingServers.length === 0)
6644
+ return;
6645
+ if (matchingServers.length > 1) {
6646
+ event.effect = "deny";
6647
+ event.message = `MCP action has an ambiguous server prefix: ${matchingServers.join(", ")}`;
6648
+ return;
6305
6649
  }
6306
- }
6307
- })();
6308
- refreshMcp.catch(() => {});
6309
- return async () => {
6310
- events.abort();
6311
- await refreshMcp.catch(() => {});
6312
- await fileLeases.dispose();
6313
- await permissionHook.dispose();
6314
- await agentTransform.dispose();
6315
- };
6650
+ if (!matchingServers.some((server) => configured.mcp.includes(server)) && !explicitMcpAccess(configured, event.action, event.resources)) {
6651
+ event.effect = "deny";
6652
+ event.message = `Agent ${event.agent} cannot use this MCP server`;
6653
+ }
6654
+ });
6655
+ resources.push(permissionHook);
6656
+ const eventLoop = startRuntimeEventLoop({
6657
+ subscribe: (signal) => ctx.event.subscribe({ signal }),
6658
+ diagnostic,
6659
+ async handle(event) {
6660
+ fileLeases.handleEvent(event);
6661
+ if (event.type === "mcp.status.changed") {
6662
+ const mcp = await ctx.mcp.list();
6663
+ const next = mcp.data.map((server) => server.name);
6664
+ if (next.length === mcpServers.length && next.every((server, index) => server === mcpServers[index]))
6665
+ return;
6666
+ mcpServers = next;
6667
+ await ctx.agent.reload();
6668
+ }
6669
+ if (event.type === "catalog.updated") {
6670
+ models = await ctx.catalog.model.list();
6671
+ await ctx.agent.reload();
6672
+ }
6673
+ }
6674
+ });
6675
+ resources.push(eventLoop);
6676
+ return async () => disposeResources(resources, diagnostic);
6677
+ } catch (error) {
6678
+ try {
6679
+ await disposeResources(resources, diagnostic);
6680
+ } catch {}
6681
+ throw error;
6682
+ }
6316
6683
  }
6317
6684
  });
6318
6685
  export {