@kevin5251984/guild 0.2.18 → 0.2.20
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/package.json +2 -2
- package/src/catalog/subagents.ts +2 -1
- package/src/chat-parts.ts +6 -1
- package/src/cli.ts +2 -5
- package/src/compact.ts +27 -0
- package/src/db.ts +236 -68
- package/src/generate.ts +59 -28
- package/src/handlers.ts +355 -84
- package/src/harness.ts +39 -10
- package/src/host-browse.ts +149 -4
- package/src/image-gen.ts +2 -1
- package/src/llm.ts +435 -71
- package/src/mcp.ts +32 -5
- package/src/memory.ts +69 -0
- package/src/mention.ts +189 -14
- package/src/oauth.ts +204 -27
- package/src/opencode-free.ts +248 -0
- package/src/public/buddy.js +432 -0
- package/src/public/chat.css +174 -72
- package/src/public/chat.html +354 -69
- package/src/public/i18n.js +44 -6
- package/src/public/index.html +1 -46
- package/src/public/mobile.css +597 -0
- package/src/public/mobile.html +954 -0
- package/src/public/settings.html +239 -35
- package/src/public/skills-add.html +8 -0
- package/src/public/studio.html +171 -117
- package/src/public/style.css +220 -26
- package/src/public/subagents-add.html +8 -0
- package/src/reasoning-catalog.ts +346 -0
- package/src/router.ts +184 -13
- package/src/store.ts +170 -5
- package/src/subagent.ts +239 -7
- package/src/tools.ts +132 -21
- package/src/trajectory.ts +72 -9
- package/src/usage.ts +10 -0
- package/src/version.ts +25 -0
- package/vendor/protocol/src/index.ts +27 -2
package/src/harness.ts
CHANGED
|
@@ -156,16 +156,25 @@ export function gateTool(
|
|
|
156
156
|
if (sandbox === "full_access") return null;
|
|
157
157
|
|
|
158
158
|
if (sandbox === "read_only") {
|
|
159
|
-
if (
|
|
159
|
+
if (
|
|
160
|
+
name === "read" ||
|
|
161
|
+
name === "list" ||
|
|
162
|
+
name === "skill" ||
|
|
163
|
+
name === "spawn" ||
|
|
164
|
+
name === "read_spawn"
|
|
165
|
+
) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
160
168
|
return {
|
|
161
169
|
text: `sandbox=read_only refused ${name}`,
|
|
162
170
|
isError: true,
|
|
163
171
|
};
|
|
164
172
|
}
|
|
165
173
|
|
|
174
|
+
// No explicit workspace means the guild checkout, never all of $HOME.
|
|
166
175
|
const workspace = input.workspace?.trim()
|
|
167
176
|
? resolveToolPath(input.workspace)
|
|
168
|
-
:
|
|
177
|
+
: defaultWorkspace();
|
|
169
178
|
|
|
170
179
|
if (name.startsWith("mcp__")) {
|
|
171
180
|
return {
|
|
@@ -174,7 +183,24 @@ export function gateTool(
|
|
|
174
183
|
};
|
|
175
184
|
}
|
|
176
185
|
|
|
177
|
-
if (name === "read" || name === "list"
|
|
186
|
+
if (name === "read" || name === "list") {
|
|
187
|
+
const raw = typeof args.path === "string" ? args.path : "";
|
|
188
|
+
if (name === "list" && !raw.trim()) return null;
|
|
189
|
+
const target = resolveToolPath(raw, workspace);
|
|
190
|
+
if (!pathInsideWorkspace(target, workspace)) {
|
|
191
|
+
return {
|
|
192
|
+
text: `sandbox=workspace_write refused ${name} outside workspace: ${target}`,
|
|
193
|
+
isError: true,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
return null;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (
|
|
200
|
+
name === "skill" ||
|
|
201
|
+
name === "spawn" ||
|
|
202
|
+
name === "read_spawn"
|
|
203
|
+
) {
|
|
178
204
|
return null;
|
|
179
205
|
}
|
|
180
206
|
|
|
@@ -210,7 +236,11 @@ export function gateTool(
|
|
|
210
236
|
};
|
|
211
237
|
}
|
|
212
238
|
|
|
213
|
-
|
|
239
|
+
// browser (CDP into the user's Chrome profile) and anything unnamed: refuse.
|
|
240
|
+
return {
|
|
241
|
+
text: `sandbox=workspace_write refused ${name}; use full_access`,
|
|
242
|
+
isError: true,
|
|
243
|
+
};
|
|
214
244
|
}
|
|
215
245
|
|
|
216
246
|
export type LoopCall = {
|
|
@@ -296,18 +326,17 @@ export async function runAgentLoop(input: {
|
|
|
296
326
|
if (traces.length) return { text: emptyAfterTools, traces, thinking };
|
|
297
327
|
return input.nullIfNoTraces ? null : { text: emptyAfterTools, traces, thinking };
|
|
298
328
|
}
|
|
299
|
-
const outcomes
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
await executeToolTraced(
|
|
329
|
+
const outcomes = await Promise.all(
|
|
330
|
+
asked.calls.map((call) =>
|
|
331
|
+
executeToolTraced(
|
|
303
332
|
call.name,
|
|
304
333
|
call.args,
|
|
305
334
|
input.toolCtx,
|
|
306
335
|
traces,
|
|
307
336
|
thinking,
|
|
308
337
|
),
|
|
309
|
-
)
|
|
310
|
-
|
|
338
|
+
),
|
|
339
|
+
);
|
|
311
340
|
input.onTools?.(asked.calls, outcomes);
|
|
312
341
|
}
|
|
313
342
|
}
|
package/src/host-browse.ts
CHANGED
|
@@ -3,10 +3,11 @@ import {
|
|
|
3
3
|
existsSync,
|
|
4
4
|
readdirSync,
|
|
5
5
|
readFileSync,
|
|
6
|
+
realpathSync,
|
|
6
7
|
statSync,
|
|
7
8
|
} from "node:fs";
|
|
8
9
|
import { homedir } from "node:os";
|
|
9
|
-
import { dirname, join, resolve } from "node:path";
|
|
10
|
+
import { basename, dirname, isAbsolute, join, resolve, sep } from "node:path";
|
|
10
11
|
import { promisify } from "node:util";
|
|
11
12
|
import { StoreError } from "./store.ts";
|
|
12
13
|
|
|
@@ -31,6 +32,117 @@ function resolveUserPath(input: string): string {
|
|
|
31
32
|
return resolve(HOME, trimmed);
|
|
32
33
|
}
|
|
33
34
|
|
|
35
|
+
/** Guild data dirs: `~/.guild` plus an explicit `GUILD_HOME` if set. */
|
|
36
|
+
function guildHomes(): string[] {
|
|
37
|
+
const homes = [join(HOME, ".guild")];
|
|
38
|
+
const extra = process.env.GUILD_HOME?.trim();
|
|
39
|
+
if (extra) homes.push(resolveUserPath(extra));
|
|
40
|
+
return homes;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const SSH_DIR = join(HOME, ".ssh");
|
|
44
|
+
/** Files whose *contents* are credentials, wherever the guild home lives. */
|
|
45
|
+
const SECRET_GUILD_FILES = new Set(["oauth.json", "models.json", "mcp.json"]);
|
|
46
|
+
/** Credential dotfiles that live directly in `$HOME`. */
|
|
47
|
+
const SECRET_HOME_FILES = new Set([
|
|
48
|
+
".claude.json",
|
|
49
|
+
".netrc",
|
|
50
|
+
"_netrc",
|
|
51
|
+
".npmrc",
|
|
52
|
+
".yarnrc.yml",
|
|
53
|
+
".git-credentials",
|
|
54
|
+
".env",
|
|
55
|
+
".env.local",
|
|
56
|
+
".env.production",
|
|
57
|
+
".pgpass",
|
|
58
|
+
".pypirc",
|
|
59
|
+
".my.cnf",
|
|
60
|
+
"credentials.json",
|
|
61
|
+
]);
|
|
62
|
+
/** `$HOME` folders that are credential stores: the folder and everything below. */
|
|
63
|
+
const SECRET_HOME_DIRS = [
|
|
64
|
+
".aws",
|
|
65
|
+
".docker",
|
|
66
|
+
".gnupg",
|
|
67
|
+
".claude",
|
|
68
|
+
".codex",
|
|
69
|
+
".cursor",
|
|
70
|
+
".kube",
|
|
71
|
+
".azure",
|
|
72
|
+
join(".config", "gcloud"),
|
|
73
|
+
join(".config", "gh"),
|
|
74
|
+
join("Library", "Keychains"),
|
|
75
|
+
].map((relative) => join(HOME, relative));
|
|
76
|
+
const SSH_KEY_NAME = /^(id_rsa|id_dsa|id_ecdsa|id_ed25519|id_xmss)$/i;
|
|
77
|
+
const SECRET_SUFFIXES = [
|
|
78
|
+
".pem",
|
|
79
|
+
".p12",
|
|
80
|
+
".pfx",
|
|
81
|
+
".key",
|
|
82
|
+
".p8",
|
|
83
|
+
".jks",
|
|
84
|
+
".keystore",
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
function under(path: string, dir: string): boolean {
|
|
88
|
+
const prefix = dir.endsWith(sep) ? dir : `${dir}${sep}`;
|
|
89
|
+
return path === dir || path.startsWith(prefix);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Longest existing ancestor resolved through symlinks, tail re-joined. */
|
|
93
|
+
function canonicalPath(target: string): string {
|
|
94
|
+
let abs = resolve(target);
|
|
95
|
+
const tail: string[] = [];
|
|
96
|
+
for (;;) {
|
|
97
|
+
try {
|
|
98
|
+
const real = realpathSync(abs);
|
|
99
|
+
return tail.length ? resolve(real, ...tail) : real;
|
|
100
|
+
} catch {
|
|
101
|
+
const parent = dirname(abs);
|
|
102
|
+
if (parent === abs) return tail.length ? resolve(abs, ...tail) : abs;
|
|
103
|
+
tail.unshift(basename(abs));
|
|
104
|
+
abs = parent;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function isSecretPath(abs: string): boolean {
|
|
110
|
+
const name = basename(abs);
|
|
111
|
+
const lower = name.toLowerCase();
|
|
112
|
+
const publicKey = lower.endsWith(".pub");
|
|
113
|
+
for (const home of guildHomes()) {
|
|
114
|
+
if (under(abs, join(home, "browser-profile"))) return true;
|
|
115
|
+
if (under(abs, home) && SECRET_GUILD_FILES.has(lower)) return true;
|
|
116
|
+
}
|
|
117
|
+
if (dirname(abs) === HOME && SECRET_HOME_FILES.has(lower)) return true;
|
|
118
|
+
if (name === ".env" || name.startsWith(".env.")) return true;
|
|
119
|
+
if (lower === "credentials.json" || lower === "service-account.json") {
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
if (SECRET_HOME_DIRS.some((dir) => under(abs, dir))) return true;
|
|
123
|
+
if (under(abs, SSH_DIR) && abs !== SSH_DIR && !publicKey) return true;
|
|
124
|
+
if (SSH_KEY_NAME.test(name)) return true;
|
|
125
|
+
if (!publicKey && SECRET_SUFFIXES.some((suffix) => lower.endsWith(suffix))) {
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The attach picker browses `$HOME` on purpose, so /host/* is not confined to a
|
|
133
|
+
* workspace. Secrets still have to stay shut: OAuth tokens, model keys, the MCP
|
|
134
|
+
* store, the cloned browser profile, private keys, and the usual credential
|
|
135
|
+
* dotfiles / folders (`.aws`, `.claude`, `.codex`, `.npmrc`, `.netrc`, …).
|
|
136
|
+
* This is a denylist over the picker, not a chroot: non-secret `$HOME` and
|
|
137
|
+
* system files such as `/etc/passwd` stay readable.
|
|
138
|
+
*/
|
|
139
|
+
export function assertHostPathAllowed(target: string): void {
|
|
140
|
+
const abs = isAbsolute(target) ? resolve(target) : resolveUserPath(target);
|
|
141
|
+
if (isSecretPath(canonicalPath(abs))) {
|
|
142
|
+
throw new StoreError(403, "host path refused");
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
34
146
|
function parentOf(path: string): string | null {
|
|
35
147
|
const parent = dirname(path);
|
|
36
148
|
if (parent === path) return null;
|
|
@@ -52,6 +164,7 @@ export function hostList(rawPath: string): {
|
|
|
52
164
|
} {
|
|
53
165
|
try {
|
|
54
166
|
const target = resolveUserPath(rawPath);
|
|
167
|
+
assertHostPathAllowed(target);
|
|
55
168
|
const st = statSync(target);
|
|
56
169
|
if (!st.isDirectory()) throw new StoreError(400, "not a directory");
|
|
57
170
|
const entries = readdirSync(target, { withFileTypes: true })
|
|
@@ -94,6 +207,7 @@ export function hostRead(rawPath: string): {
|
|
|
94
207
|
} {
|
|
95
208
|
try {
|
|
96
209
|
const target = resolveUserPath(rawPath);
|
|
210
|
+
assertHostPathAllowed(target);
|
|
97
211
|
const st = statSync(target);
|
|
98
212
|
if (!st.isFile()) throw new StoreError(400, "not a file");
|
|
99
213
|
const raw = readFileSync(target);
|
|
@@ -146,6 +260,7 @@ function walkTree(
|
|
|
146
260
|
export function hostTree(rawPath: string): { path: string; text: string } {
|
|
147
261
|
try {
|
|
148
262
|
const target = resolveUserPath(rawPath);
|
|
263
|
+
assertHostPathAllowed(target);
|
|
149
264
|
const st = statSync(target);
|
|
150
265
|
if (!st.isDirectory()) throw new StoreError(400, "not a directory");
|
|
151
266
|
const lines = [target];
|
|
@@ -158,6 +273,25 @@ export function hostTree(rawPath: string): { path: string; text: string } {
|
|
|
158
273
|
}
|
|
159
274
|
}
|
|
160
275
|
|
|
276
|
+
/**
|
|
277
|
+
* Git runs inside the user's tree: ignore their global/system config (hooks,
|
|
278
|
+
* fsmonitor, credential helpers) and never prompt on a TTY-less daemon.
|
|
279
|
+
*/
|
|
280
|
+
const GIT_GUARD = [
|
|
281
|
+
"-c",
|
|
282
|
+
"core.fsmonitor=false",
|
|
283
|
+
"-c",
|
|
284
|
+
"core.untrackedCache=false",
|
|
285
|
+
"--no-optional-locks",
|
|
286
|
+
] as const;
|
|
287
|
+
|
|
288
|
+
const GIT_ENV: NodeJS.ProcessEnv = {
|
|
289
|
+
...process.env,
|
|
290
|
+
GIT_CONFIG_GLOBAL: "/dev/null",
|
|
291
|
+
GIT_CONFIG_NOSYSTEM: "1",
|
|
292
|
+
GIT_TERMINAL_PROMPT: "0",
|
|
293
|
+
};
|
|
294
|
+
|
|
161
295
|
function findGitRoot(start: string): string | null {
|
|
162
296
|
let dir = start;
|
|
163
297
|
for (let i = 0; i < 12; i += 1) {
|
|
@@ -176,14 +310,25 @@ export async function hostGit(rawPath: string): Promise<{
|
|
|
176
310
|
}> {
|
|
177
311
|
try {
|
|
178
312
|
const start = resolveUserPath(rawPath);
|
|
313
|
+
assertHostPathAllowed(start);
|
|
179
314
|
const base = statSync(start).isDirectory() ? start : dirname(start);
|
|
180
315
|
const root = findGitRoot(base);
|
|
181
316
|
if (!root) throw new StoreError(404, "not a git repository");
|
|
182
|
-
|
|
183
|
-
const
|
|
317
|
+
assertHostPathAllowed(root);
|
|
318
|
+
const opts = {
|
|
319
|
+
cwd: root,
|
|
320
|
+
timeout: 8_000,
|
|
321
|
+
maxBuffer: GIT_CAP * 2,
|
|
322
|
+
env: GIT_ENV,
|
|
323
|
+
};
|
|
324
|
+
const status = await execFileAsync("git", [...GIT_GUARD, "status", "-sb"], opts);
|
|
184
325
|
let diff = "";
|
|
185
326
|
try {
|
|
186
|
-
const out = await execFileAsync(
|
|
327
|
+
const out = await execFileAsync(
|
|
328
|
+
"git",
|
|
329
|
+
[...GIT_GUARD, "diff", "--stat", "HEAD"],
|
|
330
|
+
opts,
|
|
331
|
+
);
|
|
187
332
|
diff = String(out.stdout || "");
|
|
188
333
|
} catch {
|
|
189
334
|
diff = "";
|
package/src/image-gen.ts
CHANGED
|
@@ -218,7 +218,7 @@ export async function generateImage(input: {
|
|
|
218
218
|
aspectRatio?: string;
|
|
219
219
|
dataDir?: string;
|
|
220
220
|
env?: NodeJS.ProcessEnv;
|
|
221
|
-
}): Promise<{ text: string; isError: boolean }> {
|
|
221
|
+
}): Promise<{ text: string; isError: boolean; publicPath?: string }> {
|
|
222
222
|
const prompt = String(input.prompt || "").trim();
|
|
223
223
|
if (!prompt) return { text: "prompt is required", isError: true };
|
|
224
224
|
const aspect = String(input.aspectRatio || "").trim();
|
|
@@ -252,6 +252,7 @@ export async function generateImage(input: {
|
|
|
252
252
|
`markdown: `,
|
|
253
253
|
].join("\n"),
|
|
254
254
|
isError: false,
|
|
255
|
+
publicPath: saved.publicPath,
|
|
255
256
|
};
|
|
256
257
|
}
|
|
257
258
|
errors.push(`${route.model} @ ${route.url}: ${hit.error}`);
|