@nail00749/agent-gvozd 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +918 -27
- package/defaults/agents/front-deep.jsonc +3 -1
- package/defaults/agents/front-fast.jsonc +2 -1
- package/defaults/agents/git.jsonc +1035 -36
- 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/agents/verifier.jsonc +873 -20
- 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/git.md +3 -1
- package/defaults/prompts/master.md +11 -2
- package/defaults/prompts/verifier.md +3 -1
- package/defaults/schema.json +11 -1
- package/dist/cli.js +1453 -258
- package/dist/index.js +758 -307
- package/package.json +5 -2
package/dist/index.js
CHANGED
|
@@ -1,12 +1,128 @@
|
|
|
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/tool-permissions.ts
|
|
6
|
+
function family(command, ...variants) {
|
|
7
|
+
return [command, ...variants].map((entry) => ({
|
|
8
|
+
exact: entry,
|
|
9
|
+
wildcard: `${entry} *`
|
|
10
|
+
}));
|
|
11
|
+
}
|
|
12
|
+
var INSPECTION_COMMANDS = [
|
|
13
|
+
...family("pwd", "true", "test"),
|
|
14
|
+
...family("cat", "head", "tail", "wc", "sort", "uniq"),
|
|
15
|
+
...family("grep", "rg", "find", "diff", "cmp"),
|
|
16
|
+
...family("ls", "du", "df", "stat", "file", "realpath", "basename", "dirname"),
|
|
17
|
+
...family("shasum", "sha256sum", "md5sum"),
|
|
18
|
+
...family("uname", "whoami", "hostname", "date", "printenv"),
|
|
19
|
+
...family("which", "command -v"),
|
|
20
|
+
...family("mktemp"),
|
|
21
|
+
...family("tr", "cut", "paste", "column"),
|
|
22
|
+
...family("node --version", "python3 --version", "python --version", "deno --version")
|
|
23
|
+
];
|
|
24
|
+
var TOOLCHAIN_COMMANDS = [
|
|
25
|
+
...family("bun test", "bun run test", "bun --version"),
|
|
26
|
+
...family("bun run typecheck", "bun run lint", "bun run build", "bun run check"),
|
|
27
|
+
...family("tsc --noEmit", "npx tsc --noEmit"),
|
|
28
|
+
...family("eslint", "biome check", "prettier --check"),
|
|
29
|
+
...family("npm test", "npm run test", "npm run typecheck", "npm run lint", "npm run build"),
|
|
30
|
+
...family("pnpm test", "pnpm run test", "pnpm run build"),
|
|
31
|
+
...family("yarn test", "yarn build"),
|
|
32
|
+
...family("vitest run", "jest", "playwright test"),
|
|
33
|
+
...family("cargo check", "cargo test", "cargo build", "cargo clippy", "cargo fmt --check", "cargo --version"),
|
|
34
|
+
...family("go build ./...", "go test ./...", "go vet ./...", "go version"),
|
|
35
|
+
...family("pytest", "python3 -m pytest", "python -m pytest"),
|
|
36
|
+
...family("ruff check", "mypy", "pyright"),
|
|
37
|
+
...family("mvn test", "mvn verify", "gradle test", "gradle check", "./gradlew test", "./gradlew check"),
|
|
38
|
+
...family("make test", "make check", "make build", "make --version"),
|
|
39
|
+
...family("just --list")
|
|
40
|
+
];
|
|
41
|
+
var GIT_READONLY_COMMANDS = [
|
|
42
|
+
...family("git status", "git status --short", "git status --short --branch", "git status --porcelain", "git status --porcelain=v1 --branch"),
|
|
43
|
+
...family("git diff", "git diff --stat", "git diff --cached", "git diff --check"),
|
|
44
|
+
...family("git log", "git show", "git reflog"),
|
|
45
|
+
...family("git rev-parse", "git rev-list", "git show-ref", "git cat-file", "git symbolic-ref"),
|
|
46
|
+
...family("git ls-files", "git ls-remote", "git grep"),
|
|
47
|
+
...family("git branch", "git remote", "git stash list", "git tag", "git describe"),
|
|
48
|
+
...family("git config --get", "git config --get-regexp"),
|
|
49
|
+
...family("git -C")
|
|
50
|
+
];
|
|
51
|
+
var GIT_MUTATING_COMMANDS = [
|
|
52
|
+
...family("git add", "git rm --cached"),
|
|
53
|
+
...family("git commit", "git merge --ff-only", "git merge --no-ff"),
|
|
54
|
+
...family("git push", "git fetch", "git pull --ff-only"),
|
|
55
|
+
...family("git tag -a", "git tag -v", "git tag --list"),
|
|
56
|
+
...family("git stash", "git cherry-pick", "git revert"),
|
|
57
|
+
...family("git switch", "git checkout -b", "git worktree list", "git worktree add")
|
|
58
|
+
];
|
|
59
|
+
var GIT_ENV_PREFIXES = ["GIT_OPTIONAL_LOCKS=0"];
|
|
60
|
+
function withEnvPrefixes(rule, prefixes = GIT_ENV_PREFIXES) {
|
|
61
|
+
return prefixes.map((prefix) => ({
|
|
62
|
+
action: rule.action,
|
|
63
|
+
resource: `${prefix} ${rule.resource}`,
|
|
64
|
+
effect: rule.effect
|
|
65
|
+
}));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// src/agent-permissions.ts
|
|
69
|
+
function normalizeMcpName(name) {
|
|
70
|
+
return name.replaceAll(/[^A-Za-z0-9_-]/g, "_");
|
|
71
|
+
}
|
|
72
|
+
function wildcardMatch(pattern, value) {
|
|
73
|
+
let source = "^";
|
|
74
|
+
for (const character of pattern) {
|
|
75
|
+
if (character === "*")
|
|
76
|
+
source += ".*";
|
|
77
|
+
else if (character === "?")
|
|
78
|
+
source += ".";
|
|
79
|
+
else
|
|
80
|
+
source += character.replace(/[\\^$.*+?()[\]{}|]/g, "\\$&");
|
|
81
|
+
}
|
|
82
|
+
return new RegExp(`${source}$`).test(value);
|
|
83
|
+
}
|
|
84
|
+
function explicitMcpAccess(agent, action, resources) {
|
|
85
|
+
if (resources.length === 0)
|
|
86
|
+
return false;
|
|
87
|
+
return resources.every((resource) => {
|
|
88
|
+
const matching = agent.permissions.filter((rule) => wildcardMatch(rule.action, action) && wildcardMatch(rule.resource, resource));
|
|
89
|
+
const effect = matching.at(-1)?.effect;
|
|
90
|
+
return effect === "allow" || effect === "ask";
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
function matchingMcpServers(action, mcpServers) {
|
|
94
|
+
return mcpServers.filter((server) => action.startsWith(`${normalizeMcpName(server)}_`));
|
|
95
|
+
}
|
|
96
|
+
function buildAgentPermissions(agent, mcpServers) {
|
|
97
|
+
const result = [
|
|
98
|
+
...agent.permissions,
|
|
99
|
+
{ action: "skill", resource: "*", effect: "deny" },
|
|
100
|
+
...agent.skills.map((skill) => ({ action: "skill", resource: skill, effect: "allow" }))
|
|
101
|
+
];
|
|
102
|
+
for (const server of mcpServers) {
|
|
103
|
+
const prefix = `${normalizeMcpName(server)}_`;
|
|
104
|
+
result.push({
|
|
105
|
+
action: `${prefix}*`,
|
|
106
|
+
resource: "*",
|
|
107
|
+
effect: agent.mcp.includes(server) ? "allow" : "deny"
|
|
108
|
+
});
|
|
109
|
+
result.push(...agent.permissions.filter((rule) => rule.action.startsWith(prefix)));
|
|
110
|
+
}
|
|
111
|
+
for (const rule of agent.permissions) {
|
|
112
|
+
if (rule.action !== "shell")
|
|
113
|
+
continue;
|
|
114
|
+
if (!/(^|\s|["'])git(?:$|\s)/.test(rule.resource))
|
|
115
|
+
continue;
|
|
116
|
+
if (rule.resource.startsWith("GIT_"))
|
|
117
|
+
continue;
|
|
118
|
+
result.push(...withEnvPrefixes(rule));
|
|
119
|
+
}
|
|
120
|
+
return result;
|
|
121
|
+
}
|
|
122
|
+
|
|
6
123
|
// src/config.ts
|
|
7
|
-
import { existsSync, readdirSync, readFileSync, realpathSync } from "fs";
|
|
8
|
-
import {
|
|
9
|
-
import { dirname, isAbsolute, join, relative, resolve } from "path";
|
|
124
|
+
import { existsSync as existsSync3, lstatSync as lstatSync3, readdirSync as readdirSync2, readFileSync as readFileSync2, realpathSync as realpathSync3 } from "fs";
|
|
125
|
+
import { dirname as dirname3, isAbsolute as isAbsolute4, join as join3, relative as relative3, resolve as resolve4 } from "path";
|
|
10
126
|
import { fileURLToPath } from "url";
|
|
11
127
|
|
|
12
128
|
// node_modules/jsonc-parser/lib/esm/impl/scanner.js
|
|
@@ -5474,191 +5590,178 @@ function refine(fn, _params = {}) {
|
|
|
5474
5590
|
function superRefine(fn, params) {
|
|
5475
5591
|
return _superRefine(fn, params);
|
|
5476
5592
|
}
|
|
5477
|
-
// src/config.ts
|
|
5478
|
-
|
|
5479
|
-
|
|
5480
|
-
|
|
5481
|
-
|
|
5482
|
-
|
|
5483
|
-
|
|
5484
|
-
|
|
5485
|
-
var fileLeaseRoleSchema = _enum(["coordinator", "writer", "readonly"]);
|
|
5486
|
-
var agentPatchSchema = object({
|
|
5487
|
-
description: string2().min(1).optional(),
|
|
5488
|
-
mode: _enum(["primary", "subagent", "all"]).optional(),
|
|
5489
|
-
models: array(modelRefSchema).min(1).optional(),
|
|
5490
|
-
prompt: string2().min(1).optional(),
|
|
5491
|
-
skills: array(string2().min(1)).optional(),
|
|
5492
|
-
mcp: array(string2().min(1)).optional(),
|
|
5493
|
-
permissions: array(permissionSchema).optional(),
|
|
5494
|
-
fileLease: fileLeaseRoleSchema.optional(),
|
|
5495
|
-
disabled: boolean2().optional()
|
|
5496
|
-
});
|
|
5497
|
-
var rootPatchSchema = object({
|
|
5498
|
-
defaultAgent: agentIdSchema.optional(),
|
|
5499
|
-
agentsDirectory: string2().min(1).optional(),
|
|
5500
|
-
agents: record(agentIdSchema, agentPatchSchema).optional()
|
|
5501
|
-
});
|
|
5502
|
-
var resolvedAgentSchema = agentPatchSchema.extend({
|
|
5503
|
-
description: string2().min(1),
|
|
5504
|
-
mode: _enum(["primary", "subagent", "all"]),
|
|
5505
|
-
models: array(modelRefSchema).min(1),
|
|
5506
|
-
prompt: string2().min(1),
|
|
5507
|
-
skills: array(string2().min(1)),
|
|
5508
|
-
mcp: array(string2().min(1)),
|
|
5509
|
-
permissions: array(permissionSchema),
|
|
5510
|
-
fileLease: fileLeaseRoleSchema,
|
|
5511
|
-
disabled: boolean2()
|
|
5512
|
-
});
|
|
5513
|
-
function readJsonc(path) {
|
|
5514
|
-
const errors = [];
|
|
5515
|
-
const value = parse2(readFileSync(path, "utf8"), errors, {
|
|
5516
|
-
allowTrailingComma: true,
|
|
5517
|
-
disallowComments: false
|
|
5518
|
-
});
|
|
5519
|
-
if (errors.length > 0) {
|
|
5520
|
-
const details = errors.map((error) => `${printParseErrorCode(error.error)} at offset ${error.offset}`).join(", ");
|
|
5521
|
-
throw new Error(`Invalid JSONC in ${path}: ${details}`);
|
|
5522
|
-
}
|
|
5523
|
-
return value;
|
|
5524
|
-
}
|
|
5525
|
-
function assertWithin(base, target, label) {
|
|
5526
|
-
const child = relative(base, target);
|
|
5527
|
-
if (child === "" || !child.startsWith("..") && !isAbsolute(child))
|
|
5528
|
-
return;
|
|
5529
|
-
throw new Error(`${label} must stay inside ${base}: ${target}`);
|
|
5530
|
-
}
|
|
5531
|
-
function resolvePrompt(patch, sourcePath, layerDirectory) {
|
|
5532
|
-
if (!patch.prompt)
|
|
5533
|
-
return patch;
|
|
5534
|
-
const prompt = resolve(dirname(sourcePath), patch.prompt);
|
|
5535
|
-
assertWithin(layerDirectory, prompt, "Agent prompt");
|
|
5536
|
-
if (!existsSync(prompt))
|
|
5537
|
-
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 };
|
|
5593
|
+
// src/config-root.ts
|
|
5594
|
+
import { homedir } from "os";
|
|
5595
|
+
import { isAbsolute, join, resolve } from "path";
|
|
5596
|
+
var GVOZD_CONFIG_ROOT_ENV = "GVOZD_OPENCODE_CONFIG_ROOT";
|
|
5597
|
+
function absolute(path, label) {
|
|
5598
|
+
if (!isAbsolute(path))
|
|
5599
|
+
throw new Error(`${label} must be an absolute path`);
|
|
5600
|
+
return resolve(path);
|
|
5541
5601
|
}
|
|
5542
|
-
function
|
|
5543
|
-
|
|
5544
|
-
|
|
5545
|
-
|
|
5546
|
-
|
|
5547
|
-
return {
|
|
5602
|
+
function resolveOpenCodeConfigRootContract(env = process.env, platform = process.platform, home = homedir(), explicitRoot) {
|
|
5603
|
+
if (explicitRoot)
|
|
5604
|
+
return { path: absolute(explicitRoot, "OpenCode config root"), source: "explicit" };
|
|
5605
|
+
const override = env[GVOZD_CONFIG_ROOT_ENV];
|
|
5606
|
+
if (override) {
|
|
5607
|
+
return { path: absolute(override, GVOZD_CONFIG_ROOT_ENV), source: GVOZD_CONFIG_ROOT_ENV };
|
|
5548
5608
|
}
|
|
5549
|
-
|
|
5550
|
-
|
|
5551
|
-
for (const [id, patch] of Object.entries(root.agents ?? {})) {
|
|
5552
|
-
agents[id] = resolvePrompt(patch, rootPath, directory);
|
|
5609
|
+
if (env.XDG_CONFIG_HOME) {
|
|
5610
|
+
return { path: join(absolute(env.XDG_CONFIG_HOME, "XDG_CONFIG_HOME"), "opencode"), source: "XDG_CONFIG_HOME" };
|
|
5553
5611
|
}
|
|
5554
|
-
|
|
5555
|
-
|
|
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))) {
|
|
5559
|
-
if (!entry.isFile() || !entry.name.endsWith(".jsonc"))
|
|
5560
|
-
continue;
|
|
5561
|
-
const id = entry.name.slice(0, -".jsonc".length);
|
|
5562
|
-
agentIdSchema.parse(id);
|
|
5563
|
-
const agentPath = join(agentsDirectory, entry.name);
|
|
5564
|
-
const patch = agentPatchSchema.parse(readJsonc(agentPath));
|
|
5565
|
-
agents[id] = mergeAgent(agents[id], resolvePrompt(patch, agentPath, directory));
|
|
5566
|
-
}
|
|
5612
|
+
if (platform === "win32" && env.APPDATA) {
|
|
5613
|
+
return { path: join(env.APPDATA, "opencode"), source: "APPDATA" };
|
|
5567
5614
|
}
|
|
5568
|
-
return {
|
|
5569
|
-
defaultAgent: root.defaultAgent,
|
|
5570
|
-
agents,
|
|
5571
|
-
sources: [rootPath]
|
|
5572
|
-
};
|
|
5615
|
+
return { path: join(home, ".config", "opencode"), source: "platform-default" };
|
|
5573
5616
|
}
|
|
5574
|
-
function
|
|
5575
|
-
return
|
|
5617
|
+
function resolveOpenCodeConfigRoot(env = process.env, platform = process.platform, home = homedir(), explicitRoot) {
|
|
5618
|
+
return resolveOpenCodeConfigRootContract(env, platform, home, explicitRoot).path;
|
|
5576
5619
|
}
|
|
5577
|
-
|
|
5578
|
-
|
|
5579
|
-
|
|
5580
|
-
|
|
5581
|
-
|
|
5582
|
-
|
|
5583
|
-
|
|
5584
|
-
|
|
5585
|
-
...parsed
|
|
5586
|
-
});
|
|
5587
|
-
}
|
|
5588
|
-
function findPackageRoot() {
|
|
5589
|
-
let current = dirname(fileURLToPath(import.meta.url));
|
|
5620
|
+
|
|
5621
|
+
// src/project-trust.ts
|
|
5622
|
+
import { createHash } from "crypto";
|
|
5623
|
+
import { existsSync, lstatSync, readdirSync, readFileSync, realpathSync } from "fs";
|
|
5624
|
+
import { dirname, isAbsolute as isAbsolute2, join as join2, relative, resolve as resolve2 } from "path";
|
|
5625
|
+
var PROJECT_TRUST_ENV = "GVOZD_TRUST_PROJECT_CONFIG";
|
|
5626
|
+
function findRoot(start) {
|
|
5627
|
+
let current = resolve2(start);
|
|
5590
5628
|
while (true) {
|
|
5591
|
-
if (existsSync(
|
|
5592
|
-
return current;
|
|
5629
|
+
if (existsSync(join2(current, ".git")))
|
|
5630
|
+
return realpathSync(current);
|
|
5593
5631
|
const parent = dirname(current);
|
|
5594
5632
|
if (parent === current)
|
|
5595
|
-
|
|
5633
|
+
return realpathSync(resolve2(start));
|
|
5596
5634
|
current = parent;
|
|
5597
5635
|
}
|
|
5598
|
-
throw new Error("Unable to locate agent-gvozd package defaults");
|
|
5599
5636
|
}
|
|
5600
|
-
function
|
|
5601
|
-
|
|
5602
|
-
|
|
5603
|
-
|
|
5604
|
-
|
|
5605
|
-
return current;
|
|
5606
|
-
const parent = dirname(current);
|
|
5607
|
-
if (parent === current)
|
|
5608
|
-
return resolve(start);
|
|
5609
|
-
current = parent;
|
|
5610
|
-
}
|
|
5637
|
+
function within(base, target, label) {
|
|
5638
|
+
const child = relative(base, target);
|
|
5639
|
+
if (child === "" || !child.startsWith("..") && !isAbsolute2(child))
|
|
5640
|
+
return;
|
|
5641
|
+
throw new Error(`${label} must stay inside ${base}: ${target}`);
|
|
5611
5642
|
}
|
|
5612
|
-
function
|
|
5613
|
-
|
|
5614
|
-
|
|
5615
|
-
|
|
5616
|
-
|
|
5617
|
-
|
|
5643
|
+
function rejectSymlinkComponents(base, target, label) {
|
|
5644
|
+
within(base, target, label);
|
|
5645
|
+
const segments = relative(base, target).split(/[\\/]/).filter(Boolean);
|
|
5646
|
+
let current = base;
|
|
5647
|
+
for (const segment of segments) {
|
|
5648
|
+
current = join2(current, segment);
|
|
5649
|
+
if (!existsSync(current))
|
|
5650
|
+
continue;
|
|
5651
|
+
if (lstatSync(current).isSymbolicLink())
|
|
5652
|
+
throw new Error(`${label} must not contain symlink components: ${current}`);
|
|
5653
|
+
}
|
|
5618
5654
|
}
|
|
5619
|
-
function
|
|
5620
|
-
const
|
|
5621
|
-
const
|
|
5622
|
-
const
|
|
5623
|
-
|
|
5624
|
-
|
|
5625
|
-
|
|
5626
|
-
loadLayer(globalConfigDirectory, "config.jsonc", false),
|
|
5627
|
-
...options.includeProject === false ? [] : [loadLayer(projectConfigDirectory, "config.jsonc", false)]
|
|
5628
|
-
];
|
|
5629
|
-
let defaultAgent;
|
|
5630
|
-
const agents = {};
|
|
5631
|
-
for (const layer of layers) {
|
|
5632
|
-
defaultAgent = layer.defaultAgent ?? defaultAgent;
|
|
5633
|
-
for (const [id, patch] of Object.entries(layer.agents)) {
|
|
5634
|
-
agents[id] = mergeAgent(agents[id], patch);
|
|
5635
|
-
}
|
|
5655
|
+
function readJsonc(path) {
|
|
5656
|
+
const bytes = readFileSync(path);
|
|
5657
|
+
const errors = [];
|
|
5658
|
+
const value = parse2(bytes.toString("utf8"), errors, { allowTrailingComma: true, disallowComments: false });
|
|
5659
|
+
if (errors.length > 0 || !value || typeof value !== "object" || Array.isArray(value)) {
|
|
5660
|
+
const details = errors.map((error) => `${printParseErrorCode(error.error)} at offset ${error.offset}`).join(", ");
|
|
5661
|
+
throw new Error(`Invalid JSONC in ${path}${details ? `: ${details}` : ""}`);
|
|
5636
5662
|
}
|
|
5637
|
-
|
|
5638
|
-
|
|
5639
|
-
|
|
5640
|
-
|
|
5641
|
-
|
|
5642
|
-
|
|
5643
|
-
|
|
5644
|
-
|
|
5645
|
-
|
|
5663
|
+
return { value, bytes };
|
|
5664
|
+
}
|
|
5665
|
+
function promptFromPatch(patch) {
|
|
5666
|
+
if (!patch || typeof patch !== "object" || Array.isArray(patch))
|
|
5667
|
+
return;
|
|
5668
|
+
const prompt = patch.prompt;
|
|
5669
|
+
return typeof prompt === "string" && prompt.length > 0 ? prompt : undefined;
|
|
5670
|
+
}
|
|
5671
|
+
function collectProjectTrustInputs(projectDirectory) {
|
|
5672
|
+
const canonicalRoot = findRoot(projectDirectory);
|
|
5673
|
+
const layer = join2(canonicalRoot, "docs", ".gvozd");
|
|
5674
|
+
const rootPath = join2(layer, "config.jsonc");
|
|
5675
|
+
if (!existsSync(rootPath))
|
|
5676
|
+
return {
|
|
5677
|
+
canonicalRoot,
|
|
5678
|
+
inputs: [{ identity: "docs/.gvozd/config.jsonc:<missing>", bytes: Buffer.alloc(0) }]
|
|
5679
|
+
};
|
|
5680
|
+
rejectSymlinkComponents(canonicalRoot, rootPath, "Project config path");
|
|
5681
|
+
const rootStat = lstatSync(rootPath);
|
|
5682
|
+
if (!rootStat.isFile() || rootStat.isSymbolicLink())
|
|
5683
|
+
throw new Error(`Project config must be a regular non-symlink file: ${rootPath}`);
|
|
5684
|
+
const root = readJsonc(rootPath);
|
|
5685
|
+
const files = new Map([[relative(canonicalRoot, rootPath).replaceAll("\\", "/"), root.bytes]]);
|
|
5686
|
+
const prompts = [];
|
|
5687
|
+
const inlineAgents = root.value.agents;
|
|
5688
|
+
if (inlineAgents && typeof inlineAgents === "object" && !Array.isArray(inlineAgents)) {
|
|
5689
|
+
for (const patch of Object.values(inlineAgents)) {
|
|
5690
|
+
const prompt = promptFromPatch(patch);
|
|
5691
|
+
if (prompt)
|
|
5692
|
+
prompts.push({ source: rootPath, value: prompt });
|
|
5693
|
+
}
|
|
5694
|
+
}
|
|
5695
|
+
const configuredDirectory = root.value.agentsDirectory;
|
|
5696
|
+
if (configuredDirectory !== undefined && (typeof configuredDirectory !== "string" || configuredDirectory.length === 0)) {
|
|
5697
|
+
throw new Error(`Invalid agentsDirectory in ${rootPath}`);
|
|
5698
|
+
}
|
|
5699
|
+
const agentsDirectory = resolve2(layer, configuredDirectory ?? "agents");
|
|
5700
|
+
within(layer, agentsDirectory, "agentsDirectory");
|
|
5701
|
+
if (existsSync(agentsDirectory)) {
|
|
5702
|
+
rejectSymlinkComponents(canonicalRoot, agentsDirectory, "agentsDirectory");
|
|
5703
|
+
const stat = lstatSync(agentsDirectory);
|
|
5704
|
+
if (!stat.isDirectory() || stat.isSymbolicLink())
|
|
5705
|
+
throw new Error(`agentsDirectory must be a regular directory: ${agentsDirectory}`);
|
|
5706
|
+
within(realpathSync(layer), realpathSync(agentsDirectory), "agentsDirectory");
|
|
5707
|
+
for (const entry of readdirSync(agentsDirectory, { withFileTypes: true }).sort((left, right) => left.name.localeCompare(right.name))) {
|
|
5708
|
+
if (!entry.name.endsWith(".jsonc"))
|
|
5709
|
+
continue;
|
|
5710
|
+
const path = join2(agentsDirectory, entry.name);
|
|
5711
|
+
if (!entry.isFile())
|
|
5712
|
+
throw new Error(`Agent fragment must be a regular non-symlink file: ${path}`);
|
|
5713
|
+
rejectSymlinkComponents(canonicalRoot, path, "Agent fragment path");
|
|
5714
|
+
const stat = lstatSync(path);
|
|
5715
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
5716
|
+
throw new Error(`Agent fragment must be a regular non-symlink file: ${path}`);
|
|
5717
|
+
const fragment = readJsonc(path);
|
|
5718
|
+
files.set(relative(canonicalRoot, path).replaceAll("\\", "/"), fragment.bytes);
|
|
5719
|
+
const prompt = promptFromPatch(fragment.value);
|
|
5720
|
+
if (prompt)
|
|
5721
|
+
prompts.push({ source: path, value: prompt });
|
|
5722
|
+
}
|
|
5723
|
+
}
|
|
5724
|
+
for (const prompt of prompts) {
|
|
5725
|
+
const path = resolve2(dirname(prompt.source), prompt.value);
|
|
5726
|
+
within(layer, path, "Agent prompt");
|
|
5727
|
+
rejectSymlinkComponents(canonicalRoot, path, "Agent prompt");
|
|
5728
|
+
if (!existsSync(path))
|
|
5729
|
+
throw new Error(`Agent prompt is missing: ${path}`);
|
|
5730
|
+
const stat = lstatSync(path);
|
|
5731
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
5732
|
+
throw new Error(`Agent prompt must be a regular non-symlink file: ${path}`);
|
|
5733
|
+
const canonical = realpathSync(path);
|
|
5734
|
+
within(realpathSync(layer), canonical, "Agent prompt");
|
|
5735
|
+
files.set(relative(canonicalRoot, path).replaceAll("\\", "/"), readFileSync(path));
|
|
5646
5736
|
}
|
|
5647
5737
|
return {
|
|
5648
|
-
|
|
5649
|
-
|
|
5650
|
-
packageRoot,
|
|
5651
|
-
projectRoot,
|
|
5652
|
-
projectConfigDirectory,
|
|
5653
|
-
globalConfigDirectory,
|
|
5654
|
-
sources: layers.flatMap((layer) => layer.sources)
|
|
5738
|
+
canonicalRoot,
|
|
5739
|
+
inputs: [...files].map(([identity, bytes]) => ({ identity, bytes })).sort((left, right) => left.identity.localeCompare(right.identity))
|
|
5655
5740
|
};
|
|
5656
5741
|
}
|
|
5742
|
+
function computeProjectTrustToken(projectDirectory) {
|
|
5743
|
+
const collected = collectProjectTrustInputs(projectDirectory);
|
|
5744
|
+
const hash = createHash("sha256");
|
|
5745
|
+
hash.update("agent-gvozd-project-trust-v1\x00");
|
|
5746
|
+
hash.update(collected.canonicalRoot);
|
|
5747
|
+
hash.update("\x00");
|
|
5748
|
+
for (const input of collected.inputs) {
|
|
5749
|
+
hash.update(String(Buffer.byteLength(input.identity)));
|
|
5750
|
+
hash.update(":");
|
|
5751
|
+
hash.update(input.identity);
|
|
5752
|
+
hash.update(":");
|
|
5753
|
+
hash.update(String(input.bytes.length));
|
|
5754
|
+
hash.update(":");
|
|
5755
|
+
hash.update(input.bytes);
|
|
5756
|
+
hash.update("\x00");
|
|
5757
|
+
}
|
|
5758
|
+
return `sha256:${hash.digest("hex")}`;
|
|
5759
|
+
}
|
|
5657
5760
|
|
|
5658
5761
|
// src/file-leases.ts
|
|
5659
5762
|
import { randomUUID } from "crypto";
|
|
5660
|
-
import { existsSync as existsSync2, lstatSync, realpathSync as realpathSync2, statSync } from "fs";
|
|
5661
|
-
import { basename, dirname as dirname2, isAbsolute as
|
|
5763
|
+
import { existsSync as existsSync2, lstatSync as lstatSync2, realpathSync as realpathSync2, statSync } from "fs";
|
|
5764
|
+
import { basename, dirname as dirname2, isAbsolute as isAbsolute3, relative as relative2, resolve as resolve3, sep } from "path";
|
|
5662
5765
|
|
|
5663
5766
|
class LeaseError extends Error {
|
|
5664
5767
|
code;
|
|
@@ -5670,6 +5773,18 @@ class LeaseError extends Error {
|
|
|
5670
5773
|
}
|
|
5671
5774
|
var DEFAULT_RESERVATION_TTL_MS = 5 * 60 * 1000;
|
|
5672
5775
|
var DEFAULT_ACTIVE_TTL_MS = 30 * 60 * 1000;
|
|
5776
|
+
var GVOZD_CASE_INSENSITIVE_FILESYSTEM = "GVOZD_CASE_INSENSITIVE_FILESYSTEM";
|
|
5777
|
+
function resolveCaseInsensitiveFilesystem(env = process.env, platform = process.platform) {
|
|
5778
|
+
const override = env[GVOZD_CASE_INSENSITIVE_FILESYSTEM];
|
|
5779
|
+
if (override === "1")
|
|
5780
|
+
return true;
|
|
5781
|
+
if (override === "0")
|
|
5782
|
+
return false;
|
|
5783
|
+
if (override !== undefined) {
|
|
5784
|
+
throw new Error(`${GVOZD_CASE_INSENSITIVE_FILESYSTEM} must be exactly 1 or 0`);
|
|
5785
|
+
}
|
|
5786
|
+
return platform === "darwin" || platform === "win32";
|
|
5787
|
+
}
|
|
5673
5788
|
function nonEmpty(value, label) {
|
|
5674
5789
|
const normalized = value.trim();
|
|
5675
5790
|
if (normalized === "")
|
|
@@ -5683,7 +5798,7 @@ function assertPositiveDuration(value, label) {
|
|
|
5683
5798
|
}
|
|
5684
5799
|
function isWithin(root, target) {
|
|
5685
5800
|
const child = relative2(root, target);
|
|
5686
|
-
return child !== "" && !child.startsWith(`..${sep}`) && child !== ".." && !
|
|
5801
|
+
return child !== "" && !child.startsWith(`..${sep}`) && child !== ".." && !isAbsolute3(child);
|
|
5687
5802
|
}
|
|
5688
5803
|
function displayPath(root, target) {
|
|
5689
5804
|
return relative2(root, target).replaceAll("\\", "/");
|
|
@@ -5695,6 +5810,7 @@ class FileLeaseManager {
|
|
|
5695
5810
|
activeTtlMs;
|
|
5696
5811
|
now;
|
|
5697
5812
|
createID;
|
|
5813
|
+
caseInsensitive;
|
|
5698
5814
|
leases = new Map;
|
|
5699
5815
|
fileOwners = new Map;
|
|
5700
5816
|
sessionOwners = new Map;
|
|
@@ -5706,6 +5822,7 @@ class FileLeaseManager {
|
|
|
5706
5822
|
this.activeTtlMs = assertPositiveDuration(options.activeTtlMs ?? DEFAULT_ACTIVE_TTL_MS, "activeTtlMs");
|
|
5707
5823
|
this.now = options.now ?? Date.now;
|
|
5708
5824
|
this.createID = options.createID ?? randomUUID;
|
|
5825
|
+
this.caseInsensitive = options.caseInsensitive ?? resolveCaseInsensitiveFilesystem({}, options.platform ?? process.platform);
|
|
5709
5826
|
}
|
|
5710
5827
|
reserve(input) {
|
|
5711
5828
|
this.sweep();
|
|
@@ -5724,14 +5841,14 @@ class FileLeaseManager {
|
|
|
5724
5841
|
agent,
|
|
5725
5842
|
label,
|
|
5726
5843
|
state: "reserved",
|
|
5727
|
-
files: new
|
|
5844
|
+
files: new Map(files.map((file) => [file.key, file.canonicalPath])),
|
|
5728
5845
|
createdAt: now,
|
|
5729
5846
|
lastActivityAt: now,
|
|
5730
5847
|
expiresAt: now + this.reservationTtlMs
|
|
5731
5848
|
};
|
|
5732
5849
|
this.leases.set(leaseId, lease);
|
|
5733
5850
|
for (const file of files)
|
|
5734
|
-
this.fileOwners.set(file, leaseId);
|
|
5851
|
+
this.fileOwners.set(file.key, leaseId);
|
|
5735
5852
|
return this.toStatus(lease);
|
|
5736
5853
|
}
|
|
5737
5854
|
extend(input) {
|
|
@@ -5743,8 +5860,9 @@ class FileLeaseManager {
|
|
|
5743
5860
|
const files = this.normalizeLeaseFiles(input.files);
|
|
5744
5861
|
this.assertFilesAvailable(files, lease.leaseId);
|
|
5745
5862
|
for (const file of files) {
|
|
5746
|
-
lease.files.
|
|
5747
|
-
|
|
5863
|
+
if (!lease.files.has(file.key))
|
|
5864
|
+
lease.files.set(file.key, file.canonicalPath);
|
|
5865
|
+
this.fileOwners.set(file.key, lease.leaseId);
|
|
5748
5866
|
}
|
|
5749
5867
|
this.refresh(lease);
|
|
5750
5868
|
return this.toStatus(lease);
|
|
@@ -5790,10 +5908,11 @@ class FileLeaseManager {
|
|
|
5790
5908
|
if (!leaseId)
|
|
5791
5909
|
throw new LeaseError("NO_ACTIVE_LEASE", `Session ${sessionID} has no active file lease`);
|
|
5792
5910
|
const lease = this.requireLease(leaseId);
|
|
5793
|
-
const files = resources.map((resource) => this.canonicalize(resource, true));
|
|
5794
|
-
const
|
|
5911
|
+
const files = resources.map((resource) => this.toLeaseFile(this.canonicalize(resource, true)));
|
|
5912
|
+
const canonicalPaths = new Set(lease.files.values());
|
|
5913
|
+
const outside = files.find((file) => !canonicalPaths.has(file.canonicalPath));
|
|
5795
5914
|
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`);
|
|
5915
|
+
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
5916
|
}
|
|
5798
5917
|
this.refresh(lease);
|
|
5799
5918
|
return this.toStatus(lease);
|
|
@@ -5872,18 +5991,18 @@ class FileLeaseManager {
|
|
|
5872
5991
|
this.leases.delete(lease.leaseId);
|
|
5873
5992
|
if (lease.sessionID)
|
|
5874
5993
|
this.sessionOwners.delete(lease.sessionID);
|
|
5875
|
-
for (const
|
|
5876
|
-
if (this.fileOwners.get(
|
|
5877
|
-
this.fileOwners.delete(
|
|
5994
|
+
for (const key of lease.files.keys()) {
|
|
5995
|
+
if (this.fileOwners.get(key) === lease.leaseId)
|
|
5996
|
+
this.fileOwners.delete(key);
|
|
5878
5997
|
}
|
|
5879
5998
|
}
|
|
5880
5999
|
assertFilesAvailable(files, currentLeaseId) {
|
|
5881
6000
|
for (const file of files) {
|
|
5882
|
-
const ownerId = this.fileOwners.get(file);
|
|
6001
|
+
const ownerId = this.fileOwners.get(file.key);
|
|
5883
6002
|
if (!ownerId || ownerId === currentLeaseId)
|
|
5884
6003
|
continue;
|
|
5885
6004
|
const owner = this.leases.get(ownerId);
|
|
5886
|
-
const relativeFile = displayPath(this.projectRoot, file);
|
|
6005
|
+
const relativeFile = displayPath(this.projectRoot, file.canonicalPath);
|
|
5887
6006
|
if (!owner)
|
|
5888
6007
|
throw new LeaseError("FILE_CONFLICT", `File ${relativeFile} is already reserved`);
|
|
5889
6008
|
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 +6011,27 @@ class FileLeaseManager {
|
|
|
5892
6011
|
normalizeLeaseFiles(files) {
|
|
5893
6012
|
if (files.length === 0)
|
|
5894
6013
|
throw new LeaseError("INVALID_PATH", "A lease requires at least one exact file");
|
|
5895
|
-
|
|
6014
|
+
const normalized = new Map;
|
|
6015
|
+
for (const file of files) {
|
|
6016
|
+
const canonical = this.toLeaseFile(this.canonicalize(file, false));
|
|
6017
|
+
const existing = normalized.get(canonical.key);
|
|
6018
|
+
if (!existing)
|
|
6019
|
+
normalized.set(canonical.key, canonical);
|
|
6020
|
+
else if (existing.canonicalPath !== canonical.canonicalPath) {
|
|
6021
|
+
throw new LeaseError("INVALID_PATH", `Lease request contains ambiguous file aliases ${displayPath(this.projectRoot, existing.canonicalPath)} and ${displayPath(this.projectRoot, canonical.canonicalPath)}`);
|
|
6022
|
+
}
|
|
6023
|
+
}
|
|
6024
|
+
return [...normalized.values()].sort((left, right) => left.canonicalPath.localeCompare(right.canonicalPath));
|
|
6025
|
+
}
|
|
6026
|
+
toLeaseFile(canonicalPath) {
|
|
6027
|
+
const key = this.caseInsensitive ? canonicalPath.toLowerCase().normalize("NFC") : canonicalPath;
|
|
6028
|
+
return { canonicalPath, key };
|
|
5896
6029
|
}
|
|
5897
6030
|
canonicalize(input, allowAbsolute) {
|
|
5898
6031
|
const value = input.trim();
|
|
5899
6032
|
if (value === "" || value.includes("\x00"))
|
|
5900
6033
|
throw new LeaseError("INVALID_PATH", "File path must not be empty");
|
|
5901
|
-
if (!allowAbsolute &&
|
|
6034
|
+
if (!allowAbsolute && isAbsolute3(value))
|
|
5902
6035
|
throw new LeaseError("INVALID_PATH", `Absolute lease path is not allowed: ${value}`);
|
|
5903
6036
|
if (value.includes("*") || value.includes("?")) {
|
|
5904
6037
|
throw new LeaseError("INVALID_PATH", `Lease paths must name exact files, not patterns: ${value}`);
|
|
@@ -5906,10 +6039,24 @@ class FileLeaseManager {
|
|
|
5906
6039
|
if (value.endsWith("/") || value.endsWith("\\")) {
|
|
5907
6040
|
throw new LeaseError("INVALID_PATH", `Lease paths must name files, not directories: ${value}`);
|
|
5908
6041
|
}
|
|
5909
|
-
const candidate =
|
|
5910
|
-
if ((!allowAbsolute || !
|
|
6042
|
+
const candidate = resolve3(this.projectRoot, value);
|
|
6043
|
+
if ((!allowAbsolute || !isAbsolute3(value)) && !isWithin(this.projectRoot, candidate)) {
|
|
5911
6044
|
throw new LeaseError("INVALID_PATH", `File must stay inside the project root: ${value}`);
|
|
5912
6045
|
}
|
|
6046
|
+
const code = allowAbsolute ? "OUT_OF_SCOPE" : "INVALID_PATH";
|
|
6047
|
+
if (existsSync2(candidate)) {
|
|
6048
|
+
try {
|
|
6049
|
+
const target = lstatSync2(candidate);
|
|
6050
|
+
if (!target.isSymbolicLink() && !target.isFile()) {
|
|
6051
|
+
throw new LeaseError(code, `Lease targets must be regular files: ${value}`);
|
|
6052
|
+
}
|
|
6053
|
+
} catch (error) {
|
|
6054
|
+
if (error instanceof LeaseError)
|
|
6055
|
+
throw error;
|
|
6056
|
+
if (error.code !== "ENOENT")
|
|
6057
|
+
throw error;
|
|
6058
|
+
}
|
|
6059
|
+
}
|
|
5913
6060
|
let ancestor = candidate;
|
|
5914
6061
|
const suffix = [];
|
|
5915
6062
|
while (!existsSync2(ancestor)) {
|
|
@@ -5925,13 +6072,17 @@ class FileLeaseManager {
|
|
|
5925
6072
|
} catch {
|
|
5926
6073
|
throw new LeaseError("INVALID_PATH", `Cannot canonicalize file path: ${value}`);
|
|
5927
6074
|
}
|
|
5928
|
-
const canonical =
|
|
6075
|
+
const canonical = resolve3(canonicalAncestor, ...suffix);
|
|
5929
6076
|
if (!isWithin(this.projectRoot, canonical)) {
|
|
5930
6077
|
throw new LeaseError("INVALID_PATH", `File resolves outside the project root: ${value}`);
|
|
5931
6078
|
}
|
|
5932
6079
|
try {
|
|
5933
|
-
|
|
5934
|
-
|
|
6080
|
+
const target = lstatSync2(canonical);
|
|
6081
|
+
if (!target.isFile()) {
|
|
6082
|
+
throw new LeaseError(code, `Lease targets must be regular files: ${value}`);
|
|
6083
|
+
}
|
|
6084
|
+
if (target.nlink > 1) {
|
|
6085
|
+
throw new LeaseError(code, `Hard-linked files cannot be used as lease targets: ${value}`);
|
|
5935
6086
|
}
|
|
5936
6087
|
} catch (error) {
|
|
5937
6088
|
if (error instanceof LeaseError)
|
|
@@ -5949,7 +6100,7 @@ class FileLeaseManager {
|
|
|
5949
6100
|
agent: lease.agent,
|
|
5950
6101
|
label: lease.label,
|
|
5951
6102
|
state: lease.state,
|
|
5952
|
-
files: [...lease.files].map((file) => displayPath(this.projectRoot, file)).sort(),
|
|
6103
|
+
files: [...lease.files.values()].map((file) => displayPath(this.projectRoot, file)).sort(),
|
|
5953
6104
|
createdAt: lease.createdAt,
|
|
5954
6105
|
...lease.claimedAt === undefined ? {} : { claimedAt: lease.claimedAt },
|
|
5955
6106
|
lastActivityAt: lease.lastActivityAt,
|
|
@@ -5958,6 +6109,243 @@ class FileLeaseManager {
|
|
|
5958
6109
|
}
|
|
5959
6110
|
}
|
|
5960
6111
|
|
|
6112
|
+
// src/config.ts
|
|
6113
|
+
var permissionSchema = object({
|
|
6114
|
+
action: string2().min(1),
|
|
6115
|
+
resource: string2().min(1),
|
|
6116
|
+
effect: _enum(["allow", "ask", "deny"])
|
|
6117
|
+
}).strict();
|
|
6118
|
+
var modelRefSchema = string2().min(1).regex(/^[^/#\s]+\/[^#\s]+(?:#[^#\s]+)?$/, "Expected provider/model or provider/model#variant");
|
|
6119
|
+
var agentIdSchema = string2().regex(/^[A-Za-z0-9][A-Za-z0-9_-]*$/, "Expected a filesystem-safe agent ID");
|
|
6120
|
+
var fileLeaseRoleSchema = _enum(["coordinator", "writer", "readonly"]);
|
|
6121
|
+
var agentPatchSchema = object({
|
|
6122
|
+
description: string2().min(1).optional(),
|
|
6123
|
+
mode: _enum(["primary", "subagent", "all"]).optional(),
|
|
6124
|
+
models: array(modelRefSchema).min(1).optional(),
|
|
6125
|
+
prompt: string2().min(1).optional(),
|
|
6126
|
+
skills: array(string2().min(1)).optional(),
|
|
6127
|
+
mcp: array(string2().min(1)).optional(),
|
|
6128
|
+
permissions: array(permissionSchema).optional(),
|
|
6129
|
+
fileLease: fileLeaseRoleSchema.optional(),
|
|
6130
|
+
disabled: boolean2().optional()
|
|
6131
|
+
}).strict();
|
|
6132
|
+
var leaseSchema = object({
|
|
6133
|
+
reservationTtlMinutes: number2().int().positive().max(24 * 60).optional(),
|
|
6134
|
+
activeTtlMinutes: number2().int().positive().max(24 * 60).optional()
|
|
6135
|
+
}).strict();
|
|
6136
|
+
var rootPatchSchema = object({
|
|
6137
|
+
$schema: string2().min(1).optional(),
|
|
6138
|
+
defaultAgent: agentIdSchema.optional(),
|
|
6139
|
+
agentsDirectory: string2().min(1).optional(),
|
|
6140
|
+
agents: record(agentIdSchema, agentPatchSchema).optional(),
|
|
6141
|
+
lease: leaseSchema.optional()
|
|
6142
|
+
}).strict();
|
|
6143
|
+
var resolvedAgentSchema = agentPatchSchema.extend({
|
|
6144
|
+
description: string2().min(1),
|
|
6145
|
+
mode: _enum(["primary", "subagent", "all"]),
|
|
6146
|
+
models: array(modelRefSchema).min(1),
|
|
6147
|
+
prompt: string2().min(1),
|
|
6148
|
+
skills: array(string2().min(1)),
|
|
6149
|
+
mcp: array(string2().min(1)),
|
|
6150
|
+
permissions: array(permissionSchema),
|
|
6151
|
+
fileLease: fileLeaseRoleSchema,
|
|
6152
|
+
disabled: boolean2()
|
|
6153
|
+
});
|
|
6154
|
+
var SAFE_UNTRUSTED_AGENT_FIELDS = new Set(["description"]);
|
|
6155
|
+
function assertTrustedProjectPatch(patch, sourcePath, id, trusted, knownAgents) {
|
|
6156
|
+
if (trusted)
|
|
6157
|
+
return;
|
|
6158
|
+
const fields = Object.keys(patch).filter((field) => !SAFE_UNTRUSTED_AGENT_FIELDS.has(field));
|
|
6159
|
+
if (fields.length === 0 && knownAgents.has(id))
|
|
6160
|
+
return;
|
|
6161
|
+
throw new Error(`Untrusted project config ${sourcePath} cannot override ${fields.join(", ") || `unknown agent ${id}`}. ` + `Set ${PROJECT_TRUST_ENV} to the exact token returned by computeProjectTrustToken() after reviewing these files.`);
|
|
6162
|
+
}
|
|
6163
|
+
function readJsonc2(path) {
|
|
6164
|
+
const errors = [];
|
|
6165
|
+
const value = parse2(readFileSync2(path, "utf8"), errors, {
|
|
6166
|
+
allowTrailingComma: true,
|
|
6167
|
+
disallowComments: false
|
|
6168
|
+
});
|
|
6169
|
+
if (errors.length > 0) {
|
|
6170
|
+
const details = errors.map((error) => `${printParseErrorCode(error.error)} at offset ${error.offset}`).join(", ");
|
|
6171
|
+
throw new Error(`Invalid JSONC in ${path}: ${details}`);
|
|
6172
|
+
}
|
|
6173
|
+
return value;
|
|
6174
|
+
}
|
|
6175
|
+
function assertWithin(base, target, label) {
|
|
6176
|
+
const child = relative3(base, target);
|
|
6177
|
+
if (child === "" || !child.startsWith("..") && !isAbsolute4(child))
|
|
6178
|
+
return;
|
|
6179
|
+
throw new Error(`${label} must stay inside ${base}: ${target}`);
|
|
6180
|
+
}
|
|
6181
|
+
function resolvePrompt(patch, sourcePath, layerDirectory) {
|
|
6182
|
+
if (!patch.prompt)
|
|
6183
|
+
return patch;
|
|
6184
|
+
const prompt = resolve4(dirname3(sourcePath), patch.prompt);
|
|
6185
|
+
assertWithin(layerDirectory, prompt, "Agent prompt");
|
|
6186
|
+
if (!existsSync3(prompt))
|
|
6187
|
+
throw new Error(`Agent prompt is missing: ${prompt}`);
|
|
6188
|
+
const promptStat = lstatSync3(prompt);
|
|
6189
|
+
if (promptStat.isSymbolicLink() || !promptStat.isFile())
|
|
6190
|
+
throw new Error(`Agent prompt must be a regular non-symlink file: ${prompt}`);
|
|
6191
|
+
const canonical = realpathSync3(prompt);
|
|
6192
|
+
assertWithin(realpathSync3(layerDirectory), canonical, "Agent prompt");
|
|
6193
|
+
const promptContent = readFileSync2(canonical, "utf8");
|
|
6194
|
+
return { ...patch, prompt: canonical, promptContent };
|
|
6195
|
+
}
|
|
6196
|
+
function loadLayer(directory, rootFileName, required, projectPolicy) {
|
|
6197
|
+
const rootPath = join3(directory, rootFileName);
|
|
6198
|
+
if (!existsSync3(rootPath)) {
|
|
6199
|
+
if (required)
|
|
6200
|
+
throw new Error(`Required config is missing: ${rootPath}`);
|
|
6201
|
+
return { agents: {}, sources: [] };
|
|
6202
|
+
}
|
|
6203
|
+
const root = rootPatchSchema.parse(readJsonc2(rootPath));
|
|
6204
|
+
if (projectPolicy && !projectPolicy.trusted) {
|
|
6205
|
+
const restricted = ["defaultAgent", "agentsDirectory", "lease"].filter((field) => Object.prototype.hasOwnProperty.call(root, field));
|
|
6206
|
+
if (restricted.length > 0)
|
|
6207
|
+
throw new Error(`Untrusted project config ${rootPath} cannot override ${restricted.join(", ")}`);
|
|
6208
|
+
}
|
|
6209
|
+
const agents = {};
|
|
6210
|
+
for (const [id, patch] of Object.entries(root.agents ?? {})) {
|
|
6211
|
+
if (projectPolicy)
|
|
6212
|
+
assertTrustedProjectPatch(patch, rootPath, id, projectPolicy.trusted, projectPolicy.knownAgents);
|
|
6213
|
+
agents[id] = resolvePrompt(patch, rootPath, directory);
|
|
6214
|
+
}
|
|
6215
|
+
const agentsDirectory = resolve4(directory, root.agentsDirectory ?? "agents");
|
|
6216
|
+
assertWithin(directory, agentsDirectory, "agentsDirectory");
|
|
6217
|
+
if (existsSync3(agentsDirectory)) {
|
|
6218
|
+
const directoryStat = lstatSync3(agentsDirectory);
|
|
6219
|
+
if (directoryStat.isSymbolicLink() || !directoryStat.isDirectory()) {
|
|
6220
|
+
throw new Error(`agentsDirectory must be a regular directory: ${agentsDirectory}`);
|
|
6221
|
+
}
|
|
6222
|
+
assertWithin(realpathSync3(directory), realpathSync3(agentsDirectory), "agentsDirectory");
|
|
6223
|
+
for (const entry of readdirSync2(agentsDirectory, { withFileTypes: true }).sort((a, b) => a.name.localeCompare(b.name))) {
|
|
6224
|
+
if (!entry.isFile() || !entry.name.endsWith(".jsonc"))
|
|
6225
|
+
continue;
|
|
6226
|
+
const id = entry.name.slice(0, -".jsonc".length);
|
|
6227
|
+
agentIdSchema.parse(id);
|
|
6228
|
+
const agentPath = join3(agentsDirectory, entry.name);
|
|
6229
|
+
const patch = agentPatchSchema.parse(readJsonc2(agentPath));
|
|
6230
|
+
if (projectPolicy)
|
|
6231
|
+
assertTrustedProjectPatch(patch, agentPath, id, projectPolicy.trusted, projectPolicy.knownAgents);
|
|
6232
|
+
agents[id] = mergeAgent(agents[id], resolvePrompt(patch, agentPath, directory));
|
|
6233
|
+
}
|
|
6234
|
+
}
|
|
6235
|
+
return {
|
|
6236
|
+
defaultAgent: root.defaultAgent,
|
|
6237
|
+
agents,
|
|
6238
|
+
lease: root.lease,
|
|
6239
|
+
sources: [rootPath]
|
|
6240
|
+
};
|
|
6241
|
+
}
|
|
6242
|
+
function mergeAgent(base, override) {
|
|
6243
|
+
return { ...base ?? {}, ...override };
|
|
6244
|
+
}
|
|
6245
|
+
function resolveAgentConfig(patch) {
|
|
6246
|
+
const parsed = agentPatchSchema.extend({ promptContent: string2().optional() }).parse(patch);
|
|
6247
|
+
const { promptContent, ...agentPatch } = parsed;
|
|
6248
|
+
const resolved = resolvedAgentSchema.parse({
|
|
6249
|
+
skills: [],
|
|
6250
|
+
mcp: [],
|
|
6251
|
+
permissions: [],
|
|
6252
|
+
fileLease: "readonly",
|
|
6253
|
+
disabled: false,
|
|
6254
|
+
...agentPatch
|
|
6255
|
+
});
|
|
6256
|
+
return promptContent === undefined ? resolved : { ...resolved, promptContent };
|
|
6257
|
+
}
|
|
6258
|
+
function findPackageRoot() {
|
|
6259
|
+
let current = dirname3(fileURLToPath(import.meta.url));
|
|
6260
|
+
while (true) {
|
|
6261
|
+
if (existsSync3(join3(current, "defaults", "default.jsonc")))
|
|
6262
|
+
return current;
|
|
6263
|
+
const parent = dirname3(current);
|
|
6264
|
+
if (parent === current)
|
|
6265
|
+
break;
|
|
6266
|
+
current = parent;
|
|
6267
|
+
}
|
|
6268
|
+
throw new Error("Unable to locate agent-gvozd package defaults");
|
|
6269
|
+
}
|
|
6270
|
+
function findProjectRoot(start) {
|
|
6271
|
+
let current = resolve4(start);
|
|
6272
|
+
while (true) {
|
|
6273
|
+
const git = join3(current, ".git");
|
|
6274
|
+
if (existsSync3(git))
|
|
6275
|
+
return current;
|
|
6276
|
+
const parent = dirname3(current);
|
|
6277
|
+
if (parent === current)
|
|
6278
|
+
return resolve4(start);
|
|
6279
|
+
current = parent;
|
|
6280
|
+
}
|
|
6281
|
+
}
|
|
6282
|
+
function loadConfig(projectDirectory, options = {}) {
|
|
6283
|
+
const projectRoot = findProjectRoot(projectDirectory);
|
|
6284
|
+
const packageRoot = findPackageRoot();
|
|
6285
|
+
const projectConfigDirectory = join3(projectRoot, "docs", ".gvozd");
|
|
6286
|
+
const globalConfigDirectory = join3(resolveOpenCodeConfigRoot(options.env, options.platform, options.home, options.configRoot), "gvozd");
|
|
6287
|
+
const baseLayers = [
|
|
6288
|
+
loadLayer(join3(packageRoot, "defaults"), "default.jsonc", true),
|
|
6289
|
+
loadLayer(globalConfigDirectory, "config.jsonc", false)
|
|
6290
|
+
];
|
|
6291
|
+
const knownAgents = new Set(baseLayers.flatMap((layer) => Object.keys(layer.agents)));
|
|
6292
|
+
const includeProject = options.includeProject !== false;
|
|
6293
|
+
const suppliedToken = includeProject ? options.projectTrustToken ?? (options.env ?? process.env)[PROJECT_TRUST_ENV] : undefined;
|
|
6294
|
+
const trustProjectConfig = includeProject && suppliedToken !== undefined && suppliedToken === computeProjectTrustToken(projectRoot);
|
|
6295
|
+
const projectLayer = includeProject ? loadLayer(projectConfigDirectory, "config.jsonc", false, { trusted: trustProjectConfig, knownAgents }) : undefined;
|
|
6296
|
+
if (trustProjectConfig && suppliedToken !== computeProjectTrustToken(projectRoot)) {
|
|
6297
|
+
throw new Error("Project configuration changed while its trust token was being validated; review it and compute a new token");
|
|
6298
|
+
}
|
|
6299
|
+
const layers = [...baseLayers, ...projectLayer ? [projectLayer] : []];
|
|
6300
|
+
const lease = {
|
|
6301
|
+
reservationTtlMs: DEFAULT_RESERVATION_TTL_MS,
|
|
6302
|
+
activeTtlMs: DEFAULT_ACTIVE_TTL_MS
|
|
6303
|
+
};
|
|
6304
|
+
for (const layer of layers) {
|
|
6305
|
+
if (!layer.lease)
|
|
6306
|
+
continue;
|
|
6307
|
+
if (layer.lease.reservationTtlMinutes !== undefined) {
|
|
6308
|
+
lease.reservationTtlMs = layer.lease.reservationTtlMinutes * 60000;
|
|
6309
|
+
}
|
|
6310
|
+
if (layer.lease.activeTtlMinutes !== undefined) {
|
|
6311
|
+
lease.activeTtlMs = layer.lease.activeTtlMinutes * 60000;
|
|
6312
|
+
}
|
|
6313
|
+
}
|
|
6314
|
+
let defaultAgent;
|
|
6315
|
+
const agents = {};
|
|
6316
|
+
for (const layer of layers) {
|
|
6317
|
+
defaultAgent = layer.defaultAgent ?? defaultAgent;
|
|
6318
|
+
for (const [id, patch] of Object.entries(layer.agents)) {
|
|
6319
|
+
agents[id] = mergeAgent(agents[id], patch);
|
|
6320
|
+
}
|
|
6321
|
+
}
|
|
6322
|
+
if (!defaultAgent)
|
|
6323
|
+
throw new Error("defaultAgent is not configured");
|
|
6324
|
+
const resolvedAgents = Object.fromEntries(Object.entries(agents).map(([id, patch]) => [
|
|
6325
|
+
id,
|
|
6326
|
+
(() => {
|
|
6327
|
+
const agent = resolveAgentConfig(patch);
|
|
6328
|
+
if (agent.promptContent === undefined)
|
|
6329
|
+
throw new Error(`Agent prompt snapshot is missing after configuration load: ${agent.prompt}`);
|
|
6330
|
+
return agent;
|
|
6331
|
+
})()
|
|
6332
|
+
]));
|
|
6333
|
+
const defaultConfig = resolvedAgents[defaultAgent];
|
|
6334
|
+
if (!defaultConfig || defaultConfig.disabled || defaultConfig.mode === "subagent") {
|
|
6335
|
+
throw new Error(`defaultAgent must reference an enabled primary agent: ${defaultAgent}`);
|
|
6336
|
+
}
|
|
6337
|
+
return {
|
|
6338
|
+
defaultAgent,
|
|
6339
|
+
agents: resolvedAgents,
|
|
6340
|
+
lease,
|
|
6341
|
+
packageRoot,
|
|
6342
|
+
projectRoot,
|
|
6343
|
+
projectConfigDirectory,
|
|
6344
|
+
globalConfigDirectory,
|
|
6345
|
+
sources: layers.flatMap((layer) => layer.sources)
|
|
6346
|
+
};
|
|
6347
|
+
}
|
|
6348
|
+
|
|
5961
6349
|
// src/file-lease-plugin.ts
|
|
5962
6350
|
var GVOZD_LEASE_TOOL = "gvozd_lease";
|
|
5963
6351
|
var GVOZD_CLAIM_TOOL = "gvozd_claim";
|
|
@@ -5968,26 +6356,12 @@ var TERMINAL_SESSION_EVENTS = new Set([
|
|
|
5968
6356
|
"session.idle",
|
|
5969
6357
|
"session.deleted"
|
|
5970
6358
|
]);
|
|
5971
|
-
var SAFE_SHELL_DURING_ACTIVE_LEASES = new Set([
|
|
5972
|
-
|
|
5973
|
-
|
|
5974
|
-
|
|
5975
|
-
|
|
5976
|
-
|
|
5977
|
-
"git rev-parse --git-dir",
|
|
5978
|
-
"git remote -v",
|
|
5979
|
-
"git branch --show-current",
|
|
5980
|
-
"git stash list",
|
|
5981
|
-
"GIT_OPTIONAL_LOCKS=0 git status",
|
|
5982
|
-
"GIT_OPTIONAL_LOCKS=0 git status --short",
|
|
5983
|
-
"GIT_OPTIONAL_LOCKS=0 git status --short --branch",
|
|
5984
|
-
"GIT_OPTIONAL_LOCKS=0 git status --porcelain=v1 --branch",
|
|
5985
|
-
"GIT_OPTIONAL_LOCKS=0 git rev-parse --show-toplevel",
|
|
5986
|
-
"GIT_OPTIONAL_LOCKS=0 git rev-parse --git-dir",
|
|
5987
|
-
"GIT_OPTIONAL_LOCKS=0 git remote -v",
|
|
5988
|
-
"GIT_OPTIONAL_LOCKS=0 git branch --show-current",
|
|
5989
|
-
"GIT_OPTIONAL_LOCKS=0 git stash list"
|
|
5990
|
-
]);
|
|
6359
|
+
var SAFE_SHELL_DURING_ACTIVE_LEASES = new Set([...INSPECTION_COMMANDS, ...TOOLCHAIN_COMMANDS, ...GIT_READONLY_COMMANDS].flatMap(({ exact, wildcard }) => [
|
|
6360
|
+
exact,
|
|
6361
|
+
wildcard,
|
|
6362
|
+
...GIT_ENV_PREFIXES.map((prefix) => `${prefix} ${exact}`),
|
|
6363
|
+
...GIT_ENV_PREFIXES.map((prefix) => `${prefix} ${wildcard}`)
|
|
6364
|
+
]));
|
|
5991
6365
|
var leaseInputSchema = discriminatedUnion("operation", [
|
|
5992
6366
|
object({
|
|
5993
6367
|
operation: literal("reserve"),
|
|
@@ -6037,8 +6411,8 @@ function deny(event, message) {
|
|
|
6037
6411
|
event.message = message;
|
|
6038
6412
|
return true;
|
|
6039
6413
|
}
|
|
6040
|
-
function
|
|
6041
|
-
return agent
|
|
6414
|
+
function safeReadonlyShell(agent, resources) {
|
|
6415
|
+
return agent !== undefined && resources.length > 0 && resources.every((resource) => SAFE_SHELL_DURING_ACTIVE_LEASES.has(resource));
|
|
6042
6416
|
}
|
|
6043
6417
|
function enforceFileLeasePermission(event, config, manager) {
|
|
6044
6418
|
const role = roleOf(config, event.agent);
|
|
@@ -6061,7 +6435,7 @@ function enforceFileLeasePermission(event, config, manager) {
|
|
|
6061
6435
|
if (role === "coordinator" || role === "writer") {
|
|
6062
6436
|
return deny(event, `Agent ${event.agent} cannot use shell while file leases enforce structured mutations`);
|
|
6063
6437
|
}
|
|
6064
|
-
if (manager.hasActiveLeases() && !(role === "readonly" &&
|
|
6438
|
+
if (manager.hasActiveLeases() && !(role === "readonly" && safeReadonlyShell(event.agent, event.resources))) {
|
|
6065
6439
|
return deny(event, "Shell commands are paused until all active writer file leases are released");
|
|
6066
6440
|
}
|
|
6067
6441
|
return false;
|
|
@@ -6085,8 +6459,14 @@ function requireRole(config, agentID, allowed) {
|
|
|
6085
6459
|
}
|
|
6086
6460
|
return role;
|
|
6087
6461
|
}
|
|
6088
|
-
async function installFileLeaseRuntime(ctx, config) {
|
|
6089
|
-
const
|
|
6462
|
+
async function installFileLeaseRuntime(ctx, config, options = {}) {
|
|
6463
|
+
const caseInsensitive = options.caseInsensitive ?? resolveCaseInsensitiveFilesystem(options.env ?? process.env, options.platform ?? process.platform);
|
|
6464
|
+
const manager = new FileLeaseManager({
|
|
6465
|
+
projectRoot: config.projectRoot,
|
|
6466
|
+
caseInsensitive,
|
|
6467
|
+
reservationTtlMs: config.lease.reservationTtlMs,
|
|
6468
|
+
activeTtlMs: config.lease.activeTtlMs
|
|
6469
|
+
});
|
|
6090
6470
|
const toolTransform = await ctx.tool.transform((tools) => {
|
|
6091
6471
|
tools.namespace({
|
|
6092
6472
|
name: "gvozd",
|
|
@@ -6176,43 +6556,103 @@ async function installFileLeaseRuntime(ctx, config) {
|
|
|
6176
6556
|
};
|
|
6177
6557
|
}
|
|
6178
6558
|
|
|
6179
|
-
// src/
|
|
6180
|
-
function
|
|
6181
|
-
|
|
6182
|
-
}
|
|
6183
|
-
|
|
6184
|
-
|
|
6185
|
-
|
|
6186
|
-
|
|
6187
|
-
|
|
6188
|
-
|
|
6189
|
-
|
|
6190
|
-
|
|
6191
|
-
|
|
6559
|
+
// src/runtime-events.ts
|
|
6560
|
+
function redactDiagnostic(error) {
|
|
6561
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
6562
|
+
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);
|
|
6563
|
+
}
|
|
6564
|
+
async function disposeResources(resources, diagnostic) {
|
|
6565
|
+
const failures = [];
|
|
6566
|
+
for (const resource of [...resources].reverse()) {
|
|
6567
|
+
try {
|
|
6568
|
+
await resource.dispose();
|
|
6569
|
+
} catch (error) {
|
|
6570
|
+
failures.push(error);
|
|
6571
|
+
diagnostic?.(`agent-gvozd cleanup failed: ${redactDiagnostic(error)}`);
|
|
6572
|
+
}
|
|
6192
6573
|
}
|
|
6193
|
-
|
|
6574
|
+
if (failures.length > 0)
|
|
6575
|
+
throw new AggregateError(failures, "agent-gvozd cleanup failed");
|
|
6194
6576
|
}
|
|
6195
|
-
|
|
6196
|
-
|
|
6197
|
-
|
|
6198
|
-
|
|
6199
|
-
|
|
6200
|
-
|
|
6201
|
-
|
|
6577
|
+
var DEFAULT_RETRY_BASE_DELAY_MS = 25;
|
|
6578
|
+
var DEFAULT_RETRY_MAX_DELAY_MS = 30000;
|
|
6579
|
+
function defaultDelay(milliseconds, signal) {
|
|
6580
|
+
if (signal.aborted)
|
|
6581
|
+
return Promise.resolve();
|
|
6582
|
+
return new Promise((resolve) => {
|
|
6583
|
+
const timeout = setTimeout(finish, milliseconds);
|
|
6584
|
+
function finish() {
|
|
6585
|
+
clearTimeout(timeout);
|
|
6586
|
+
signal.removeEventListener("abort", finish);
|
|
6587
|
+
resolve();
|
|
6588
|
+
}
|
|
6589
|
+
signal.addEventListener("abort", finish, { once: true });
|
|
6202
6590
|
});
|
|
6203
6591
|
}
|
|
6204
|
-
function
|
|
6205
|
-
|
|
6206
|
-
|
|
6207
|
-
|
|
6208
|
-
|
|
6209
|
-
|
|
6210
|
-
|
|
6211
|
-
|
|
6212
|
-
|
|
6213
|
-
|
|
6592
|
+
function retryDelay(attempt, baseDelayMs, maxDelayMs, random) {
|
|
6593
|
+
const capped = Math.min(baseDelayMs * 2 ** attempt, maxDelayMs);
|
|
6594
|
+
const boundedRandom = Math.max(0, Math.min(1, random()));
|
|
6595
|
+
return Math.round(capped * (0.5 + boundedRandom * 0.5));
|
|
6596
|
+
}
|
|
6597
|
+
async function waitForRetry(delay, milliseconds, signal) {
|
|
6598
|
+
if (signal.aborted)
|
|
6599
|
+
return false;
|
|
6600
|
+
let onAbort;
|
|
6601
|
+
const aborted = new Promise((resolve) => {
|
|
6602
|
+
onAbort = () => resolve();
|
|
6603
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
6604
|
+
if (signal.aborted)
|
|
6605
|
+
onAbort();
|
|
6214
6606
|
});
|
|
6607
|
+
try {
|
|
6608
|
+
await Promise.race([delay(milliseconds, signal), aborted]);
|
|
6609
|
+
} finally {
|
|
6610
|
+
signal.removeEventListener("abort", onAbort);
|
|
6611
|
+
}
|
|
6612
|
+
return !signal.aborted;
|
|
6613
|
+
}
|
|
6614
|
+
function startRuntimeEventLoop(input) {
|
|
6615
|
+
const controller = new AbortController;
|
|
6616
|
+
const delay = input.delay ?? defaultDelay;
|
|
6617
|
+
const random = input.random ?? Math.random;
|
|
6618
|
+
const baseDelayMs = Math.max(1, input.retryBaseDelayMs ?? DEFAULT_RETRY_BASE_DELAY_MS);
|
|
6619
|
+
const maxDelayMs = Math.max(baseDelayMs, input.retryMaxDelayMs ?? DEFAULT_RETRY_MAX_DELAY_MS);
|
|
6620
|
+
const done = (async () => {
|
|
6621
|
+
let retryAttempt = 0;
|
|
6622
|
+
while (!controller.signal.aborted) {
|
|
6623
|
+
try {
|
|
6624
|
+
for await (const event of input.subscribe(controller.signal)) {
|
|
6625
|
+
retryAttempt = 0;
|
|
6626
|
+
try {
|
|
6627
|
+
await input.handle(event);
|
|
6628
|
+
} catch (error) {
|
|
6629
|
+
input.diagnostic(`agent-gvozd runtime refresh failed: ${redactDiagnostic(error)}`);
|
|
6630
|
+
}
|
|
6631
|
+
}
|
|
6632
|
+
if (controller.signal.aborted)
|
|
6633
|
+
return;
|
|
6634
|
+
input.diagnostic("agent-gvozd event subscription ended unexpectedly");
|
|
6635
|
+
} catch (error) {
|
|
6636
|
+
if (controller.signal.aborted)
|
|
6637
|
+
return;
|
|
6638
|
+
input.diagnostic(`agent-gvozd event subscription failed: ${redactDiagnostic(error)}`);
|
|
6639
|
+
}
|
|
6640
|
+
const milliseconds = retryDelay(retryAttempt, baseDelayMs, maxDelayMs, random);
|
|
6641
|
+
retryAttempt += 1;
|
|
6642
|
+
if (!await waitForRetry(delay, milliseconds, controller.signal))
|
|
6643
|
+
return;
|
|
6644
|
+
}
|
|
6645
|
+
})();
|
|
6646
|
+
return {
|
|
6647
|
+
done,
|
|
6648
|
+
async dispose() {
|
|
6649
|
+
controller.abort();
|
|
6650
|
+
await done;
|
|
6651
|
+
}
|
|
6652
|
+
};
|
|
6215
6653
|
}
|
|
6654
|
+
|
|
6655
|
+
// src/index.ts
|
|
6216
6656
|
function selectModel(models, available) {
|
|
6217
6657
|
const configured = models.map((model) => Model.Ref.parse(model));
|
|
6218
6658
|
const selected = configured.find((candidate) => {
|
|
@@ -6232,11 +6672,14 @@ function applyAgentConfiguration(agents, config, models, mcpServers) {
|
|
|
6232
6672
|
if (!agents.get(id))
|
|
6233
6673
|
continue;
|
|
6234
6674
|
agents.update(id, (agent) => {
|
|
6675
|
+
if (configured.promptContent === undefined) {
|
|
6676
|
+
throw new Error(`Agent ${id} is missing its immutable prompt snapshot (${configured.prompt})`);
|
|
6677
|
+
}
|
|
6235
6678
|
agent.description = configured.description;
|
|
6236
6679
|
agent.mode = configured.mode;
|
|
6237
|
-
agent.system =
|
|
6680
|
+
agent.system = configured.promptContent.trim();
|
|
6238
6681
|
agent.model = selectModel(configured.models, models);
|
|
6239
|
-
agent.permissions
|
|
6682
|
+
agent.permissions = buildAgentPermissions(configured, mcpServers);
|
|
6240
6683
|
});
|
|
6241
6684
|
}
|
|
6242
6685
|
if (agents.get(config.defaultAgent))
|
|
@@ -6245,74 +6688,82 @@ function applyAgentConfiguration(agents, config, models, mcpServers) {
|
|
|
6245
6688
|
var src_default = Plugin.define({
|
|
6246
6689
|
id: "agent-gvozd",
|
|
6247
6690
|
async setup(ctx) {
|
|
6248
|
-
const
|
|
6691
|
+
const caseInsensitive = resolveCaseInsensitiveFilesystem(process.env);
|
|
6692
|
+
const config = loadConfig(ctx.location.project.directory, { env: process.env });
|
|
6249
6693
|
const mcp = await ctx.mcp.list();
|
|
6250
6694
|
let mcpServers = mcp.data.map((server) => server.name);
|
|
6251
6695
|
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
|
-
|
|
6696
|
+
const diagnostic = (message) => console.error(message);
|
|
6697
|
+
const resources = [];
|
|
6698
|
+
try {
|
|
6699
|
+
const fileLeases = await installFileLeaseRuntime(ctx, config, { caseInsensitive });
|
|
6700
|
+
resources.push(fileLeases);
|
|
6701
|
+
const agentTransform = await ctx.agent.transform((agents) => {
|
|
6702
|
+
applyAgentConfiguration(agents, config, models.data, mcpServers);
|
|
6703
|
+
});
|
|
6704
|
+
resources.push(agentTransform);
|
|
6705
|
+
const permissionHook = await ctx.permission.hook("evaluate", async (event) => {
|
|
6706
|
+
if (fileLeases.enforcePermission(event))
|
|
6707
|
+
return;
|
|
6708
|
+
if (!event.agent)
|
|
6709
|
+
return;
|
|
6710
|
+
const configured = config.agents[event.agent];
|
|
6711
|
+
if (!configured || configured.disabled)
|
|
6712
|
+
return;
|
|
6713
|
+
if (event.action === "skill") {
|
|
6714
|
+
const allowed = new Set(configured.skills);
|
|
6715
|
+
if (event.resources.some((resource) => !allowed.has(resource))) {
|
|
6716
|
+
event.effect = "deny";
|
|
6717
|
+
event.message = `Agent ${event.agent} cannot use this skill`;
|
|
6718
|
+
}
|
|
6719
|
+
return;
|
|
6269
6720
|
}
|
|
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") {
|
|
6721
|
+
let matchingServers = matchingMcpServers(event.action, mcpServers);
|
|
6722
|
+
if (matchingServers.length === 0 && event.action.includes("_")) {
|
|
6295
6723
|
const mcp = await ctx.mcp.list();
|
|
6296
|
-
|
|
6297
|
-
|
|
6298
|
-
|
|
6299
|
-
|
|
6300
|
-
|
|
6724
|
+
mcpServers = mcp.data.map((server) => server.name);
|
|
6725
|
+
matchingServers = matchingMcpServers(event.action, mcpServers);
|
|
6726
|
+
}
|
|
6727
|
+
if (matchingServers.length === 0)
|
|
6728
|
+
return;
|
|
6729
|
+
if (matchingServers.length > 1) {
|
|
6730
|
+
event.effect = "deny";
|
|
6731
|
+
event.message = `MCP action has an ambiguous server prefix: ${matchingServers.join(", ")}`;
|
|
6732
|
+
return;
|
|
6301
6733
|
}
|
|
6302
|
-
if (
|
|
6303
|
-
|
|
6304
|
-
|
|
6734
|
+
if (!matchingServers.some((server) => configured.mcp.includes(server)) && !explicitMcpAccess(configured, event.action, event.resources)) {
|
|
6735
|
+
event.effect = "deny";
|
|
6736
|
+
event.message = `Agent ${event.agent} cannot use this MCP server`;
|
|
6305
6737
|
}
|
|
6306
|
-
}
|
|
6307
|
-
|
|
6308
|
-
|
|
6309
|
-
|
|
6310
|
-
|
|
6311
|
-
|
|
6312
|
-
|
|
6313
|
-
|
|
6314
|
-
|
|
6315
|
-
|
|
6738
|
+
});
|
|
6739
|
+
resources.push(permissionHook);
|
|
6740
|
+
const eventLoop = startRuntimeEventLoop({
|
|
6741
|
+
subscribe: (signal) => ctx.event.subscribe({ signal }),
|
|
6742
|
+
diagnostic,
|
|
6743
|
+
async handle(event) {
|
|
6744
|
+
fileLeases.handleEvent(event);
|
|
6745
|
+
if (event.type === "mcp.status.changed") {
|
|
6746
|
+
const mcp = await ctx.mcp.list();
|
|
6747
|
+
const next = mcp.data.map((server) => server.name);
|
|
6748
|
+
if (next.length === mcpServers.length && next.every((server, index) => server === mcpServers[index]))
|
|
6749
|
+
return;
|
|
6750
|
+
mcpServers = next;
|
|
6751
|
+
await ctx.agent.reload();
|
|
6752
|
+
}
|
|
6753
|
+
if (event.type === "catalog.updated") {
|
|
6754
|
+
models = await ctx.catalog.model.list();
|
|
6755
|
+
await ctx.agent.reload();
|
|
6756
|
+
}
|
|
6757
|
+
}
|
|
6758
|
+
});
|
|
6759
|
+
resources.push(eventLoop);
|
|
6760
|
+
return async () => disposeResources(resources, diagnostic);
|
|
6761
|
+
} catch (error) {
|
|
6762
|
+
try {
|
|
6763
|
+
await disposeResources(resources, diagnostic);
|
|
6764
|
+
} catch {}
|
|
6765
|
+
throw error;
|
|
6766
|
+
}
|
|
6316
6767
|
}
|
|
6317
6768
|
});
|
|
6318
6769
|
export {
|