@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/README.md +172 -11
- package/defaults/agents/back-deep.jsonc +3 -1
- package/defaults/agents/back-fast.jsonc +2 -1
- package/defaults/agents/devops.jsonc +2 -0
- package/defaults/agents/front-deep.jsonc +3 -1
- package/defaults/agents/front-fast.jsonc +2 -1
- package/defaults/agents/master.jsonc +2 -1
- package/defaults/agents/planner.jsonc +2 -1
- package/defaults/agents/review-deep.jsonc +2 -1
- package/defaults/agents/review-fast.jsonc +2 -1
- package/defaults/default.jsonc +2 -1
- package/defaults/prompts/back-deep.md +1 -1
- package/defaults/prompts/devops.md +1 -1
- package/defaults/prompts/explorer.md +1 -1
- package/defaults/prompts/front-deep.md +1 -1
- package/defaults/prompts/master.md +2 -2
- package/dist/cli.js +991 -255
- package/dist/index.js +544 -177
- package/package.json +5 -2
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 {
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
5527
|
-
if (child === "" || !child.startsWith("..") && !
|
|
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 =
|
|
5756
|
+
const prompt = resolve3(dirname2(sourcePath), patch.prompt);
|
|
5535
5757
|
assertWithin(layerDirectory, prompt, "Agent prompt");
|
|
5536
|
-
if (!
|
|
5758
|
+
if (!existsSync2(prompt))
|
|
5537
5759
|
throw new Error(`Agent prompt is missing: ${prompt}`);
|
|
5538
|
-
const
|
|
5539
|
-
|
|
5540
|
-
|
|
5541
|
-
|
|
5542
|
-
|
|
5543
|
-
const
|
|
5544
|
-
|
|
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(
|
|
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 =
|
|
5787
|
+
const agentsDirectory = resolve3(directory, root.agentsDirectory ?? "agents");
|
|
5555
5788
|
assertWithin(directory, agentsDirectory, "agentsDirectory");
|
|
5556
|
-
if (
|
|
5557
|
-
|
|
5558
|
-
|
|
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 =
|
|
5564
|
-
const patch = agentPatchSchema.parse(
|
|
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
|
-
|
|
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
|
-
...
|
|
5825
|
+
...agentPatch
|
|
5586
5826
|
});
|
|
5827
|
+
return promptContent === undefined ? resolved : { ...resolved, promptContent };
|
|
5587
5828
|
}
|
|
5588
5829
|
function findPackageRoot() {
|
|
5589
|
-
let current =
|
|
5830
|
+
let current = dirname2(fileURLToPath(import.meta.url));
|
|
5590
5831
|
while (true) {
|
|
5591
|
-
if (
|
|
5832
|
+
if (existsSync2(join3(current, "defaults", "default.jsonc")))
|
|
5592
5833
|
return current;
|
|
5593
|
-
const parent =
|
|
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 =
|
|
5842
|
+
let current = resolve3(start);
|
|
5602
5843
|
while (true) {
|
|
5603
|
-
const git =
|
|
5604
|
-
if (
|
|
5844
|
+
const git = join3(current, ".git");
|
|
5845
|
+
if (existsSync2(git))
|
|
5605
5846
|
return current;
|
|
5606
|
-
const parent =
|
|
5847
|
+
const parent = dirname2(current);
|
|
5607
5848
|
if (parent === current)
|
|
5608
|
-
return
|
|
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 =
|
|
5623
|
-
const globalConfigDirectory =
|
|
5624
|
-
const
|
|
5625
|
-
loadLayer(
|
|
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
|
-
|
|
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
|
|
5661
|
-
import { basename, dirname as
|
|
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 =
|
|
5686
|
-
return child !== "" && !child.startsWith(`..${sep}`) && 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
|
|
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 =
|
|
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
|
|
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.
|
|
5747
|
-
|
|
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
|
|
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
|
|
5876
|
-
if (this.fileOwners.get(
|
|
5877
|
-
this.fileOwners.delete(
|
|
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
|
-
|
|
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 &&
|
|
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 =
|
|
5910
|
-
if ((!allowAbsolute || !
|
|
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 (!
|
|
5916
|
-
const parent =
|
|
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 =
|
|
6215
|
+
canonicalAncestor = realpathSync3(ancestor);
|
|
5925
6216
|
} catch {
|
|
5926
6217
|
throw new LeaseError("INVALID_PATH", `Cannot canonicalize file path: ${value}`);
|
|
5927
6218
|
}
|
|
5928
|
-
const canonical =
|
|
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
|
-
|
|
5934
|
-
|
|
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
|
|
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/
|
|
6180
|
-
function
|
|
6181
|
-
|
|
6182
|
-
}
|
|
6183
|
-
|
|
6184
|
-
|
|
6185
|
-
|
|
6186
|
-
|
|
6187
|
-
|
|
6188
|
-
|
|
6189
|
-
|
|
6190
|
-
|
|
6191
|
-
|
|
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
|
-
|
|
6490
|
+
if (failures.length > 0)
|
|
6491
|
+
throw new AggregateError(failures, "agent-gvozd cleanup failed");
|
|
6194
6492
|
}
|
|
6195
|
-
|
|
6196
|
-
|
|
6197
|
-
|
|
6198
|
-
|
|
6199
|
-
|
|
6200
|
-
|
|
6201
|
-
|
|
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
|
|
6205
|
-
|
|
6206
|
-
|
|
6207
|
-
|
|
6208
|
-
|
|
6209
|
-
|
|
6210
|
-
|
|
6211
|
-
|
|
6212
|
-
|
|
6213
|
-
|
|
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 =
|
|
6596
|
+
agent.system = configured.promptContent.trim();
|
|
6238
6597
|
agent.model = selectModel(configured.models, models);
|
|
6239
|
-
agent.permissions
|
|
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
|
|
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
|
|
6253
|
-
const
|
|
6254
|
-
|
|
6255
|
-
|
|
6256
|
-
|
|
6257
|
-
|
|
6258
|
-
|
|
6259
|
-
|
|
6260
|
-
|
|
6261
|
-
const
|
|
6262
|
-
|
|
6263
|
-
|
|
6264
|
-
|
|
6265
|
-
|
|
6266
|
-
|
|
6267
|
-
|
|
6268
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6297
|
-
|
|
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 (
|
|
6303
|
-
|
|
6304
|
-
|
|
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
|
-
|
|
6309
|
-
|
|
6310
|
-
|
|
6311
|
-
|
|
6312
|
-
|
|
6313
|
-
|
|
6314
|
-
|
|
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 {
|