@arnilo/prism-tools 0.0.0-stage → 0.13.1
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/CHANGELOG.md +418 -0
- package/LICENSE +21 -0
- package/README.md +44 -2
- package/browser/README.md +98 -0
- package/browser/skills/browser-automation/SKILL.md +14 -0
- package/browser/src/actions/cdp.ts +131 -0
- package/browser/src/actions/dialog.ts +25 -0
- package/browser/src/actions/download_release.ts +22 -0
- package/browser/src/actions/index.ts +37 -0
- package/browser/src/actions/locator.ts +70 -0
- package/browser/src/actions/navigate.ts +23 -0
- package/browser/src/actions/screenshot.ts +27 -0
- package/browser/src/actions/scroll.ts +24 -0
- package/browser/src/actions/select_page.ts +21 -0
- package/browser/src/actions/types.ts +114 -0
- package/browser/src/actions/upload.ts +44 -0
- package/browser/src/actions/wait.ts +45 -0
- package/browser/src/browser.ts +197 -0
- package/browser/src/cdp.ts +178 -0
- package/browser/src/checkpoint.ts +189 -0
- package/browser/src/config.ts +72 -0
- package/browser/src/downloads.ts +253 -0
- package/browser/src/errors.ts +31 -0
- package/browser/src/evaluate.ts +101 -0
- package/browser/src/index.ts +129 -0
- package/browser/src/limits.ts +271 -0
- package/browser/src/manager.ts +730 -0
- package/browser/src/network.ts +263 -0
- package/browser/src/observe.ts +178 -0
- package/browser/src/policy.ts +133 -0
- package/browser/src/screenshot.ts +107 -0
- package/browser/src/shared-sandbox.ts +53 -0
- package/browser/src/snapshot.ts +99 -0
- package/browser/src/targets.ts +165 -0
- package/browser/src/tools.ts +480 -0
- package/browser/src/types.ts +316 -0
- package/browser/src/uploads.ts +174 -0
- package/coding/README.md +337 -0
- package/coding/src/agent/acp-operations.ts +146 -0
- package/coding/src/agent/artifacts.ts +38 -0
- package/coding/src/agent/ask-user-decision.ts +623 -0
- package/coding/src/agent/atomic-write.ts +23 -0
- package/coding/src/agent/bounded-file.ts +21 -0
- package/coding/src/agent/checks.ts +301 -0
- package/coding/src/agent/coding-checkpoint.ts +828 -0
- package/coding/src/agent/delete.ts +237 -0
- package/coding/src/agent/diagnostics-tool.ts +163 -0
- package/coding/src/agent/diagnostics.ts +262 -0
- package/coding/src/agent/edit-diff.ts +382 -0
- package/coding/src/agent/edit.ts +322 -0
- package/coding/src/agent/effects.ts +102 -0
- package/coding/src/agent/env.ts +28 -0
- package/coding/src/agent/execution-policy.ts +89 -0
- package/coding/src/agent/file-mutation-queue.ts +72 -0
- package/coding/src/agent/forge/github.ts +603 -0
- package/coding/src/agent/forge/index.ts +16 -0
- package/coding/src/agent/forge/types.ts +185 -0
- package/coding/src/agent/git-aware-repository.ts +305 -0
- package/coding/src/agent/git-exec.ts +326 -0
- package/coding/src/agent/git-status.ts +177 -0
- package/coding/src/agent/git-tools.ts +360 -0
- package/coding/src/agent/git.ts +1074 -0
- package/coding/src/agent/glob-match.ts +146 -0
- package/coding/src/agent/glob.ts +182 -0
- package/coding/src/agent/goal-verify.ts +385 -0
- package/coding/src/agent/grep-worker.ts +74 -0
- package/coding/src/agent/grep.ts +459 -0
- package/coding/src/agent/identity.ts +48 -0
- package/coding/src/agent/index.ts +676 -0
- package/coding/src/agent/language/client.ts +429 -0
- package/coding/src/agent/language/framing.ts +190 -0
- package/coding/src/agent/language/index.ts +23 -0
- package/coding/src/agent/language/intelligence.ts +682 -0
- package/coding/src/agent/language/types.ts +187 -0
- package/coding/src/agent/lifecycle-events.ts +121 -0
- package/coding/src/agent/lifecycle.ts +166 -0
- package/coding/src/agent/limits.ts +214 -0
- package/coding/src/agent/move.ts +178 -0
- package/coding/src/agent/mutation-path.ts +52 -0
- package/coding/src/agent/output-accumulator.ts +304 -0
- package/coding/src/agent/path-utils.ts +120 -0
- package/coding/src/agent/process/index.ts +58 -0
- package/coding/src/agent/process/recovery.ts +508 -0
- package/coding/src/agent/process/sessions-host.ts +222 -0
- package/coding/src/agent/process/sessions-monitor.ts +141 -0
- package/coding/src/agent/process/sessions-recovery.ts +241 -0
- package/coding/src/agent/process/sessions-spawn.ts +613 -0
- package/coding/src/agent/process/sessions-teardown.ts +281 -0
- package/coding/src/agent/process/sessions.ts +30 -0
- package/coding/src/agent/process/types.ts +358 -0
- package/coding/src/agent/read-path-set.ts +126 -0
- package/coding/src/agent/read.ts +713 -0
- package/coding/src/agent/repository/glob.ts +147 -0
- package/coding/src/agent/repository/indexed-search.ts +480 -0
- package/coding/src/agent/repository/list.ts +131 -0
- package/coding/src/agent/repository/operations.ts +22 -0
- package/coding/src/agent/repository/path.ts +98 -0
- package/coding/src/agent/repository/search.ts +277 -0
- package/coding/src/agent/repository/types.ts +243 -0
- package/coding/src/agent/repository/walk.ts +126 -0
- package/coding/src/agent/repository.ts +12 -0
- package/coding/src/agent/shell-background.ts +264 -0
- package/coding/src/agent/shell-output.ts +75 -0
- package/coding/src/agent/shell.ts +526 -0
- package/coding/src/agent/todo.ts +399 -0
- package/coding/src/agent/truncate.ts +251 -0
- package/coding/src/agent/write.ts +170 -0
- package/coding/src/ask-user-host.ts +48 -0
- package/coding/src/config.ts +278 -0
- package/coding/src/hard-deny.ts +97 -0
- package/coding/src/index.ts +318 -0
- package/coding/src/principal-guard.ts +84 -0
- package/coding/src/sandbox.ts +92 -0
- package/coding/src/security/approval.ts +144 -0
- package/coding/src/security/command-rules.ts +57 -0
- package/coding/src/security/docker-cli.ts +222 -0
- package/coding/src/security/docker-sandbox.ts +1205 -0
- package/coding/src/security/e2b-sandbox.ts +853 -0
- package/coding/src/security/egress/dns-pin.ts +88 -0
- package/coding/src/security/egress/index.ts +32 -0
- package/coding/src/security/egress/limits.ts +57 -0
- package/coding/src/security/egress/policy.ts +137 -0
- package/coding/src/security/egress/proxy.ts +599 -0
- package/coding/src/security/egress/types.ts +30 -0
- package/coding/src/security/index.ts +199 -0
- package/coding/src/security/native-sandbox.ts +639 -0
- package/coding/src/security/path-containment.ts +55 -0
- package/coding/src/security/sandbox-coding-operations.ts +392 -0
- package/coding/src/security/sandbox-fs-operations.ts +644 -0
- package/coding/src/security/sandbox-limits.ts +147 -0
- package/coding/src/security/sandbox-tar.ts +308 -0
- package/coding/src/security/sandbox.ts +179 -0
- package/coding/src/security/semaphore.ts +55 -0
- package/coding/src/targets.ts +110 -0
- package/coding/src/workspace/diff.ts +78 -0
- package/coding/src/workspace/errors.ts +29 -0
- package/coding/src/workspace/index.ts +5 -0
- package/coding/src/workspace/merge.ts +151 -0
- package/coding/src/workspace/routing.ts +76 -0
- package/coding/src/workspace/service.ts +584 -0
- package/computer-use/README.md +105 -0
- package/computer-use/skills/computer-use-linux/SKILL.md +17 -0
- package/computer-use/src/classify.ts +53 -0
- package/computer-use/src/command.ts +29 -0
- package/computer-use/src/config.ts +69 -0
- package/computer-use/src/create.ts +170 -0
- package/computer-use/src/devices.ts +247 -0
- package/computer-use/src/env.ts +28 -0
- package/computer-use/src/index.ts +157 -0
- package/computer-use/src/skill.ts +21 -0
- package/openapi/README.md +97 -0
- package/openapi/src/compile.ts +386 -0
- package/openapi/src/config.ts +73 -0
- package/openapi/src/errors.ts +16 -0
- package/openapi/src/execute.ts +241 -0
- package/openapi/src/index.ts +124 -0
- package/openapi/src/limits.ts +63 -0
- package/openapi/src/specs.ts +86 -0
- package/openapi/src/tools.ts +60 -0
- package/openapi/src/types.ts +61 -0
- package/package.json +171 -4
- package/research/README.md +72 -0
- package/research/skills/research-report/SKILL.md +12 -0
- package/research/skills/research-scoping/SKILL.md +10 -0
- package/research/skills/research-synthesis/SKILL.md +10 -0
- package/research/skills/source-triage/SKILL.md +10 -0
- package/research/src/check.ts +71 -0
- package/research/src/config.ts +41 -0
- package/research/src/index.ts +147 -0
- package/research/src/ledger.ts +157 -0
- package/research/src/urls.ts +21 -0
- package/web/README.md +287 -0
- package/web/skills/web-research/SKILL.md +17 -0
- package/web/src/brave.ts +66 -0
- package/web/src/cache.ts +151 -0
- package/web/src/config.ts +210 -0
- package/web/src/controls.ts +128 -0
- package/web/src/domains.ts +114 -0
- package/web/src/exa.ts +60 -0
- package/web/src/firecrawl.ts +160 -0
- package/web/src/index.ts +387 -0
- package/web/src/kagi.ts +73 -0
- package/web/src/limits.ts +55 -0
- package/web/src/native/convert.ts +84 -0
- package/web/src/native/fetch.ts +226 -0
- package/web/src/normalize.ts +131 -0
- package/web/src/obscura/cdp.ts +201 -0
- package/web/src/obscura/classify.ts +35 -0
- package/web/src/obscura/cli.ts +254 -0
- package/web/src/obscura/errors.ts +8 -0
- package/web/src/obscura/index.ts +59 -0
- package/web/src/obscura/limits.ts +92 -0
- package/web/src/obscura/mcp.ts +104 -0
- package/web/src/obscura/playwright.ts +133 -0
- package/web/src/obscura/process.ts +235 -0
- package/web/src/obscura/search-profile.ts +73 -0
- package/web/src/obscura/types.ts +48 -0
- package/web/src/obscura/web.ts +419 -0
- package/web/src/prompt-fetch.ts +88 -0
- package/web/src/render.ts +75 -0
- package/web/src/searxng.ts +78 -0
- package/web/src/slice.ts +139 -0
- package/web/src/tavily.ts +77 -0
- package/web/src/tools.ts +182 -0
- package/web/src/transport.ts +310 -0
- package/web/src/types.ts +199 -0
- package/work/README.md +77 -0
- package/work/THIRD_PARTY_NOTICES +24 -0
- package/work/docling/ocr.py +150 -0
- package/work/sandbox/Dockerfile +36 -0
- package/work/sandbox/soffice.sh +12 -0
- package/work/src/command.ts +29 -0
- package/work/src/config.ts +138 -0
- package/work/src/connectors/cli.ts +343 -0
- package/work/src/connectors/drafts.ts +647 -0
- package/work/src/connectors/errors.ts +8 -0
- package/work/src/connectors/file-bytes.ts +149 -0
- package/work/src/connectors/google-workspace-http.ts +399 -0
- package/work/src/connectors/google-workspace.ts +465 -0
- package/work/src/connectors/gws-tools.ts +378 -0
- package/work/src/connectors/http.ts +123 -0
- package/work/src/connectors/idempotency.ts +235 -0
- package/work/src/connectors/identity.ts +34 -0
- package/work/src/connectors/index.ts +93 -0
- package/work/src/connectors/limits.ts +50 -0
- package/work/src/connectors/m365-tools.ts +308 -0
- package/work/src/connectors/microsoft365-http.ts +428 -0
- package/work/src/connectors/microsoft365.ts +402 -0
- package/work/src/connectors/normalize.ts +120 -0
- package/work/src/connectors/tool-helpers.ts +398 -0
- package/work/src/connectors/tools.ts +42 -0
- package/work/src/connectors/types.ts +399 -0
- package/work/src/diagrams/canonicalize.ts +166 -0
- package/work/src/diagrams/caps.ts +52 -0
- package/work/src/diagrams/embed.ts +340 -0
- package/work/src/diagrams/errors.ts +76 -0
- package/work/src/diagrams/index.ts +81 -0
- package/work/src/diagrams/messages.ts +150 -0
- package/work/src/diagrams/origin.ts +52 -0
- package/work/src/diagrams/telemetry.ts +34 -0
- package/work/src/diagrams/xml.ts +267 -0
- package/work/src/document-extraction/index.ts +563 -0
- package/work/src/document-reader/ambient.d.ts +2 -0
- package/work/src/document-reader/errors.ts +7 -0
- package/work/src/document-reader/index.ts +181 -0
- package/work/src/document-reader/mistral-ocr.ts +318 -0
- package/work/src/document-reader/parsers.ts +171 -0
- package/work/src/documents/caps.ts +135 -0
- package/work/src/documents/diff.ts +269 -0
- package/work/src/documents/errors.ts +44 -0
- package/work/src/documents/fidelity.ts +98 -0
- package/work/src/documents/generate.ts +96 -0
- package/work/src/documents/hash.ts +8 -0
- package/work/src/documents/index.ts +14 -0
- package/work/src/documents/model-schema.ts +639 -0
- package/work/src/documents/parse.ts +252 -0
- package/work/src/documents/patch-history.ts +81 -0
- package/work/src/documents/patch.ts +476 -0
- package/work/src/documents/preview-html.ts +245 -0
- package/work/src/documents/preview.ts +227 -0
- package/work/src/documents/telemetry.ts +36 -0
- package/work/src/documents/translate/docx.ts +292 -0
- package/work/src/documents/translate/pptx.ts +304 -0
- package/work/src/documents/translate/xlsx.ts +137 -0
- package/work/src/documents/types.ts +188 -0
- package/work/src/index.ts +311 -0
- package/work/src/runtime/worker-pool-entry.ts +102 -0
- package/work/src/runtime/worker-pool.ts +267 -0
- package/work/src/sandbox/composition.ts +212 -0
- package/work/src/sandbox/image.ts +6 -0
- package/work/src/sandbox/index.ts +14 -0
- package/work/src/sheets/caps.ts +68 -0
- package/work/src/sheets/csv.ts +351 -0
- package/work/src/sheets/decimal.ts +388 -0
- package/work/src/sheets/errors.ts +52 -0
- package/work/src/sheets/index.ts +62 -0
- package/work/src/sheets/inference.ts +378 -0
- package/work/src/sheets/telemetry.ts +34 -0
- package/work/src/sheets/types.ts +162 -0
- package/work/src/sheets/xlsx.ts +388 -0
- package/work/src/skills/index.ts +8 -0
- package/work/src/skills/load.ts +43 -0
- package/work/src/tools/filesystem.ts +21 -0
- package/work/src/tools/index.ts +1 -0
- package/work/src/tools/local-filesystem.ts +33 -0
- package/work/src/tools/office.ts +311 -0
- package/work/vendor/hermes-agent/LICENSE +21 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/LICENSE +21 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/SKILL.md +196 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_comments.py +289 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_common.py +94 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_create.py +177 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_edit.py +250 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_read.py +149 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_revisions.py +147 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_template.py +70 -0
- package/work/vendor/hermes-agent/skills/productivity/docx/scripts/docx_validate.py +156 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/LICENSE +21 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/SKILL.md +125 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/_raster.py +76 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/extract_marker.py +88 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/extract_pymupdf.py +101 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_create.py +130 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_fill_form.py +97 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_form_layout.py +168 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_make_form.py +145 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_merge.py +50 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_meta.py +115 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_page_image.py +99 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_read.py +153 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_secure.py +71 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_split.py +84 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_stamp.py +143 -0
- package/work/vendor/hermes-agent/skills/productivity/pdf/scripts/pdf_watermark.py +51 -0
- package/work/vendor/hermes-agent/skills/productivity/powerpoint/LICENSE +21 -0
- package/work/vendor/hermes-agent/skills/productivity/powerpoint/SKILL.md +220 -0
- package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_create.py +214 -0
- package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_edit.py +436 -0
- package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_from_template.py +88 -0
- package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_read.py +131 -0
- package/work/vendor/hermes-agent/skills/productivity/powerpoint/scripts/pptx_render.py +93 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/LICENSE +21 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/SKILL.md +196 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/csv_to_xlsx.py +104 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_create.py +259 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_edit.py +263 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_read.py +160 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_recalc.py +111 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_restructure.py +337 -0
- package/work/vendor/hermes-agent/skills/productivity/xlsx/scripts/xlsx_to_csv.py +69 -0
- package/work/vendor-lock.json +11 -0
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Write tool: create or overwrite a file on the host filesystem.
|
|
3
|
+
*
|
|
4
|
+
* Behavioral port of pi's core/tools/write for @arnilo/prism-tools/coding, adapted to Prism's
|
|
5
|
+
* `ToolDefinition` contract. Faithfully ports the create-parent-dirs → write → serialize-per-path
|
|
6
|
+
* flow. Drops pi's TUI (`renderCall`/`renderResult`, the incremental syntax-highlight cache, key hints).
|
|
7
|
+
*
|
|
8
|
+
* Deviations from pi (documented):
|
|
9
|
+
* - **Confirmation message carries the absolute path + real byte count + line count** (pi returns the
|
|
10
|
+
* caller-supplied path and `content.length`, which is a UTF-16 *code-unit* count mislabeled "bytes").
|
|
11
|
+
* The plan's acceptance criteria call for absolute path + byte/line counts; this is strictly more
|
|
12
|
+
* informative and the byte count is now UTF-8-correct.
|
|
13
|
+
* - Abort + all fs failures return a Prism `error` result (pi throws/rejects). Abort is checked before
|
|
14
|
+
* each filesystem operation (mkdir, writeFile); if the write completes it is reported as success
|
|
15
|
+
* (pi would throw "Operation aborted" even after a successful write — misleading, so dropped).
|
|
16
|
+
*/
|
|
17
|
+
import { Buffer } from "node:buffer";
|
|
18
|
+
import { mkdir as fsMkdir } from "node:fs/promises";
|
|
19
|
+
import { dirname } from "node:path";
|
|
20
|
+
import type { JsonObject, ToolDefinition, ToolExecutionContext, ToolResult } from "@arnilo/prism";
|
|
21
|
+
import { atomicWriteUtf8File } from "./atomic-write.js";
|
|
22
|
+
import { CODING_LOCAL_EFFECT } from "./effects.js";
|
|
23
|
+
import type { ExecutionPolicy } from "./execution-policy.js";
|
|
24
|
+
import { enforceExecutionPolicy } from "./execution-policy.js";
|
|
25
|
+
import { withFileMutationQueue } from "./file-mutation-queue.js";
|
|
26
|
+
import type { CodingLifecycleEvent } from "./lifecycle-events.js";
|
|
27
|
+
import { DEFAULT_MAX_WRITE_BYTES, HARD_MAX_WRITE_BYTES, validateCodingLimit } from "./limits.js";
|
|
28
|
+
import { resolveToCwd } from "./path-utils.js";
|
|
29
|
+
import { type ReadBeforeWriteOptions, refuseReadBeforeWrite } from "./read-path-set.js";
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Pluggable operations for the write tool. Override to delegate file writing to remote systems
|
|
33
|
+
* (e.g. SSH) while keeping the tool's directory-creation + per-path serialization behavior.
|
|
34
|
+
*/
|
|
35
|
+
export interface WriteOperations {
|
|
36
|
+
/** Write content to a file (creating or overwriting). */
|
|
37
|
+
writeFile: (absolutePath: string, content: string, options?: { maxBytes: number; signal?: AbortSignal }) => Promise<void>;
|
|
38
|
+
/** Create a directory recursively. */
|
|
39
|
+
mkdir: (dir: string, options?: { signal?: AbortSignal }) => Promise<void>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface WriteToolOptions extends ReadBeforeWriteOptions {
|
|
43
|
+
/** Structured pre-execution policy checked before filesystem writes. */
|
|
44
|
+
executionPolicy?: ExecutionPolicy;
|
|
45
|
+
/** Custom operations backend (default: local filesystem). */
|
|
46
|
+
operations?: WriteOperations;
|
|
47
|
+
/** Maximum UTF-8 input bytes accepted before any filesystem mutation (default 8 MiB). */
|
|
48
|
+
maxInputBytes?: number;
|
|
49
|
+
/** Optional consumer-gated lifecycle listener (file_changed / permission_denied). */
|
|
50
|
+
onEvent?: (event: CodingLifecycleEvent) => void;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const defaultWriteOperations: WriteOperations = {
|
|
54
|
+
writeFile: (path, content, options) => atomicWriteUtf8File(path, content, { signal: options?.signal }),
|
|
55
|
+
mkdir: (dir) => fsMkdir(dir, { recursive: true }).then(() => {}),
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
function errorResult(toolCallId: string, message: string): ToolResult {
|
|
59
|
+
return {
|
|
60
|
+
toolCallId,
|
|
61
|
+
name: "write",
|
|
62
|
+
content: [{ type: "text", text: message }],
|
|
63
|
+
error: { message },
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function countLines(content: string): number {
|
|
68
|
+
if (content.length === 0) return 0;
|
|
69
|
+
return content.split("\n").length;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function createWriteTool(cwd: string, options?: WriteToolOptions): ToolDefinition {
|
|
73
|
+
const ops = options?.operations ?? defaultWriteOperations;
|
|
74
|
+
const maxInputBytes = validateCodingLimit("maxInputBytes", options?.maxInputBytes ?? DEFAULT_MAX_WRITE_BYTES, HARD_MAX_WRITE_BYTES);
|
|
75
|
+
const guard = options?.requireReadBeforeWrite === true;
|
|
76
|
+
|
|
77
|
+
return {
|
|
78
|
+
name: "write",
|
|
79
|
+
kind: "edit",
|
|
80
|
+
effect: CODING_LOCAL_EFFECT,
|
|
81
|
+
effects: ["write"],
|
|
82
|
+
description: `Create or overwrite a file (full replace). Creates parent directories. Prefer edit for targeted changes.${guard ? " Read the path first or pass force=true to override the host's read-before-write guard." : ""}`,
|
|
83
|
+
parameters: {
|
|
84
|
+
type: "object",
|
|
85
|
+
properties: {
|
|
86
|
+
path: { type: "string", description: "Path to the file to write (relative or absolute)" },
|
|
87
|
+
content: { type: "string", description: "Content to write to the file" },
|
|
88
|
+
...(guard
|
|
89
|
+
? {
|
|
90
|
+
force: {
|
|
91
|
+
type: "boolean",
|
|
92
|
+
description: "Bypass the read-before-write guard for this call.",
|
|
93
|
+
},
|
|
94
|
+
}
|
|
95
|
+
: {}),
|
|
96
|
+
},
|
|
97
|
+
required: ["path", "content"],
|
|
98
|
+
additionalProperties: false,
|
|
99
|
+
} as JsonObject,
|
|
100
|
+
async execute(args, context: ToolExecutionContext): Promise<ToolResult> {
|
|
101
|
+
const toolCallId = context.toolCallId;
|
|
102
|
+
|
|
103
|
+
const path = typeof args.path === "string" ? args.path : "";
|
|
104
|
+
const content = typeof args.content === "string" ? args.content : undefined;
|
|
105
|
+
const force = args.force === true;
|
|
106
|
+
|
|
107
|
+
if (path.length === 0) {
|
|
108
|
+
return errorResult(toolCallId, "path is required and must be a non-empty string.");
|
|
109
|
+
}
|
|
110
|
+
if (content === undefined) {
|
|
111
|
+
return errorResult(toolCallId, "content is required and must be a string.");
|
|
112
|
+
}
|
|
113
|
+
const bytes = Buffer.byteLength(content, "utf-8");
|
|
114
|
+
if (bytes > maxInputBytes) {
|
|
115
|
+
return errorResult(toolCallId, `Write input is ${bytes} bytes, exceeds ${maxInputBytes} byte limit.`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
const absolutePath = resolveToCwd(path, cwd);
|
|
120
|
+
|
|
121
|
+
const policyCheck = await enforceExecutionPolicy(
|
|
122
|
+
options?.executionPolicy,
|
|
123
|
+
{
|
|
124
|
+
kind: "write",
|
|
125
|
+
operation: "write",
|
|
126
|
+
paths: [absolutePath],
|
|
127
|
+
risk: "medium",
|
|
128
|
+
metadata: { bytes, sessionId: context.sessionId, runId: context.runId, signal: context.signal },
|
|
129
|
+
},
|
|
130
|
+
toolCallId,
|
|
131
|
+
"write",
|
|
132
|
+
(denied) => options?.onEvent?.({ type: "permission_denied", ...denied }),
|
|
133
|
+
);
|
|
134
|
+
if (!policyCheck.allowed) return policyCheck.result;
|
|
135
|
+
const allowedPath = policyCheck.action.paths?.[0] ?? absolutePath;
|
|
136
|
+
|
|
137
|
+
const rbwRefusal = refuseReadBeforeWrite("write", path, allowedPath, options, force);
|
|
138
|
+
if (rbwRefusal) return errorResult(toolCallId, rbwRefusal);
|
|
139
|
+
|
|
140
|
+
const dir = dirname(allowedPath);
|
|
141
|
+
|
|
142
|
+
return await withFileMutationQueue(allowedPath, async () => {
|
|
143
|
+
// Check abort before each fs op — do not start a new operation once aborted. We intentionally
|
|
144
|
+
// do NOT throw from an abort listener: that could release the mutation queue mid-operation.
|
|
145
|
+
if (context.signal?.aborted) return errorResult(toolCallId, "Operation aborted");
|
|
146
|
+
await ops.mkdir(dir, { signal: context.signal });
|
|
147
|
+
if (context.signal?.aborted) return errorResult(toolCallId, "Operation aborted");
|
|
148
|
+
await ops.writeFile(allowedPath, content, { maxBytes: maxInputBytes, signal: context.signal });
|
|
149
|
+
options?.onEvent?.({ type: "file_changed", path: allowedPath, op: "write", toolCallId });
|
|
150
|
+
|
|
151
|
+
const lines = countLines(content);
|
|
152
|
+
return {
|
|
153
|
+
toolCallId,
|
|
154
|
+
name: "write",
|
|
155
|
+
content: [
|
|
156
|
+
{
|
|
157
|
+
type: "text",
|
|
158
|
+
text: `Successfully wrote ${bytes} bytes (${lines} lines) to ${allowedPath}`,
|
|
159
|
+
},
|
|
160
|
+
],
|
|
161
|
+
metadata: { bytes, lines, path: allowedPath },
|
|
162
|
+
};
|
|
163
|
+
});
|
|
164
|
+
} catch (error) {
|
|
165
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
166
|
+
return errorResult(toolCallId, message);
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
};
|
|
170
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `ask_user_decision` host gate (plan 154 Task 5).
|
|
3
|
+
*
|
|
4
|
+
* A `tool.call` decide handler owns the ask: it calls `host.ui` `select` (options, plus a custom
|
|
5
|
+
* sentinel when `allowCustom`) and `input` (a custom answer) and returns `{ respond }` with the
|
|
6
|
+
* resulting tool result. When the host answers `unavailable` — or no UI is attached — it returns
|
|
7
|
+
* `{ ask }`; the tool's `elicitation` contract then carries the answer schema and the kernel
|
|
8
|
+
* suspends the run, so `session.resume(decisions)` delivers the answer without executing the tool.
|
|
9
|
+
* `dismissed` becomes a declined tool result and never allows a fabricated choice.
|
|
10
|
+
*/
|
|
11
|
+
import type { HostUi, ToolCallDecision, ToolCallHookPayload } from "@arnilo/prism";
|
|
12
|
+
import {
|
|
13
|
+
ASK_USER_DECISION_TOOL_NAME,
|
|
14
|
+
askUserDecisionViaUi,
|
|
15
|
+
parseAskUserDecisionArgs,
|
|
16
|
+
resolveAskUserDecisionLimits,
|
|
17
|
+
} from "./agent/ask-user-decision.js";
|
|
18
|
+
|
|
19
|
+
export interface AskUserHostHookOptions {
|
|
20
|
+
/** The host UI port, when one is attached. */
|
|
21
|
+
readonly ui?: HostUi;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Build the `ask_user_decision` gate. It ignores every other tool and never throws. */
|
|
25
|
+
export function createAskUserHostHook(
|
|
26
|
+
options: AskUserHostHookOptions,
|
|
27
|
+
): (payload: ToolCallHookPayload) => Promise<ToolCallDecision | undefined> {
|
|
28
|
+
const limits = resolveAskUserDecisionLimits();
|
|
29
|
+
return async (payload) => {
|
|
30
|
+
if (payload.name !== ASK_USER_DECISION_TOOL_NAME) return undefined;
|
|
31
|
+
let parsed: ReturnType<typeof parseAskUserDecisionArgs>;
|
|
32
|
+
try {
|
|
33
|
+
parsed = parseAskUserDecisionArgs(payload.args, limits);
|
|
34
|
+
} catch {
|
|
35
|
+
// Malformed args: let `execute` report the parse error rather than suspend on a bad question.
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
if (options.ui === undefined) return { ask: parsed.question };
|
|
39
|
+
try {
|
|
40
|
+
const outcome = await askUserDecisionViaUi(options.ui, parsed, limits, payload.callId);
|
|
41
|
+
if (outcome.kind === "unavailable") return { ask: parsed.question };
|
|
42
|
+
return { respond: outcome.result };
|
|
43
|
+
} catch {
|
|
44
|
+
// A throwing host UI cannot answer: suspend instead of denying or fabricating a choice.
|
|
45
|
+
return { ask: parsed.question };
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
}
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
import type { JsonSchema } from "@arnilo/prism";
|
|
2
|
+
|
|
3
|
+
import type { NamedCheckDefinition } from "./agent/checks.js";
|
|
4
|
+
import type { RepositoryLimitOptions } from "./agent/repository/types.js";
|
|
5
|
+
import type { CodingSandboxKind } from "./sandbox.js";
|
|
6
|
+
|
|
7
|
+
/** Shell tool settings. `enabled: false` registers every other tool but not `shell`. */
|
|
8
|
+
export interface ToolsCodingShellConfig {
|
|
9
|
+
/** Default `true`. When `false`, `shell` is not registered. */
|
|
10
|
+
readonly enabled?: boolean;
|
|
11
|
+
/** Default wall timeout in milliseconds. Converted to the tool's seconds option. */
|
|
12
|
+
readonly timeoutMs?: number;
|
|
13
|
+
/** Background processes from `shell({ background: true })` (plan 189 Task 9). */
|
|
14
|
+
readonly background?: ToolsCodingShellBackgroundConfig;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Background-process settings. `shell_output` reads, waits on, and kills these. */
|
|
18
|
+
export interface ToolsCodingShellBackgroundConfig {
|
|
19
|
+
/** Live background processes per session. Default 4, hard 16. */
|
|
20
|
+
readonly maxProcesses?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** One JSON command rule. `match` is a substring or a `/pattern/` string. */
|
|
24
|
+
export interface ToolsCodingCommandRule {
|
|
25
|
+
readonly match: string;
|
|
26
|
+
readonly decision: "allow" | "deny" | "requireApproval";
|
|
27
|
+
readonly reason?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** One egress allow entry, as `createEgressPolicy` accepts it. */
|
|
31
|
+
export interface ToolsCodingEgressRule {
|
|
32
|
+
readonly host: string;
|
|
33
|
+
readonly port: number;
|
|
34
|
+
readonly protocol?: "http" | "https";
|
|
35
|
+
readonly allowPrivate?: boolean;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Egress allow-list for sandboxed shells. Validated with `createEgressPolicy`. */
|
|
39
|
+
export interface ToolsCodingEgressConfig {
|
|
40
|
+
readonly allow?: readonly ToolsCodingEgressRule[];
|
|
41
|
+
readonly presets?: readonly ("npm-registry" | "github")[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** One language server. Mirrors `LanguageServerSpec`; the model never supplies `command`. */
|
|
45
|
+
export interface ToolsCodingLanguageServerConfig {
|
|
46
|
+
readonly command: string;
|
|
47
|
+
readonly args?: readonly string[];
|
|
48
|
+
/** LSP language ids the server handles (`typescript`, `typescriptreact`, `python`, …). */
|
|
49
|
+
readonly languages: readonly string[];
|
|
50
|
+
readonly env?: Readonly<Record<string, string>>;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Language servers behind the opt-in `diagnostics` tool. No servers means no tool. */
|
|
54
|
+
export interface ToolsCodingLanguageConfig {
|
|
55
|
+
readonly servers?: Readonly<Record<string, ToolsCodingLanguageServerConfig>>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Structured git tools. `enabled: false` registers no `git_*` tool. Default `{ enabled: true }`. */
|
|
59
|
+
export interface ToolsCodingGitConfig {
|
|
60
|
+
readonly enabled?: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** `todo_write` and its `run.end` continuation. `continueOnOpen: false` keeps the tool, drops the hook. */
|
|
64
|
+
export interface ToolsCodingTodoConfig {
|
|
65
|
+
readonly enabled?: boolean;
|
|
66
|
+
readonly continueOnOpen?: boolean;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** The `workspace` service: disposable git worktrees for delegated coding work (plan 165 Task 6). */
|
|
70
|
+
export interface ToolsCodingWorkspaceConfig {
|
|
71
|
+
/** Default `true`. When `false`, no `workspace` service is provided. */
|
|
72
|
+
readonly enabled?: boolean;
|
|
73
|
+
/** Where worktrees are created. Default: a per-repository directory under the OS temp dir. */
|
|
74
|
+
readonly worktreeRoot?: string;
|
|
75
|
+
/** Maximum active worktrees. Default 4, hard cap 16. */
|
|
76
|
+
readonly maxWorktrees?: number;
|
|
77
|
+
/** `diff` bounds. Defaults 200 files and 1 MiB per file. */
|
|
78
|
+
readonly diff?: {
|
|
79
|
+
readonly maxFiles?: number;
|
|
80
|
+
readonly maxBytes?: number;
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** The principal durable-effect tools require. Opaque; governance verifies it in Phase 9. */
|
|
85
|
+
export interface ToolsCodingPrincipalConfig {
|
|
86
|
+
readonly id: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Forge credential ref (never a literal). Resolved through `prism.credentials.resolve`. */
|
|
90
|
+
export interface ToolsCodingForgeConfig {
|
|
91
|
+
/** `env:NAME` or `keyring:NAME`; a literal fails config validation. */
|
|
92
|
+
readonly token: string;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Shared repository limits for `grep` and `glob`. */
|
|
96
|
+
export type ToolsCodingRepositoryConfig = RepositoryLimitOptions;
|
|
97
|
+
|
|
98
|
+
/** Validated `packageConfig` for `@arnilo/prism-tools/coding`. */
|
|
99
|
+
export interface ToolsCodingConfig {
|
|
100
|
+
/** The workspace root the file tools operate in. Required; checked in `setup` with a naming error. */
|
|
101
|
+
readonly root: string;
|
|
102
|
+
/** Extra roots that the `coding.targets` service (Task 4) reports alongside `root`. */
|
|
103
|
+
readonly roots?: readonly string[];
|
|
104
|
+
/** Registers only `read`, `grep`, and `glob`. Default `false`. */
|
|
105
|
+
readonly readOnly?: boolean;
|
|
106
|
+
readonly shell?: ToolsCodingShellConfig;
|
|
107
|
+
readonly repository?: ToolsCodingRepositoryConfig;
|
|
108
|
+
/** Structured git tools. Default `{ enabled: true }`. */
|
|
109
|
+
readonly git?: ToolsCodingGitConfig;
|
|
110
|
+
/** Language servers for the opt-in `diagnostics` tool. Default: none. */
|
|
111
|
+
readonly language?: ToolsCodingLanguageConfig;
|
|
112
|
+
/** Named check commands for `coding_check`. No checks means no tool. */
|
|
113
|
+
readonly checks?: Readonly<Record<string, NamedCheckDefinition>>;
|
|
114
|
+
/** `todo_write` plus the `run.end` continuation. Default `{ enabled: true, continueOnOpen: true }`. */
|
|
115
|
+
readonly todo?: ToolsCodingTodoConfig;
|
|
116
|
+
/** Principal durable-effect tools require; when absent they fail closed. */
|
|
117
|
+
readonly principal?: ToolsCodingPrincipalConfig;
|
|
118
|
+
/** Forge credential ref; resolved only through `prism.credentials.resolve`. */
|
|
119
|
+
readonly forge?: ToolsCodingForgeConfig;
|
|
120
|
+
/** Shell/file backend. Default `"none"` (local). `docker` fails closed without host wiring. */
|
|
121
|
+
readonly sandbox?: CodingSandboxKind;
|
|
122
|
+
/** Hard-deny command rules for `shell`; a `deny` decision wins over any approval. */
|
|
123
|
+
readonly commandRules?: readonly ToolsCodingCommandRule[];
|
|
124
|
+
/** Egress allow-list for sandboxed shells. */
|
|
125
|
+
readonly egress?: ToolsCodingEgressConfig;
|
|
126
|
+
/** Disposable git worktrees for delegated coding work. Default `{ enabled: true }`. */
|
|
127
|
+
readonly workspace?: ToolsCodingWorkspaceConfig;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const repositoryProperties: Readonly<Record<string, JsonSchema>> = {
|
|
131
|
+
maxDepth: { type: "number", minimum: 1 },
|
|
132
|
+
maxEntries: { type: "number", minimum: 1 },
|
|
133
|
+
maxFiles: { type: "number", minimum: 1 },
|
|
134
|
+
maxResults: { type: "number", minimum: 1 },
|
|
135
|
+
maxConcurrency: { type: "number", minimum: 1 },
|
|
136
|
+
maxScanBytes: { type: "number", minimum: 1 },
|
|
137
|
+
maxFileBytes: { type: "number", minimum: 1 },
|
|
138
|
+
maxMatches: { type: "number", minimum: 1 },
|
|
139
|
+
maxPatternBytes: { type: "number", minimum: 1 },
|
|
140
|
+
maxLineBytes: { type: "number", minimum: 1 },
|
|
141
|
+
maxContextLines: { type: "number", minimum: 0 },
|
|
142
|
+
maxTimeMs: { type: "number", minimum: 1 },
|
|
143
|
+
exclude: { type: "array", items: { type: "string", minLength: 1 } },
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* The config schema. `root` is deliberately not declared `required`: a required field with no
|
|
148
|
+
* default would fail package conformance, so `setup` checks it and names the field instead
|
|
149
|
+
* (plan 152 Task 6's config rule).
|
|
150
|
+
*/
|
|
151
|
+
export const toolsCodingConfigSchema: JsonSchema = {
|
|
152
|
+
type: "object",
|
|
153
|
+
properties: {
|
|
154
|
+
root: { type: "string", minLength: 1, maxLength: 4096 },
|
|
155
|
+
roots: { type: "array", items: { type: "string", minLength: 1, maxLength: 4096 }, maxItems: 64 },
|
|
156
|
+
readOnly: { type: "boolean", default: false },
|
|
157
|
+
shell: {
|
|
158
|
+
type: "object",
|
|
159
|
+
properties: {
|
|
160
|
+
enabled: { type: "boolean", default: true },
|
|
161
|
+
timeoutMs: { type: "number", minimum: 1, maximum: 3_600_000 },
|
|
162
|
+
background: {
|
|
163
|
+
type: "object",
|
|
164
|
+
properties: {
|
|
165
|
+
maxProcesses: { type: "number", minimum: 1, maximum: 16, default: 4 },
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
},
|
|
170
|
+
language: {
|
|
171
|
+
type: "object",
|
|
172
|
+
properties: {
|
|
173
|
+
servers: {
|
|
174
|
+
type: "object",
|
|
175
|
+
additionalProperties: {
|
|
176
|
+
type: "object",
|
|
177
|
+
properties: {
|
|
178
|
+
command: { type: "string", minLength: 1, maxLength: 4096 },
|
|
179
|
+
args: { type: "array", maxItems: 64, items: { type: "string", maxLength: 4096 } },
|
|
180
|
+
languages: { type: "array", minItems: 1, maxItems: 32, items: { type: "string", minLength: 1, maxLength: 64 } },
|
|
181
|
+
env: { type: "object", additionalProperties: { type: "string", maxLength: 4096 } },
|
|
182
|
+
},
|
|
183
|
+
required: ["command", "languages"],
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
repository: { type: "object", properties: repositoryProperties },
|
|
189
|
+
git: {
|
|
190
|
+
type: "object",
|
|
191
|
+
properties: { enabled: { type: "boolean", default: true } },
|
|
192
|
+
},
|
|
193
|
+
checks: {
|
|
194
|
+
type: "object",
|
|
195
|
+
additionalProperties: {
|
|
196
|
+
type: "object",
|
|
197
|
+
properties: {
|
|
198
|
+
file: { type: "string", minLength: 1, maxLength: 4096 },
|
|
199
|
+
args: { type: "array", maxItems: 256, items: { type: "string", maxLength: 4096 } },
|
|
200
|
+
cwd: { type: "string", minLength: 1, maxLength: 4096 },
|
|
201
|
+
env: { type: "object", additionalProperties: { type: "string", maxLength: 4096 } },
|
|
202
|
+
timeoutMs: { type: "number", minimum: 1, maximum: 3_600_000 },
|
|
203
|
+
},
|
|
204
|
+
required: ["file", "args"],
|
|
205
|
+
},
|
|
206
|
+
},
|
|
207
|
+
todo: {
|
|
208
|
+
type: "object",
|
|
209
|
+
properties: {
|
|
210
|
+
enabled: { type: "boolean", default: true },
|
|
211
|
+
continueOnOpen: { type: "boolean", default: true },
|
|
212
|
+
},
|
|
213
|
+
},
|
|
214
|
+
principal: {
|
|
215
|
+
type: "object",
|
|
216
|
+
properties: { id: { type: "string", minLength: 1, maxLength: 256 } },
|
|
217
|
+
},
|
|
218
|
+
forge: {
|
|
219
|
+
type: "object",
|
|
220
|
+
properties: {
|
|
221
|
+
token: {
|
|
222
|
+
type: "string",
|
|
223
|
+
minLength: 1,
|
|
224
|
+
maxLength: 4096,
|
|
225
|
+
"x-prism-secret": true,
|
|
226
|
+
description: "An `env:` or `keyring:` ref the credentials slot resolves. A literal token is rejected.",
|
|
227
|
+
},
|
|
228
|
+
},
|
|
229
|
+
},
|
|
230
|
+
sandbox: { type: "string", enum: ["none", "native", "docker", "e2b"], default: "none" },
|
|
231
|
+
workspace: {
|
|
232
|
+
type: "object",
|
|
233
|
+
properties: {
|
|
234
|
+
enabled: { type: "boolean", default: true },
|
|
235
|
+
worktreeRoot: { type: "string", minLength: 1, maxLength: 4096 },
|
|
236
|
+
maxWorktrees: { type: "number", minimum: 1, maximum: 16 },
|
|
237
|
+
diff: {
|
|
238
|
+
type: "object",
|
|
239
|
+
properties: {
|
|
240
|
+
maxFiles: { type: "number", minimum: 1, maximum: 2000 },
|
|
241
|
+
maxBytes: { type: "number", minimum: 1, maximum: 8_388_608 },
|
|
242
|
+
},
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
},
|
|
246
|
+
commandRules: {
|
|
247
|
+
type: "array",
|
|
248
|
+
maxItems: 256,
|
|
249
|
+
items: {
|
|
250
|
+
type: "object",
|
|
251
|
+
properties: {
|
|
252
|
+
match: { type: "string", minLength: 1, maxLength: 4096 },
|
|
253
|
+
decision: { type: "string", enum: ["allow", "deny", "requireApproval"] },
|
|
254
|
+
reason: { type: "string", maxLength: 512 },
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
egress: {
|
|
259
|
+
type: "object",
|
|
260
|
+
properties: {
|
|
261
|
+
allow: {
|
|
262
|
+
type: "array",
|
|
263
|
+
maxItems: 1024,
|
|
264
|
+
items: {
|
|
265
|
+
type: "object",
|
|
266
|
+
properties: {
|
|
267
|
+
host: { type: "string", minLength: 1, maxLength: 253 },
|
|
268
|
+
port: { type: "number", minimum: 1, maximum: 65_535 },
|
|
269
|
+
protocol: { type: "string", enum: ["http", "https"] },
|
|
270
|
+
allowPrivate: { type: "boolean", default: false },
|
|
271
|
+
},
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
presets: { type: "array", maxItems: 8, items: { type: "string", enum: ["npm-registry", "github"] } },
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
},
|
|
278
|
+
};
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hard denies for this package's own tools, through one `tool.call` decide handler.
|
|
3
|
+
*
|
|
4
|
+
* The kernel's combination rule for `tool.call` is any-deny-wins, so these holds under
|
|
5
|
+
* `approval` `auto`, under a host `allow` hook, and from a custom loop. Reasons name the root
|
|
6
|
+
* and the tool, never file contents. Tools this package did not register are ignored. For
|
|
7
|
+
* read-effect tools, `targets.roots` also carries each skill directory; mutating tools never do.
|
|
8
|
+
*/
|
|
9
|
+
import { realpath } from "node:fs/promises";
|
|
10
|
+
import { isAbsolute, relative, resolve } from "node:path";
|
|
11
|
+
import type { ToolCallDecision, ToolCallHookPayload } from "@arnilo/prism";
|
|
12
|
+
import { type CommandRule, evaluateCommandRules } from "./security/command-rules.js";
|
|
13
|
+
import { realpathNearest } from "./security/path-containment.js";
|
|
14
|
+
import { CODING_TOOL_NAMES, type CodingTargetsFn } from "./targets.js";
|
|
15
|
+
|
|
16
|
+
/** Tools that change state; denied when `readOnly` is set. */
|
|
17
|
+
const MUTATING_TOOLS: ReadonlySet<string> = new Set(["write", "edit", "delete", "move", "shell"]);
|
|
18
|
+
|
|
19
|
+
export interface HardDenyOptions {
|
|
20
|
+
readonly targets: CodingTargetsFn;
|
|
21
|
+
readonly readOnly: boolean;
|
|
22
|
+
readonly commandRules?: readonly CommandRule[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isPathInside(root: string, target: string): boolean {
|
|
26
|
+
const rel = relative(root, target);
|
|
27
|
+
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A containment check that realpaths the roots once and then costs one `realpath` per path
|
|
32
|
+
* argument, with no directory walk. Fails closed when a root or the target cannot be resolved.
|
|
33
|
+
*/
|
|
34
|
+
function createContainmentChecker(roots: readonly string[]): (target: string) => Promise<boolean> {
|
|
35
|
+
let realRoots: Promise<readonly string[]> | undefined;
|
|
36
|
+
const loadRoots = () => {
|
|
37
|
+
realRoots ??= Promise.all(
|
|
38
|
+
roots.map(async (root) => {
|
|
39
|
+
try {
|
|
40
|
+
return await realpath(resolve(root));
|
|
41
|
+
} catch {
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
44
|
+
}),
|
|
45
|
+
).then((resolved) => resolved.filter((root): root is string => root !== undefined));
|
|
46
|
+
return realRoots;
|
|
47
|
+
};
|
|
48
|
+
return async (target: string) => {
|
|
49
|
+
const resolved = await loadRoots();
|
|
50
|
+
if (resolved.length === 0) return false;
|
|
51
|
+
const to = await realpathNearest(target);
|
|
52
|
+
return to !== undefined && resolved.some((root) => isPathInside(root, to));
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The `tool.call` decide handler. Returns `{ deny }` or `undefined`; never throws. */
|
|
57
|
+
export function createHardDenyHook(options: HardDenyOptions) {
|
|
58
|
+
// Targets hand back the same frozen roots array while worktree and skill roots are unchanged, so
|
|
59
|
+
// the roots are realpathed once per array instead of once per decision.
|
|
60
|
+
const checkers = new WeakMap<readonly string[], (target: string) => Promise<boolean>>();
|
|
61
|
+
const checkerFor = (roots: readonly string[]) => {
|
|
62
|
+
let checker = checkers.get(roots);
|
|
63
|
+
if (checker === undefined) {
|
|
64
|
+
checker = createContainmentChecker(roots);
|
|
65
|
+
checkers.set(roots, checker);
|
|
66
|
+
}
|
|
67
|
+
return checker;
|
|
68
|
+
};
|
|
69
|
+
return async (payload: ToolCallHookPayload): Promise<ToolCallDecision | undefined> => {
|
|
70
|
+
if (!CODING_TOOL_NAMES.has(payload.name)) return undefined;
|
|
71
|
+
const targets = options.targets(payload.name, payload.args, { sessionId: payload.sessionId });
|
|
72
|
+
if (!targets) return undefined;
|
|
73
|
+
|
|
74
|
+
if (options.readOnly && MUTATING_TOOLS.has(payload.name)) {
|
|
75
|
+
return { deny: `${payload.name} denied: @arnilo/prism-tools/coding is read-only` };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (targets.paths.length > 0) {
|
|
79
|
+
const contained = checkerFor(targets.roots);
|
|
80
|
+
for (const path of targets.paths) {
|
|
81
|
+
if (!(await contained(path))) {
|
|
82
|
+
return { deny: `${payload.name} denied: path outside coding roots (${targets.roots.join(", ")}): ${path}` };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (payload.name === "shell") {
|
|
88
|
+
const command = typeof payload.args.command === "string" ? payload.args.command : "";
|
|
89
|
+
const evaluation = evaluateCommandRules(command, options.commandRules, { denyMetacharacters: false });
|
|
90
|
+
if (evaluation.action === "deny") {
|
|
91
|
+
return { deny: `shell denied: ${evaluation.reason ?? "command matches a deny rule"}` };
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return undefined;
|
|
96
|
+
};
|
|
97
|
+
}
|