@ikarmic/forge-core 5.0.0 → 5.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent/agent.d.ts +8 -0
- package/dist/agent/agent.js +123 -22
- package/dist/agent/background.d.ts +58 -0
- package/dist/agent/background.js +139 -0
- package/dist/agent/enhance-prompt.d.ts +3 -1
- package/dist/agent/enhance-prompt.js +6 -2
- package/dist/agent/goal.d.ts +54 -0
- package/dist/agent/goal.js +65 -0
- package/dist/agents/agent-files.d.ts +43 -0
- package/dist/agents/agent-files.js +109 -0
- package/dist/checkpoints/store.js +5 -1
- package/dist/commands/custom-commands.d.ts +9 -4
- package/dist/commands/custom-commands.js +13 -2
- package/dist/config-file.d.ts +43 -0
- package/dist/config-file.js +249 -5
- package/dist/config-merge.d.ts +13 -0
- package/dist/config-merge.js +67 -0
- package/dist/config-schema.d.ts +340 -0
- package/dist/config-schema.js +126 -0
- package/dist/config.js +2 -1
- package/dist/context/compaction.js +4 -0
- package/dist/context/mask.js +1 -1
- package/dist/custom-providers.d.ts +95 -0
- package/dist/custom-providers.js +120 -0
- package/dist/git/auto-commit.d.ts +2 -2
- package/dist/git/auto-commit.js +5 -4
- package/dist/github/github-tools.d.ts +2 -2
- package/dist/hooks/runner.js +3 -2
- package/dist/import/claude-code.d.ts +39 -0
- package/dist/import/claude-code.js +254 -0
- package/dist/index.d.ts +32 -4
- package/dist/index.js +24 -3
- package/dist/lsp/client.d.ts +5 -0
- package/dist/lsp/client.js +39 -3
- package/dist/mcp/index.d.ts +5 -1
- package/dist/mcp/index.js +3 -1
- package/dist/mcp/manager.d.ts +17 -3
- package/dist/mcp/manager.js +27 -2
- package/dist/mcp/marketplace.d.ts +2 -0
- package/dist/mcp/marketplace.js +10 -4
- package/dist/mcp/mcp-client.d.ts +12 -2
- package/dist/mcp/mcp-client.js +74 -17
- package/dist/mcp/mcp-config.d.ts +2 -0
- package/dist/mcp/mcp-config.js +1 -0
- package/dist/mcp/oauth.d.ts +55 -0
- package/dist/mcp/oauth.js +197 -0
- package/dist/mcp/resource-tools.d.ts +8 -0
- package/dist/mcp/resource-tools.js +56 -0
- package/dist/memory/session-digest.d.ts +12 -0
- package/dist/memory/session-digest.js +47 -0
- package/dist/memory/stale.js +2 -1
- package/dist/model-alias.d.ts +18 -2
- package/dist/model-alias.js +27 -3
- package/dist/model-catalog.js +0 -1
- package/dist/model-list.js +2 -1
- package/dist/model-overrides.d.ts +30 -0
- package/dist/model-overrides.js +48 -0
- package/dist/orchestrator/specialists-registry.js +2 -3
- package/dist/orchestrator/specialists.js +4 -4
- package/dist/policy/ca-certs.d.ts +8 -0
- package/dist/policy/ca-certs.js +30 -0
- package/dist/policy/proxy.d.ts +10 -3
- package/dist/policy/proxy.js +33 -6
- package/dist/pricing.js +4 -0
- package/dist/providers.d.ts +7 -0
- package/dist/providers.js +39 -5
- package/dist/semantic/embedder.d.ts +15 -0
- package/dist/semantic/embedder.js +25 -0
- package/dist/semantic/semantic-index.d.ts +10 -2
- package/dist/semantic/semantic-index.js +16 -7
- package/dist/sessions/session-store.js +2 -1
- package/dist/skills/builtin.d.ts +2 -0
- package/dist/skills/builtin.js +68 -0
- package/dist/skills/index.d.ts +2 -0
- package/dist/skills/index.js +2 -0
- package/dist/skills/loader.d.ts +10 -3
- package/dist/skills/loader.js +20 -7
- package/dist/skills/marketplace.js +2 -1
- package/dist/skills/remote.d.ts +14 -0
- package/dist/skills/remote.js +131 -0
- package/dist/skills/types.d.ts +10 -0
- package/dist/steering/extra-instructions.d.ts +8 -0
- package/dist/steering/extra-instructions.js +104 -0
- package/dist/steering/folder-instructions.d.ts +8 -0
- package/dist/steering/folder-instructions.js +28 -0
- package/dist/steering/select.js +2 -1
- package/dist/timed-fetch.d.ts +12 -0
- package/dist/timed-fetch.js +83 -0
- package/dist/tools/apply-patch.d.ts +52 -0
- package/dist/tools/apply-patch.js +197 -0
- package/dist/tools/ask-user.d.ts +27 -0
- package/dist/tools/ask-user.js +32 -0
- package/dist/tools/browser-tools.d.ts +6 -6
- package/dist/tools/browser-tools.js +3 -13
- package/dist/tools/codebase-search.d.ts +4 -2
- package/dist/tools/codebase-search.js +2 -2
- package/dist/tools/edit-file.js +5 -3
- package/dist/tools/fetch-url.d.ts +7 -0
- package/dist/tools/fetch-url.js +52 -19
- package/dist/tools/find-symbol.d.ts +2 -2
- package/dist/tools/ignore-rules.js +2 -1
- package/dist/tools/index.d.ts +5 -1
- package/dist/tools/index.js +10 -1
- package/dist/tools/memory.d.ts +6 -6
- package/dist/tools/notebook.d.ts +2 -2
- package/dist/tools/permission-rules.d.ts +32 -0
- package/dist/tools/permission-rules.js +182 -0
- package/dist/tools/read-file.d.ts +5 -0
- package/dist/tools/read-file.js +7 -2
- package/dist/tools/read-only.d.ts +1 -1
- package/dist/tools/read-only.js +2 -0
- package/dist/tools/run-command.d.ts +4 -2
- package/dist/tools/run-command.js +12 -8
- package/dist/tools/sandbox-loader.js +3 -13
- package/dist/tools/search-files.d.ts +8 -2
- package/dist/tools/search-files.js +43 -0
- package/dist/tools/shared.d.ts +5 -0
- package/dist/tools/shared.js +30 -2
- package/dist/tools/shell-env.d.ts +7 -0
- package/dist/tools/shell-env.js +9 -0
- package/dist/tools/sub-agent-wait.d.ts +29 -0
- package/dist/tools/sub-agent-wait.js +60 -0
- package/dist/tools/sub-agent.d.ts +50 -4
- package/dist/tools/sub-agent.js +210 -125
- package/dist/tools/text-encoding.d.ts +10 -0
- package/dist/tools/text-encoding.js +40 -0
- package/dist/tools/todo.d.ts +36 -0
- package/dist/tools/todo.js +42 -0
- package/dist/tools/write-file.js +15 -9
- package/dist/tools/write-tools.d.ts +12 -0
- package/dist/tools/write-tools.js +18 -0
- package/dist/types.d.ts +73 -0
- package/dist/worktree/worktree.d.ts +3 -0
- package/dist/worktree/worktree.js +22 -0
- package/package.json +1 -1
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import type { ToolDefinition } from '../types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Ask the person a question with optional choices. The agent only offers this tool when a surface can answer
|
|
5
|
+
* (AgentConfig.askUser) and the run is not autonomous; if it is called anyway, or the run is stopped while the
|
|
6
|
+
* question is open, it returns a result that tells the model to carry on with the safest option.
|
|
7
|
+
*/
|
|
8
|
+
declare const params: z.ZodObject<{
|
|
9
|
+
question: z.ZodString;
|
|
10
|
+
options: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
11
|
+
allowCustom: z.ZodOptional<z.ZodBoolean>;
|
|
12
|
+
}, "strip", z.ZodTypeAny, {
|
|
13
|
+
question: string;
|
|
14
|
+
options?: string[] | undefined;
|
|
15
|
+
allowCustom?: boolean | undefined;
|
|
16
|
+
}, {
|
|
17
|
+
question: string;
|
|
18
|
+
options?: string[] | undefined;
|
|
19
|
+
allowCustom?: boolean | undefined;
|
|
20
|
+
}>;
|
|
21
|
+
export interface AskUserResult {
|
|
22
|
+
answer: string;
|
|
23
|
+
unavailable?: boolean;
|
|
24
|
+
note?: string;
|
|
25
|
+
}
|
|
26
|
+
export declare const askUserTool: ToolDefinition<z.infer<typeof params>, AskUserResult>;
|
|
27
|
+
export {};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
/**
|
|
3
|
+
* Ask the person a question with optional choices. The agent only offers this tool when a surface can answer
|
|
4
|
+
* (AgentConfig.askUser) and the run is not autonomous; if it is called anyway, or the run is stopped while the
|
|
5
|
+
* question is open, it returns a result that tells the model to carry on with the safest option.
|
|
6
|
+
*/
|
|
7
|
+
const params = z.object({
|
|
8
|
+
question: z.string().min(1).max(1000).describe('One clear question.'),
|
|
9
|
+
options: z.array(z.string().min(1).max(200)).max(8).optional().describe('Suggested answers to pick from.'),
|
|
10
|
+
allowCustom: z.boolean().optional().describe('Let the person type their own answer. Default true.'),
|
|
11
|
+
});
|
|
12
|
+
const UNAVAILABLE = {
|
|
13
|
+
answer: '',
|
|
14
|
+
unavailable: true,
|
|
15
|
+
note: 'No user is available. Choose the safest reasonable option and say what you assumed.',
|
|
16
|
+
};
|
|
17
|
+
export const askUserTool = {
|
|
18
|
+
name: 'ask_user',
|
|
19
|
+
description: 'Ask the user a clarifying question when you cannot proceed safely without their answer. Give a few options when you can. Do not ask for permission to run tools; approvals are handled separately.',
|
|
20
|
+
parameters: params,
|
|
21
|
+
noApproval: true,
|
|
22
|
+
execute: async (args, ctx) => {
|
|
23
|
+
if (!ctx.askUser || ctx.auto)
|
|
24
|
+
return UNAVAILABLE;
|
|
25
|
+
const signal = ctx.abortSignal;
|
|
26
|
+
if (signal?.aborted)
|
|
27
|
+
return UNAVAILABLE;
|
|
28
|
+
const answered = ctx.askUser(args, signal).then((answer) => ({ answer }));
|
|
29
|
+
const aborted = new Promise((resolve) => signal?.addEventListener('abort', () => resolve(UNAVAILABLE), { once: true }));
|
|
30
|
+
return Promise.race([answered, aborted]);
|
|
31
|
+
},
|
|
32
|
+
};
|
|
@@ -7,10 +7,10 @@ declare const observeParams: z.ZodObject<{
|
|
|
7
7
|
action: z.ZodEnum<["open", "screenshot", "text", "console", "close"]>;
|
|
8
8
|
url: z.ZodOptional<z.ZodString>;
|
|
9
9
|
}, "strip", z.ZodTypeAny, {
|
|
10
|
-
action: "close" | "
|
|
10
|
+
action: "close" | "text" | "open" | "screenshot" | "console";
|
|
11
11
|
url?: string | undefined;
|
|
12
12
|
}, {
|
|
13
|
-
action: "close" | "
|
|
13
|
+
action: "close" | "text" | "open" | "screenshot" | "console";
|
|
14
14
|
url?: string | undefined;
|
|
15
15
|
}>;
|
|
16
16
|
export type BrowserResult = {
|
|
@@ -55,23 +55,23 @@ declare const actParams: z.ZodEffects<z.ZodObject<{
|
|
|
55
55
|
expression: z.ZodOptional<z.ZodString>;
|
|
56
56
|
}, "strip", z.ZodTypeAny, {
|
|
57
57
|
action: "type" | "click" | "evaluate";
|
|
58
|
-
expression?: string | undefined;
|
|
59
58
|
text?: string | undefined;
|
|
59
|
+
expression?: string | undefined;
|
|
60
60
|
selector?: string | undefined;
|
|
61
61
|
}, {
|
|
62
62
|
action: "type" | "click" | "evaluate";
|
|
63
|
-
expression?: string | undefined;
|
|
64
63
|
text?: string | undefined;
|
|
64
|
+
expression?: string | undefined;
|
|
65
65
|
selector?: string | undefined;
|
|
66
66
|
}>, {
|
|
67
67
|
action: "type" | "click" | "evaluate";
|
|
68
|
-
expression?: string | undefined;
|
|
69
68
|
text?: string | undefined;
|
|
69
|
+
expression?: string | undefined;
|
|
70
70
|
selector?: string | undefined;
|
|
71
71
|
}, {
|
|
72
72
|
action: "type" | "click" | "evaluate";
|
|
73
|
-
expression?: string | undefined;
|
|
74
73
|
text?: string | undefined;
|
|
74
|
+
expression?: string | undefined;
|
|
75
75
|
selector?: string | undefined;
|
|
76
76
|
}>;
|
|
77
77
|
export {};
|
|
@@ -13,6 +13,7 @@ import { lookup } from 'node:dns/promises';
|
|
|
13
13
|
import net from 'node:net';
|
|
14
14
|
import { z } from 'zod';
|
|
15
15
|
import { redact } from './redact.js';
|
|
16
|
+
import { isBlockedAddress } from './fetch-url.js';
|
|
16
17
|
import { checkPolicyViolation } from './policy.js';
|
|
17
18
|
function isLoopback(ip) {
|
|
18
19
|
if (net.isIPv4(ip))
|
|
@@ -20,20 +21,9 @@ function isLoopback(ip) {
|
|
|
20
21
|
const lower = ip.toLowerCase();
|
|
21
22
|
return lower === '::1' || lower === '::ffff:127.0.0.1';
|
|
22
23
|
}
|
|
24
|
+
/** Private, link-local or otherwise internal: the same list as `fetch_url`, minus loopback (a local dev server is the point here). */
|
|
23
25
|
function isPrivateOrLinkLocal(ip) {
|
|
24
|
-
|
|
25
|
-
const [a, b] = ip.split('.').map(Number);
|
|
26
|
-
return a === 10 || (a === 172 && b >= 16 && b <= 31) || (a === 192 && b === 168) || (a === 169 && b === 254) || a === 0;
|
|
27
|
-
}
|
|
28
|
-
if (net.isIPv6(ip)) {
|
|
29
|
-
const lower = ip.toLowerCase();
|
|
30
|
-
const mapped = lower.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/);
|
|
31
|
-
if (mapped)
|
|
32
|
-
return isPrivateOrLinkLocal(mapped[1]);
|
|
33
|
-
const first = parseInt(lower.split(':')[0] ?? '', 16);
|
|
34
|
-
return (!Number.isNaN(first) && first >= 0xfe80 && first <= 0xfebf) || lower.startsWith('fc') || lower.startsWith('fd');
|
|
35
|
-
}
|
|
36
|
-
return false;
|
|
26
|
+
return isBlockedAddress(ip) && !isLoopback(ip);
|
|
37
27
|
}
|
|
38
28
|
/** Throws unless `rawUrl` is an http(s) URL the browser may visit. */
|
|
39
29
|
export async function assertBrowsableUrl(rawUrl, policy) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
-
import { type SemanticHit } from '../semantic/semantic-index.js';
|
|
2
|
+
import { type SemanticHit, type SemanticOptions } from '../semantic/semantic-index.js';
|
|
3
3
|
import type { Embedder } from '../semantic/embedder.js';
|
|
4
4
|
import type { ToolDefinition } from '../types.js';
|
|
5
5
|
declare const params: z.ZodObject<{
|
|
@@ -18,5 +18,7 @@ export interface CodebaseSearchResult {
|
|
|
18
18
|
results: SemanticHit[];
|
|
19
19
|
note: string;
|
|
20
20
|
}
|
|
21
|
-
export declare function createCodebaseSearchTool(embedder: Embedder
|
|
21
|
+
export declare function createCodebaseSearchTool(embedder: Embedder, options?: SemanticOptions & {
|
|
22
|
+
defaultLimit?: number;
|
|
23
|
+
}): ToolDefinition<z.infer<typeof params>, CodebaseSearchResult>;
|
|
22
24
|
export {};
|
|
@@ -4,7 +4,7 @@ const params = z.object({
|
|
|
4
4
|
query: z.string().describe('What you are looking for, in natural language or identifiers (e.g. "where passwords are hashed").'),
|
|
5
5
|
limit: z.number().int().min(1).max(20).optional().describe('Max results. Default 8.'),
|
|
6
6
|
});
|
|
7
|
-
export function createCodebaseSearchTool(embedder) {
|
|
7
|
+
export function createCodebaseSearchTool(embedder, options = {}) {
|
|
8
8
|
return {
|
|
9
9
|
name: 'codebase_search',
|
|
10
10
|
description: 'Semantic search over the codebase: returns the most relevant code chunks (path, line range, snippet) for a description. Use it to find where something is implemented when you do not know the exact name; use find_symbol/search_files when you do.',
|
|
@@ -12,7 +12,7 @@ export function createCodebaseSearchTool(embedder) {
|
|
|
12
12
|
execute: async (args, ctx) => ({
|
|
13
13
|
query: args.query,
|
|
14
14
|
embedder: embedder.id,
|
|
15
|
-
results: await searchSemantic(ctx.cwd, embedder, args.query, args.limit ?? 8),
|
|
15
|
+
results: await searchSemantic(ctx.cwd, embedder, args.query, args.limit ?? options.defaultLimit ?? 8, options),
|
|
16
16
|
note: embedder.id.startsWith('hashing')
|
|
17
17
|
? 'Ranking is by shared vocabulary (offline embedder). Set FORGE_EMBED_PROVIDER=openai|ollama for meaning-based ranking.'
|
|
18
18
|
: 'Ranked by embedding similarity.',
|
package/dist/tools/edit-file.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { resolveAndCheckPath } from './shared.js';
|
|
4
|
+
import { decodeText, encodeText } from './text-encoding.js';
|
|
4
5
|
const params = z.object({
|
|
5
6
|
path: z.string().describe('Path of the file to edit, relative to the working directory or absolute.'),
|
|
6
7
|
old_text: z.string().describe('Exact text to find in the file. Must match exactly (whitespace-sensitive).'),
|
|
@@ -18,7 +19,7 @@ export const editFileTool = {
|
|
|
18
19
|
destructive: false,
|
|
19
20
|
execute: async (args, ctx) => {
|
|
20
21
|
const resolved = resolveAndCheckPath(args.path, ctx);
|
|
21
|
-
const original = await readFile(resolved
|
|
22
|
+
const { text: original, encoding } = await readFile(resolved).then(decodeText).catch((err) => {
|
|
22
23
|
if (err.code === 'ENOENT')
|
|
23
24
|
throw new Error(`File not found: ${args.path}`);
|
|
24
25
|
throw err;
|
|
@@ -37,11 +38,12 @@ export const editFileTool = {
|
|
|
37
38
|
// string 2nd arg to String.prototype.replace still honors special
|
|
38
39
|
// patterns like $&, $1, $$ even though old_text isn't a regex.
|
|
39
40
|
: original.replace(args.old_text, () => args.new_text);
|
|
40
|
-
|
|
41
|
+
const bytes = encodeText(updated, encoding);
|
|
42
|
+
await writeFile(resolved, bytes);
|
|
41
43
|
return {
|
|
42
44
|
path: resolved,
|
|
43
45
|
replacements: replaceAll ? occurrences : 1,
|
|
44
|
-
bytes:
|
|
46
|
+
bytes: bytes.length,
|
|
45
47
|
};
|
|
46
48
|
},
|
|
47
49
|
};
|
|
@@ -37,5 +37,12 @@ export interface FetchUrlResult {
|
|
|
37
37
|
/** True when the response was truncated to fit `max_bytes` or `excerpt` size. */
|
|
38
38
|
truncated: boolean;
|
|
39
39
|
}
|
|
40
|
+
/**
|
|
41
|
+
* SSRF protection: true for an IP literal that is loopback, link-local,
|
|
42
|
+
* or RFC1918 private. `fetch_url` refuses these hosts by default (see
|
|
43
|
+
* {@link assertUrlNotBlocked}) so an agent-supplied URL can't reach
|
|
44
|
+
* loopback services or cloud metadata endpoints (e.g. 169.254.169.254).
|
|
45
|
+
*/
|
|
46
|
+
export declare function isBlockedAddress(ip: string): boolean;
|
|
40
47
|
export declare const fetchUrlTool: ToolDefinition<z.infer<typeof params>, FetchUrlResult>;
|
|
41
48
|
export {};
|
package/dist/tools/fetch-url.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { lookup } from 'node:dns/promises';
|
|
2
2
|
import net from 'node:net';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
+
import { installedFetch } from '../policy/proxy.js';
|
|
4
5
|
import { checkPolicyViolation } from './policy.js';
|
|
5
6
|
const params = z.object({
|
|
6
7
|
url: z
|
|
@@ -37,19 +38,24 @@ const DEFAULT_MAX_BYTES = 200_000;
|
|
|
37
38
|
const DEFAULT_TIMEOUT_MS = 10_000;
|
|
38
39
|
const EXCERPT_MAX_CHARS = 8_000;
|
|
39
40
|
const USER_AGENT = 'ikarmic-forge/0.1 (fetch_url)';
|
|
41
|
+
const MAX_REDIRECTS = 5;
|
|
40
42
|
/**
|
|
41
43
|
* SSRF protection: true for an IP literal that is loopback, link-local,
|
|
42
44
|
* or RFC1918 private. `fetch_url` refuses these hosts by default (see
|
|
43
45
|
* {@link assertUrlNotBlocked}) so an agent-supplied URL can't reach
|
|
44
46
|
* loopback services or cloud metadata endpoints (e.g. 169.254.169.254).
|
|
45
47
|
*/
|
|
46
|
-
function isBlockedAddress(ip) {
|
|
48
|
+
export function isBlockedAddress(ip) {
|
|
47
49
|
if (net.isIPv4(ip)) {
|
|
48
50
|
const parts = ip.split('.').map(Number);
|
|
49
51
|
const a = parts[0];
|
|
50
52
|
const b = parts[1];
|
|
53
|
+
if (a === 0)
|
|
54
|
+
return true; // 0.0.0.0/8: "this host" (0.0.0.0 reaches local services)
|
|
51
55
|
if (a === 127)
|
|
52
56
|
return true; // 127.0.0.0/8: loopback
|
|
57
|
+
if (a === 100 && b >= 64 && b <= 127)
|
|
58
|
+
return true; // 100.64.0.0/10: carrier-grade NAT / cloud-internal
|
|
53
59
|
if (a === 10)
|
|
54
60
|
return true; // 10.0.0.0/8: private
|
|
55
61
|
if (a === 172 && b >= 16 && b <= 31)
|
|
@@ -62,16 +68,25 @@ function isBlockedAddress(ip) {
|
|
|
62
68
|
}
|
|
63
69
|
if (net.isIPv6(ip)) {
|
|
64
70
|
const lower = ip.toLowerCase();
|
|
65
|
-
if (lower === '::1')
|
|
66
|
-
return true; // loopback
|
|
71
|
+
if (lower === '::1' || lower === '::')
|
|
72
|
+
return true; // loopback, unspecified
|
|
67
73
|
// IPv4-mapped IPv6 (::ffff:a.b.c.d): check the embedded v4 address too,
|
|
68
74
|
// otherwise it's a trivial bypass of the v4 checks above.
|
|
69
75
|
const mapped = lower.match(/^::ffff:(\d+\.\d+\.\d+\.\d+)$/);
|
|
70
76
|
if (mapped)
|
|
71
77
|
return isBlockedAddress(mapped[1]);
|
|
78
|
+
// The same mapped address written in hex (::ffff:7f00:1 is 127.0.0.1).
|
|
79
|
+
const mappedHex = lower.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);
|
|
80
|
+
if (mappedHex) {
|
|
81
|
+
const hi = parseInt(mappedHex[1], 16);
|
|
82
|
+
const lo = parseInt(mappedHex[2], 16);
|
|
83
|
+
return isBlockedAddress(`${hi >> 8}.${hi & 255}.${lo >> 8}.${lo & 255}`);
|
|
84
|
+
}
|
|
72
85
|
const firstGroup = parseInt(lower.split(':')[0] ?? '', 16);
|
|
73
86
|
if (!Number.isNaN(firstGroup) && firstGroup >= 0xfe80 && firstGroup <= 0xfebf)
|
|
74
87
|
return true; // fe80::/10: link-local
|
|
88
|
+
if (!Number.isNaN(firstGroup) && firstGroup >= 0xfc00 && firstGroup <= 0xfdff)
|
|
89
|
+
return true; // fc00::/7: unique local (private)
|
|
75
90
|
return false;
|
|
76
91
|
}
|
|
77
92
|
return false;
|
|
@@ -140,21 +155,39 @@ export const fetchUrlTool = {
|
|
|
140
155
|
? setTimeout(() => controller.abort(), timeoutMs)
|
|
141
156
|
: null;
|
|
142
157
|
let response;
|
|
158
|
+
let finalUrl = args.url;
|
|
143
159
|
try {
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
160
|
+
// Redirects are followed here, one hop at a time, so every hop passes the same policy and SSRF checks as the
|
|
161
|
+
// first URL: a public page must not be able to bounce the agent to 169.254.169.254 or localhost.
|
|
162
|
+
// The installed fetch honours the proxy and the extra CA roots even where the host replaced the global one.
|
|
163
|
+
const doFetch = installedFetch() ?? fetch;
|
|
164
|
+
let current = args.url;
|
|
165
|
+
for (let hop = 0;; hop++) {
|
|
166
|
+
response = await doFetch(current, {
|
|
167
|
+
method: 'GET',
|
|
168
|
+
redirect: 'manual',
|
|
169
|
+
signal: controller.signal,
|
|
170
|
+
headers: {
|
|
171
|
+
'user-agent': USER_AGENT,
|
|
172
|
+
accept: 'text/html, application/json, text/plain;q=0.9, */*;q=0.5',
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
const location = response.headers.get('location');
|
|
176
|
+
if (!follow || response.status < 300 || response.status >= 400 || !location)
|
|
177
|
+
break;
|
|
178
|
+
if (hop >= MAX_REDIRECTS)
|
|
179
|
+
throw new Error(`stopped after ${MAX_REDIRECTS} redirects`);
|
|
180
|
+
const next = new URL(location, current).href;
|
|
181
|
+
if (!/^https?:$/i.test(new URL(next).protocol))
|
|
182
|
+
throw new Error(`refusing a redirect to ${next}`);
|
|
183
|
+
const hopViolation = checkPolicyViolation(ctx.policy, { cwd: ctx.cwd, url: next });
|
|
184
|
+
if (hopViolation)
|
|
185
|
+
throw new Error(`[sandbox] ${hopViolation}`);
|
|
186
|
+
await assertUrlNotBlocked(next, ctx.policy);
|
|
187
|
+
await response.body?.cancel().catch(() => undefined);
|
|
188
|
+
current = next;
|
|
189
|
+
}
|
|
190
|
+
finalUrl = current;
|
|
158
191
|
}
|
|
159
192
|
catch (err) {
|
|
160
193
|
if (timeoutHandle)
|
|
@@ -167,7 +200,7 @@ export const fetchUrlTool = {
|
|
|
167
200
|
if (timeoutHandle)
|
|
168
201
|
clearTimeout(timeoutHandle);
|
|
169
202
|
if (response.status >= 400) {
|
|
170
|
-
throw new Error(`fetch_url: HTTP ${response.status} ${response.statusText || ''} (${
|
|
203
|
+
throw new Error(`fetch_url: HTTP ${response.status} ${response.statusText || ''} (${finalUrl})`);
|
|
171
204
|
}
|
|
172
205
|
const contentType = response.headers.get('content-type')?.toLowerCase() ?? 'text/plain';
|
|
173
206
|
const buffer = await readWithCap(response, maxBytes);
|
|
@@ -195,7 +228,7 @@ export const fetchUrlTool = {
|
|
|
195
228
|
}
|
|
196
229
|
const excerpt = body.length > EXCERPT_MAX_CHARS ? body.slice(0, EXCERPT_MAX_CHARS) + '\n\n…(truncated for display; raw response was ' + body.length + ' chars)' : body;
|
|
197
230
|
return {
|
|
198
|
-
url:
|
|
231
|
+
url: finalUrl,
|
|
199
232
|
status: response.status,
|
|
200
233
|
statusText: response.statusText,
|
|
201
234
|
contentType,
|
|
@@ -6,10 +6,10 @@ declare const params: z.ZodObject<{
|
|
|
6
6
|
kind: z.ZodOptional<z.ZodEnum<["function", "class", "interface", "type", "enum", "const", "method"]>>;
|
|
7
7
|
}, "strip", z.ZodTypeAny, {
|
|
8
8
|
name: string;
|
|
9
|
-
kind?: "function" | "
|
|
9
|
+
kind?: "function" | "class" | "interface" | "type" | "enum" | "const" | "method" | undefined;
|
|
10
10
|
}, {
|
|
11
11
|
name: string;
|
|
12
|
-
kind?: "function" | "
|
|
12
|
+
kind?: "function" | "class" | "interface" | "type" | "enum" | "const" | "method" | undefined;
|
|
13
13
|
}>;
|
|
14
14
|
export interface FindSymbolMatch {
|
|
15
15
|
name: string;
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import ignore from 'ignore';
|
|
10
|
+
import { isOutsideRel } from './shared.js';
|
|
10
11
|
/** Files read in every directory, in this order. `.forgeignore` is for paths only the agent should skip. */
|
|
11
12
|
export const IGNORE_FILE_NAMES = ['.gitignore', '.forgeignore'];
|
|
12
13
|
function readIfFile(file) {
|
|
@@ -62,7 +63,7 @@ export class IgnoreRules {
|
|
|
62
63
|
/** Whether `absPath` is ignored. Paths outside the rule root are never ignored here. */
|
|
63
64
|
isIgnored(absPath, isDir) {
|
|
64
65
|
const rel = path.relative(this.top, path.resolve(absPath));
|
|
65
|
-
if (!rel ||
|
|
66
|
+
if (!rel || isOutsideRel(rel))
|
|
66
67
|
return false;
|
|
67
68
|
const parts = rel.split(path.sep);
|
|
68
69
|
let ignored = false;
|
package/dist/tools/index.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { applyPatchTool } from './apply-patch.js';
|
|
1
2
|
import { editFileTool } from './edit-file.js';
|
|
2
3
|
import { fetchUrlTool } from './fetch-url.js';
|
|
3
4
|
import { findReferencesTool } from './find-references.js';
|
|
@@ -10,7 +11,10 @@ import { readFileTool } from './read-file.js';
|
|
|
10
11
|
import { readImageTool } from './read-image.js';
|
|
11
12
|
import { runCommandTool } from './run-command.js';
|
|
12
13
|
import { searchFilesTool } from './search-files.js';
|
|
14
|
+
import { askUserTool } from './ask-user.js';
|
|
13
15
|
import { subAgentTool } from './sub-agent.js';
|
|
16
|
+
import { subAgentWaitTool } from './sub-agent-wait.js';
|
|
17
|
+
import { todoReadTool, todoWriteTool } from './todo.js';
|
|
14
18
|
import { writeFileTool } from './write-file.js';
|
|
15
19
|
import type { ToolDefinition } from '../types.js';
|
|
16
20
|
/**
|
|
@@ -26,7 +30,7 @@ type AnyToolDefinition = ToolDefinition<any, any>;
|
|
|
26
30
|
* intentional: the model sees this order in its tool list.
|
|
27
31
|
*/
|
|
28
32
|
export declare const BUILTIN_TOOLS: readonly AnyToolDefinition[];
|
|
29
|
-
export { editFileTool, editNotebookTool, fetchUrlTool, findReferencesTool, findSymbolTool, listFilesTool, listSymbolsTool, memoryForgetTool, memoryReadTool, memoryWriteTool, readFileTool, readImageTool, runCommandTool, searchFilesTool, subAgentTool, writeFileTool, };
|
|
33
|
+
export { applyPatchTool, askUserTool, editFileTool, editNotebookTool, fetchUrlTool, findReferencesTool, findSymbolTool, listFilesTool, listSymbolsTool, memoryForgetTool, memoryReadTool, memoryWriteTool, readFileTool, readImageTool, runCommandTool, searchFilesTool, subAgentTool, subAgentWaitTool, todoReadTool, todoWriteTool, writeFileTool, };
|
|
30
34
|
export type { ToolContext, ToolDefinition, ToolPolicy, ToolSandbox } from '../types.js';
|
|
31
35
|
export { checkPolicyViolation, globToRegex, mergeToolSandbox, normalizePathPattern, pathMatchesAny } from './policy.js';
|
|
32
36
|
export { redact, countRedactions, buildRules } from './redact.js';
|
package/dist/tools/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { applyPatchTool } from './apply-patch.js';
|
|
1
2
|
import { editFileTool } from './edit-file.js';
|
|
2
3
|
import { fetchUrlTool } from './fetch-url.js';
|
|
3
4
|
import { findReferencesTool } from './find-references.js';
|
|
@@ -10,7 +11,10 @@ import { readFileTool } from './read-file.js';
|
|
|
10
11
|
import { readImageTool } from './read-image.js';
|
|
11
12
|
import { runCommandTool } from './run-command.js';
|
|
12
13
|
import { searchFilesTool } from './search-files.js';
|
|
14
|
+
import { askUserTool } from './ask-user.js';
|
|
13
15
|
import { subAgentTool } from './sub-agent.js';
|
|
16
|
+
import { subAgentWaitTool } from './sub-agent-wait.js';
|
|
17
|
+
import { todoReadTool, todoWriteTool } from './todo.js';
|
|
14
18
|
import { writeFileTool } from './write-file.js';
|
|
15
19
|
/**
|
|
16
20
|
* The built-in tool set that ships with Forge. These cover the file system,
|
|
@@ -28,6 +32,7 @@ export const BUILTIN_TOOLS = [
|
|
|
28
32
|
findReferencesTool,
|
|
29
33
|
writeFileTool,
|
|
30
34
|
editFileTool,
|
|
35
|
+
applyPatchTool,
|
|
31
36
|
editNotebookTool,
|
|
32
37
|
runCommandTool,
|
|
33
38
|
fetchUrlTool,
|
|
@@ -35,8 +40,12 @@ export const BUILTIN_TOOLS = [
|
|
|
35
40
|
memoryWriteTool,
|
|
36
41
|
memoryForgetTool,
|
|
37
42
|
subAgentTool,
|
|
43
|
+
subAgentWaitTool,
|
|
44
|
+
todoWriteTool,
|
|
45
|
+
todoReadTool,
|
|
46
|
+
askUserTool,
|
|
38
47
|
];
|
|
39
|
-
export { editFileTool, editNotebookTool, fetchUrlTool, findReferencesTool, findSymbolTool, listFilesTool, listSymbolsTool, memoryForgetTool, memoryReadTool, memoryWriteTool, readFileTool, readImageTool, runCommandTool, searchFilesTool, subAgentTool, writeFileTool, };
|
|
48
|
+
export { applyPatchTool, askUserTool, editFileTool, editNotebookTool, fetchUrlTool, findReferencesTool, findSymbolTool, listFilesTool, listSymbolsTool, memoryForgetTool, memoryReadTool, memoryWriteTool, readFileTool, readImageTool, runCommandTool, searchFilesTool, subAgentTool, subAgentWaitTool, todoReadTool, todoWriteTool, writeFileTool, };
|
|
40
49
|
export { checkPolicyViolation, globToRegex, mergeToolSandbox, normalizePathPattern, pathMatchesAny } from './policy.js';
|
|
41
50
|
export { redact, countRedactions, buildRules } from './redact.js';
|
|
42
51
|
export { ToolResultCache, buildCacheKey, cachedExecute } from './result-cache.js';
|
package/dist/tools/memory.d.ts
CHANGED
|
@@ -11,11 +11,11 @@ declare const readParams: z.ZodObject<{
|
|
|
11
11
|
/** Optional heading to filter by. Returns just that section's body. */
|
|
12
12
|
section: z.ZodOptional<z.ZodString>;
|
|
13
13
|
}, "strip", z.ZodTypeAny, {
|
|
14
|
-
scope?: "project" | "global" | "both" | undefined;
|
|
15
14
|
section?: string | undefined;
|
|
16
|
-
}, {
|
|
17
15
|
scope?: "project" | "global" | "both" | undefined;
|
|
16
|
+
}, {
|
|
18
17
|
section?: string | undefined;
|
|
18
|
+
scope?: "project" | "global" | "both" | undefined;
|
|
19
19
|
}>;
|
|
20
20
|
export interface MemoryReadResult {
|
|
21
21
|
scope: 'project' | 'global' | 'both';
|
|
@@ -43,17 +43,17 @@ declare const writeParams: z.ZodObject<{
|
|
|
43
43
|
}, "strip", z.ZodTypeAny, {
|
|
44
44
|
body: string;
|
|
45
45
|
heading: string;
|
|
46
|
-
type?: "
|
|
47
|
-
scope?: "project" | "global" | undefined;
|
|
46
|
+
type?: "convention" | "decision" | "correction" | "reference" | undefined;
|
|
48
47
|
refs?: string[] | undefined;
|
|
49
48
|
pin?: boolean | undefined;
|
|
49
|
+
scope?: "project" | "global" | undefined;
|
|
50
50
|
}, {
|
|
51
51
|
body: string;
|
|
52
52
|
heading: string;
|
|
53
|
-
type?: "
|
|
54
|
-
scope?: "project" | "global" | undefined;
|
|
53
|
+
type?: "convention" | "decision" | "correction" | "reference" | undefined;
|
|
55
54
|
refs?: string[] | undefined;
|
|
56
55
|
pin?: boolean | undefined;
|
|
56
|
+
scope?: "project" | "global" | undefined;
|
|
57
57
|
}>;
|
|
58
58
|
export interface MemoryWriteResult {
|
|
59
59
|
scope: 'project' | 'global';
|
package/dist/tools/notebook.d.ts
CHANGED
|
@@ -46,16 +46,16 @@ declare const params: z.ZodObject<{
|
|
|
46
46
|
cell_type: z.ZodOptional<z.ZodEnum<["code", "markdown", "raw"]>>;
|
|
47
47
|
}, "strip", z.ZodTypeAny, {
|
|
48
48
|
path: string;
|
|
49
|
+
mode?: "replace" | "delete" | "insert" | undefined;
|
|
49
50
|
source?: string | undefined;
|
|
50
51
|
cell_type?: "code" | "raw" | "markdown" | undefined;
|
|
51
|
-
mode?: "replace" | "insert" | "delete" | undefined;
|
|
52
52
|
cell_index?: number | undefined;
|
|
53
53
|
cell_id?: string | undefined;
|
|
54
54
|
}, {
|
|
55
55
|
path: string;
|
|
56
|
+
mode?: "replace" | "delete" | "insert" | undefined;
|
|
56
57
|
source?: string | undefined;
|
|
57
58
|
cell_type?: "code" | "raw" | "markdown" | undefined;
|
|
58
|
-
mode?: "replace" | "insert" | "delete" | undefined;
|
|
59
59
|
cell_index?: number | undefined;
|
|
60
60
|
cell_id?: string | undefined;
|
|
61
61
|
}>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ordered permission rules, as in Kilo's `permission` config: each permission (read, edit, bash, ...) maps to
|
|
3
|
+
* an action or to a pattern map, rules run in config order and the LAST match wins. Pure; the agent consults it
|
|
4
|
+
* in `requestApproval`.
|
|
5
|
+
*/
|
|
6
|
+
export type PermissionAction = 'allow' | 'ask' | 'deny';
|
|
7
|
+
export type PermissionConfig = Record<string, PermissionAction | Record<string, PermissionAction>>;
|
|
8
|
+
export interface PermissionRule {
|
|
9
|
+
permission: string;
|
|
10
|
+
pattern: string;
|
|
11
|
+
action: PermissionAction;
|
|
12
|
+
}
|
|
13
|
+
/** Flatten a config object into ordered rules; entries with an unknown action are dropped. */
|
|
14
|
+
export declare function compilePermissionRules(config: PermissionConfig | undefined): PermissionRule[];
|
|
15
|
+
/** The action of the last rule matching `permission` and `target`, or undefined when none does. */
|
|
16
|
+
export declare function evaluatePermission(rules: readonly PermissionRule[], permission: string, target: string): PermissionAction | undefined;
|
|
17
|
+
/**
|
|
18
|
+
* Split a shell command into the commands a permission rule must each allow: `&&`, `||`, `;`, `|` and newlines
|
|
19
|
+
* outside quotes separate commands; the inside of `$(...)` and backticks is added as extra commands, and an
|
|
20
|
+
* unquoted `>` redirect adds a `redirect:` entry, so an allow rule for `git status *` cannot cover
|
|
21
|
+
* `git status $(rm -rf x)` or `git status > file`.
|
|
22
|
+
*/
|
|
23
|
+
export declare function splitShellCommands(command: string): string[];
|
|
24
|
+
/** `.env` and `.env.*` hold secrets; `.env.example` and `.env.sample` are documentation. */
|
|
25
|
+
export declare function isSensitiveFile(relPath: string): boolean;
|
|
26
|
+
/** True when a token of the command points outside `cwd`: `~`, a `..` escape, or an absolute path elsewhere. */
|
|
27
|
+
export declare function touchesOutsideCwd(command: string, cwd: string): boolean;
|
|
28
|
+
/** Which permission a tool call falls under and the strings the rules are matched against; undefined = no rule applies. */
|
|
29
|
+
export declare function permissionFor(toolName: string, args: unknown, cwd: string): {
|
|
30
|
+
permission: string;
|
|
31
|
+
targets: string[];
|
|
32
|
+
} | undefined;
|