Агент-инженер по репозиторию · Модуль 1 · Урок 1.3
Первый инструмент: read_file (схема, tool_use, tool_result)
Зачем (какую проблему чиним)
Чтобы ответить на вопрос о файле, агент должен этот файл прочитать. Модель сама файлы не читает — она может лишь попросить инструмент. Дадим ей первый инструмент read_file: модель присылает блок tool_use с именем и аргументами, мы исполняем и возвращаем tool_result.
Решение и альтернативы
Решение: объявляем инструмент с JSON-схемой входа (имя файла), регистрируем обработчик, и в цикле при stop_reason == tool_use исполняем каждый запрошенный инструмент, возвращая результат тем же tool_use_id. Уже здесь закладываем безопасность чтения: путь обязан оставаться внутри корня репозитория (защита от ../../etc/passwd).
Альтернативы: отдавать модели весь файл в системном промпте заранее — не масштабируется (версия v2 про это целиком) и засоряет окно. Один универсальный инструмент «выполни что угодно» — дыра в безопасности; мы идём узкими, явными инструментами с валидацией.
DIFF
Добавляем описание инструмента, обработчик с проверкой пути и ветку исполнения инструментов в цикле.
⚠ Безопасность
Любой путь от модели — недоверенный ввод. read_file обязан: привести путь к абсолютному внутри корня репозитория и отвергнуть всё, что выходит за корень (path traversal). Это первый пример сквозного правила: инструмент агента валидирует вход так, будто он враждебный, — потому что через prompt injection он может таким и оказаться.
Проверка
Спросите агента «что делает функция main в main.go?». В логе видно: модель прислала tool_use read_file → мы вернули содержимое → модель ответила по сути. Запрос файла вне корня (../secret) — отклонён с ошибкой.
Глубже
Дизайн инструментов и схем, обработка ошибок, песочницы — курс «Продакшн-разработка», Модуль 4 (инструменты и MCP, гл. 7–8) и курс архитектора (CCA), модуль про проектирование инструментов и MCP.
+package main
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/anthropics/anthropic-sdk-go"
+)
+
+// readFileTool — описание инструмента для модели: имя, назначение, схема входа.
+func readFileTool() anthropic.ToolParam {
+ return anthropic.ToolParam{
+ Name: "read_file",
+ Description: anthropic.String("Прочитать текстовый файл репозитория по относительному пути."),
+ InputSchema: anthropic.ToolInputSchemaParam{
+ Properties: map[string]any{
+ "path": map[string]any{
+ "type": "string",
+ "description": "Путь относительно корня репозитория, напр. main.go",
+ },
+ },
+ Required: []string{"path"},
+ },
+ }
+}
+
+// safeJoin не выпускает путь за пределы корня репозитория (anti path-traversal).
+func safeJoin(root, rel string) (string, error) {
+ abs := filepath.Join(root, filepath.Clean("/"+rel))
+ if !strings.HasPrefix(abs, filepath.Clean(root)+string(os.PathSeparator)) && abs != filepath.Clean(root) {
+ return "", fmt.Errorf("путь вне репозитория: %s", rel)
+ }
+ return abs, nil
+}
+
+// execReadFile — обработчик инструмента: валидирует путь и читает файл.
+func execReadFile(root, rel string) (string, error) {
+ abs, err := safeJoin(root, rel)
+ if err != nil {
+ return "", err
+ }
+ b, err := os.ReadFile(abs)
+ if err != nil {
+ return "", err
+ }
+ return string(b), nil
+} func (a *Agent) callModel(ctx context.Context, msgs []anthropic.MessageParam) (*anthropic.Message, error) {
return a.client.Messages.New(ctx, anthropic.MessageNewParams{
Model: anthropic.Model(a.cfg.Model),
MaxTokens: int64(a.cfg.MaxTokens),
Messages: msgs,
+ Tools: []anthropic.ToolUnionParam{
+ {OfTool: &[]anthropic.ToolParam{readFileTool()}[0]},
+ },
})
}
msgs = append(msgs, resp.ToParam())
if resp.StopReason != anthropic.StopReasonToolUse {
return textOf(resp), nil
}
- // Исполнение инструментов добавим в уроке 1.3.
+ // Исполняем каждый запрошенный инструмент и возвращаем результаты.
+ var results []anthropic.ContentBlockParamUnion
+ for _, block := range resp.Content {
+ tu, ok := block.AsAny().(anthropic.ToolUseBlock)
+ if !ok {
+ continue
+ }
+ out, err := a.dispatch(tu)
+ results = append(results, anthropic.NewToolResultBlock(tu.ID, out, err != nil))
+ }
+ msgs = append(msgs, anthropic.NewUserMessage(results...))+// dispatch исполняет один tool_use. Возвращает текст результата; флаг ошибки
+// проставляется вызывающим через err != nil.
+func (a *Agent) dispatch(tu anthropic.ToolUseBlock) (string, error) {
+ switch tu.Name {
+ case "read_file":
+ var in struct{ Path string }
+ if err := json.Unmarshal(tu.Input, &in); err != nil {
+ return "неверные аргументы: " + err.Error(), err
+ }
+ out, err := execReadFile(a.cfg.Root, in.Path)
+ if err != nil {
+ return err.Error(), err
+ }
+ return out, nil
+ default:
+ return "неизвестный инструмент: " + tu.Name, fmt.Errorf("unknown tool %q", tu.Name)
+ }
+}Anti-patterns
| Грабля | Почему плохо | Как правильно |
|---|---|---|
Доверять пути от модели как есть (os.ReadFile(rel)) | Path traversal: модель (или инъекция) читает ../../etc/passwd или секреты вне репозитория | Валидировать путь через safeJoin: только внутри корня репозитория |
| Один универсальный инструмент «выполни команду/код» | Произвольное исполнение — дыра в безопасности; невозможно ограничить | Узкие явные инструменты с JSON-схемой и валидацией каждого аргумента |
Не возвращать tool_result на каждый tool_use | Протокол ломается: модель ждёт результат по tool_use_id, иначе зависает или ошибается | На каждый запрошенный инструмент вернуть результат с тем же tool_use_id |
| Глотать ошибку инструмента молча (вернуть пустую строку без флага ошибки) | Модель не понимает, что инструмент упал, и строит ответ на пустоте | Возвращать текст ошибки и помечать is_error=true, чтобы модель отреагировала |
Практическое задание (RA-v0)
- Добавить
RootвConfig(корень анализируемого репозитория) иread_fileсsafeJoin. - В цикле при
tool_useисполнять инструменты и возвращатьtool_resultпоtool_use_id(флаг is_error при ошибке). - Проверить отказ на
../-пути. Закоммитить:git commit -m "v0: add read_file tool with path safety".
Проверка знаний
Обработчик read_file реализован как os.ReadFile(relPathFromModel) без проверок.
Какая уязвимость здесь и как её закрыть?
Верный ответ: B
B. Путь приходит от модели и потому недоверенный: ../../etc/passwd или путь к секретам вне репозитория. safeJoin приводит путь к абсолютному внутри корня и отвергает выходящее за корень. Это не про утечку памяти (A) и не безопасно по умолчанию (C).
Модель в одном ответе прислала два блока tool_use. Что обязан сделать цикл перед следующим вызовом модели?
Верный ответ: B
B. На каждый tool_use нужен соответствующий tool_result с тем же tool_use_id; их возвращают вместе одним user-сообщением. Исполнить только один (A) или проигнорировать (D) — сломать протокол; два вызова модели (C) не нужны.