@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,73 @@
|
|
|
1
|
+
import type { JsonSchema } from "@arnilo/prism";
|
|
2
|
+
|
|
3
|
+
import type { OpenApiLimits } from "./limits.js";
|
|
4
|
+
|
|
5
|
+
/** One OpenAPI 3.1 document the package compiles into tools. */
|
|
6
|
+
export interface ToolsOpenapiSpecConfig {
|
|
7
|
+
/** Tool-name prefix: every operation compiles as `<id>_<operationId>`. */
|
|
8
|
+
readonly id: string;
|
|
9
|
+
/** Path to a local JSON document, or an `https:` URL fetched once at setup through pinned fetch. */
|
|
10
|
+
readonly source: string;
|
|
11
|
+
/** Pinned origin for the compiled operations. Defaults to the document's first absolute `servers[0].url`. */
|
|
12
|
+
readonly baseUrl?: string;
|
|
13
|
+
/** Header name → `env:NAME` or `keyring:NAME`. Resolved per request; a literal fails config validation. */
|
|
14
|
+
readonly headers?: Readonly<Record<string, string>>;
|
|
15
|
+
/** Allow-listed operationIds. Omit to compile every operation the document declares. */
|
|
16
|
+
readonly include?: readonly string[];
|
|
17
|
+
/** operationIds to skip when `include` is absent. */
|
|
18
|
+
readonly exclude?: readonly string[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Validated `packageConfig` for `@arnilo/prism-tools/openapi`. */
|
|
22
|
+
export interface ToolsOpenapiConfig {
|
|
23
|
+
/** Required; an empty list registers nothing and notes why. */
|
|
24
|
+
readonly specs?: readonly ToolsOpenapiSpecConfig[];
|
|
25
|
+
/** Byte, retry, pagination, and body bounds shared by every spec. */
|
|
26
|
+
readonly limits?: OpenApiLimits;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const secretString: JsonSchema = {
|
|
30
|
+
type: "string",
|
|
31
|
+
minLength: 1,
|
|
32
|
+
maxLength: 4096,
|
|
33
|
+
pattern: "^(env|keyring):",
|
|
34
|
+
"x-prism-secret": true,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const limitProperties: Readonly<Record<string, JsonSchema>> = {
|
|
38
|
+
maxDocumentBytes: { type: "number", minimum: 1 },
|
|
39
|
+
maxOperations: { type: "number", minimum: 1 },
|
|
40
|
+
maxSchemaDepth: { type: "number", minimum: 1 },
|
|
41
|
+
maxRefs: { type: "number", minimum: 1 },
|
|
42
|
+
maxBodyBytes: { type: "number", minimum: 1 },
|
|
43
|
+
maxResponseBytes: { type: "number", minimum: 1 },
|
|
44
|
+
maxPages: { type: "number", minimum: 1 },
|
|
45
|
+
maxPaginationItems: { type: "number", minimum: 1 },
|
|
46
|
+
maxRetries: { type: "number", minimum: 0 },
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The config schema. `specs` is deliberately not declared `required`: a required field with no
|
|
51
|
+
* default would fail package conformance, so `setup` checks it and names the field instead.
|
|
52
|
+
* `headers` values are ref-only secret fields; a literal fails validation.
|
|
53
|
+
*/
|
|
54
|
+
export const toolsOpenapiConfigSchema: JsonSchema = {
|
|
55
|
+
type: "object",
|
|
56
|
+
properties: {
|
|
57
|
+
specs: {
|
|
58
|
+
type: "array",
|
|
59
|
+
items: {
|
|
60
|
+
type: "object",
|
|
61
|
+
properties: {
|
|
62
|
+
id: { type: "string", minLength: 1, maxLength: 64, pattern: "^[A-Za-z0-9][A-Za-z0-9._-]*$" },
|
|
63
|
+
source: { type: "string", minLength: 1, maxLength: 4096 },
|
|
64
|
+
baseUrl: { type: "string", minLength: 1, maxLength: 4096 },
|
|
65
|
+
headers: { type: "object", additionalProperties: secretString },
|
|
66
|
+
include: { type: "array", items: { type: "string", minLength: 1 } },
|
|
67
|
+
exclude: { type: "array", items: { type: "string", minLength: 1 } },
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
limits: { type: "object", properties: limitProperties },
|
|
72
|
+
},
|
|
73
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export type OpenApiToolErrorCode =
|
|
2
|
+
| "ERR_PRISM_OPENAPI_DOCUMENT_BOUNDS"
|
|
3
|
+
| "ERR_PRISM_OPENAPI_OPERATION_UNKNOWN"
|
|
4
|
+
| "ERR_PRISM_OPENAPI_SERVER_DRIFT"
|
|
5
|
+
| "ERR_PRISM_OPENAPI_SCHEMA_BOUNDS"
|
|
6
|
+
| "ERR_PRISM_OPENAPI_RESPONSE_BOUNDS"
|
|
7
|
+
| "ERR_PRISM_OPENAPI_RETRY_EXHAUSTED";
|
|
8
|
+
|
|
9
|
+
export class OpenApiToolError extends Error {
|
|
10
|
+
readonly code: OpenApiToolErrorCode;
|
|
11
|
+
constructor(code: OpenApiToolErrorCode, message: string) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.name = "OpenApiToolError";
|
|
14
|
+
this.code = code;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
import { UNTRUSTED_EXTERNAL_TRUST, type JsonObject, type SecretRedactor, type ToolExecutionContext, type ToolResult } from "@arnilo/prism";
|
|
2
|
+
import type { CompiledOperation } from "./compile.js";
|
|
3
|
+
import { OpenApiToolError } from "./errors.js";
|
|
4
|
+
import type { ResolvedOpenApiLimits } from "./limits.js";
|
|
5
|
+
import type { OpenApiCredentialResolver, OpenApiPagination, OpenApiPolicyCheck } from "./types.js";
|
|
6
|
+
|
|
7
|
+
export interface ExecuteOpenApiOperationOptions {
|
|
8
|
+
readonly operation: CompiledOperation;
|
|
9
|
+
readonly server: string;
|
|
10
|
+
readonly toolName: string;
|
|
11
|
+
readonly args: JsonObject;
|
|
12
|
+
readonly context: ToolExecutionContext;
|
|
13
|
+
readonly credentials?: OpenApiCredentialResolver;
|
|
14
|
+
readonly policy?: OpenApiPolicyCheck;
|
|
15
|
+
readonly redactor?: SecretRedactor;
|
|
16
|
+
readonly pagination?: OpenApiPagination;
|
|
17
|
+
readonly idempotencyKeyHeader?: boolean;
|
|
18
|
+
readonly limits: ResolvedOpenApiLimits;
|
|
19
|
+
readonly fetchImpl: typeof globalThis.fetch;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function buildRequestUrl(
|
|
23
|
+
operation: CompiledOperation,
|
|
24
|
+
server: string,
|
|
25
|
+
args: JsonObject,
|
|
26
|
+
credentialQuery: Readonly<Record<string, string>> | undefined,
|
|
27
|
+
): string {
|
|
28
|
+
let path = operation.path;
|
|
29
|
+
for (const name of operation.pathParams) {
|
|
30
|
+
const value = args[name];
|
|
31
|
+
if (value === undefined || value === null) {
|
|
32
|
+
throw new OpenApiToolError("ERR_PRISM_OPENAPI_SCHEMA_BOUNDS", `missing path parameter: ${name}`);
|
|
33
|
+
}
|
|
34
|
+
path = path.replace(`{${name}}`, encodeURIComponent(String(value)));
|
|
35
|
+
}
|
|
36
|
+
const url = new URL(`${server.replace(/\/+$/, "")}${path}`);
|
|
37
|
+
for (const param of operation.queryParams) {
|
|
38
|
+
const value = args[param.name];
|
|
39
|
+
if (value === undefined || value === null) continue;
|
|
40
|
+
url.searchParams.set(param.name, String(value));
|
|
41
|
+
}
|
|
42
|
+
if (credentialQuery) {
|
|
43
|
+
for (const [key, value] of Object.entries(credentialQuery)) url.searchParams.set(key, value);
|
|
44
|
+
}
|
|
45
|
+
return url.href;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function buildHeaders(
|
|
49
|
+
operation: CompiledOperation,
|
|
50
|
+
args: JsonObject,
|
|
51
|
+
credentialHeaders: Readonly<Record<string, string>> | undefined,
|
|
52
|
+
idempotencyKey: string | undefined,
|
|
53
|
+
): Record<string, string> {
|
|
54
|
+
const headers: Record<string, string> = {};
|
|
55
|
+
for (const param of operation.headerParams) {
|
|
56
|
+
const value = args[param.name];
|
|
57
|
+
if (value === undefined || value === null) continue;
|
|
58
|
+
headers[param.name] = String(value);
|
|
59
|
+
}
|
|
60
|
+
if (operation.bodyParam && args[operation.bodyParam.name] !== undefined) headers["content-type"] = "application/json";
|
|
61
|
+
if (idempotencyKey !== undefined && operation.effect.kind !== "none") headers["idempotency-key"] = idempotencyKey;
|
|
62
|
+
if (credentialHeaders) {
|
|
63
|
+
for (const [key, value] of Object.entries(credentialHeaders)) headers[key] = value;
|
|
64
|
+
}
|
|
65
|
+
return headers;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function buildBody(operation: CompiledOperation, args: JsonObject, maxBodyBytes: number): string | undefined {
|
|
69
|
+
if (!operation.bodyParam) return undefined;
|
|
70
|
+
const value = args[operation.bodyParam.name];
|
|
71
|
+
if (value === undefined) return undefined;
|
|
72
|
+
const text = JSON.stringify(value);
|
|
73
|
+
if (text === undefined) {
|
|
74
|
+
throw new OpenApiToolError("ERR_PRISM_OPENAPI_RESPONSE_BOUNDS", "request body is not JSON-serializable");
|
|
75
|
+
}
|
|
76
|
+
if (Buffer.byteLength(text, "utf8") > maxBodyBytes) {
|
|
77
|
+
throw new OpenApiToolError("ERR_PRISM_OPENAPI_RESPONSE_BOUNDS", "request body exceeds maxBodyBytes");
|
|
78
|
+
}
|
|
79
|
+
return text;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function readBoundedText(response: Response, maxBytes: number): Promise<string> {
|
|
83
|
+
if (!response.body) return "";
|
|
84
|
+
const reader = response.body.getReader();
|
|
85
|
+
const chunks: Uint8Array[] = [];
|
|
86
|
+
let total = 0;
|
|
87
|
+
for (;;) {
|
|
88
|
+
const { done, value } = await reader.read();
|
|
89
|
+
if (done) break;
|
|
90
|
+
total += value.byteLength;
|
|
91
|
+
if (total > maxBytes) {
|
|
92
|
+
await reader.cancel();
|
|
93
|
+
throw new OpenApiToolError("ERR_PRISM_OPENAPI_RESPONSE_BOUNDS", "response exceeds maxResponseBytes");
|
|
94
|
+
}
|
|
95
|
+
chunks.push(value);
|
|
96
|
+
}
|
|
97
|
+
return Buffer.concat(chunks).toString("utf8");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function pointer(record: Record<string, unknown>, path: string): unknown {
|
|
101
|
+
let current: unknown = record;
|
|
102
|
+
for (const part of path.split(".")) {
|
|
103
|
+
if (typeof current !== "object" || current === null) return undefined;
|
|
104
|
+
current = (current as Record<string, unknown>)[part];
|
|
105
|
+
}
|
|
106
|
+
return current;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function nextToken(record: Record<string, unknown>, nextPath: string | undefined): string | undefined {
|
|
110
|
+
const paths = nextPath ? [nextPath] : ["next", "nextPageToken"];
|
|
111
|
+
for (const path of paths) {
|
|
112
|
+
const value = pointer(record, path);
|
|
113
|
+
if (typeof value === "string" && value) return value;
|
|
114
|
+
}
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function result(options: ExecuteOpenApiOperationOptions, value: unknown): ToolResult {
|
|
119
|
+
// No banner here: the kernel wraps untrusted results (UNTRUSTED_EXTERNAL_TRUST) in its envelope.
|
|
120
|
+
return {
|
|
121
|
+
toolCallId: options.context.toolCallId,
|
|
122
|
+
name: options.toolName,
|
|
123
|
+
value,
|
|
124
|
+
metadata: {
|
|
125
|
+
trust: UNTRUSTED_EXTERNAL_TRUST,
|
|
126
|
+
source: openApiSource(options),
|
|
127
|
+
operationId: options.operation.operationId,
|
|
128
|
+
method: options.operation.method,
|
|
129
|
+
path: options.operation.path,
|
|
130
|
+
},
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** The request URL without query or credentials — enough for the envelope's `source` attribute. */
|
|
135
|
+
function openApiSource(options: ExecuteOpenApiOperationOptions): string | undefined {
|
|
136
|
+
const source = `${options.server.replace(/\/+$/, "")}${options.operation.path}`;
|
|
137
|
+
try {
|
|
138
|
+
const { protocol } = new URL(source);
|
|
139
|
+
return protocol === "http:" || protocol === "https:" ? source : undefined;
|
|
140
|
+
} catch {
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function executeOpenApiOperation(options: ExecuteOpenApiOperationOptions): Promise<ToolResult> {
|
|
146
|
+
const { operation, context, limits } = options;
|
|
147
|
+
await options.policy?.(
|
|
148
|
+
{ operationId: operation.operationId, method: operation.method, path: operation.path, args: options.args },
|
|
149
|
+
context,
|
|
150
|
+
);
|
|
151
|
+
const credential = await options.credentials?.(
|
|
152
|
+
{ operationId: operation.operationId, method: operation.method, path: operation.path },
|
|
153
|
+
context,
|
|
154
|
+
);
|
|
155
|
+
const url = buildRequestUrl(operation, options.server, options.args, credential?.query);
|
|
156
|
+
const headers = buildHeaders(
|
|
157
|
+
operation,
|
|
158
|
+
options.args,
|
|
159
|
+
credential?.headers,
|
|
160
|
+
options.idempotencyKeyHeader === true ? context.idempotencyKey : undefined,
|
|
161
|
+
);
|
|
162
|
+
const body = buildBody(operation, options.args, limits.maxBodyBytes);
|
|
163
|
+
|
|
164
|
+
const fetchPage = async (pageUrl: string, pageHeaders: Record<string, string>, pageBody: string | undefined): Promise<Response> => {
|
|
165
|
+
for (let attempt = 0; ; attempt++) {
|
|
166
|
+
let response: Response;
|
|
167
|
+
try {
|
|
168
|
+
response = await options.fetchImpl(pageUrl, {
|
|
169
|
+
method: operation.method,
|
|
170
|
+
headers: pageHeaders,
|
|
171
|
+
body: pageBody,
|
|
172
|
+
signal: context.signal,
|
|
173
|
+
redirect: "manual",
|
|
174
|
+
});
|
|
175
|
+
} catch (error) {
|
|
176
|
+
if (context.signal?.aborted) throw error;
|
|
177
|
+
if (attempt < limits.maxRetries) continue;
|
|
178
|
+
throw new OpenApiToolError(
|
|
179
|
+
"ERR_PRISM_OPENAPI_RETRY_EXHAUSTED",
|
|
180
|
+
`request failed after ${limits.maxRetries} retries: ${String(error)}`,
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
if (response.status >= 500 && attempt < limits.maxRetries) continue;
|
|
184
|
+
return response;
|
|
185
|
+
}
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
if (options.pagination && operation.paginatable) {
|
|
189
|
+
const { pageParam, pageSizeParam, pageSize, nextPath, itemsPath } = options.pagination;
|
|
190
|
+
const items: unknown[] = [];
|
|
191
|
+
let token: string | undefined;
|
|
192
|
+
let pageCount = 0;
|
|
193
|
+
for (;;) {
|
|
194
|
+
pageCount += 1;
|
|
195
|
+
if (pageCount > limits.maxPages) {
|
|
196
|
+
throw new OpenApiToolError("ERR_PRISM_OPENAPI_RESPONSE_BOUNDS", "pagination exceeded maxPages");
|
|
197
|
+
}
|
|
198
|
+
const pageUrl = new URL(url);
|
|
199
|
+
if (token !== undefined) pageUrl.searchParams.set(pageParam, token);
|
|
200
|
+
if (pageSizeParam !== undefined && pageSize !== undefined) pageUrl.searchParams.set(pageSizeParam, String(pageSize));
|
|
201
|
+
const response = await fetchPage(pageUrl.href, headers, body);
|
|
202
|
+
const text = await readBoundedText(response, limits.maxResponseBytes);
|
|
203
|
+
const safe = options.redactor ? options.redactor.redact(text) : text;
|
|
204
|
+
let parsed: unknown;
|
|
205
|
+
try {
|
|
206
|
+
parsed = JSON.parse(safe);
|
|
207
|
+
} catch {
|
|
208
|
+
throw new OpenApiToolError("ERR_PRISM_OPENAPI_RESPONSE_BOUNDS", "pagination response is not JSON");
|
|
209
|
+
}
|
|
210
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
211
|
+
throw new OpenApiToolError("ERR_PRISM_OPENAPI_RESPONSE_BOUNDS", "pagination response must be a JSON object");
|
|
212
|
+
}
|
|
213
|
+
const record = parsed as Record<string, unknown>;
|
|
214
|
+
const pageItems = pointer(record, itemsPath ?? "items");
|
|
215
|
+
if (!Array.isArray(pageItems)) {
|
|
216
|
+
throw new OpenApiToolError("ERR_PRISM_OPENAPI_RESPONSE_BOUNDS", `pagination items path ${itemsPath ?? "items"} is not an array`);
|
|
217
|
+
}
|
|
218
|
+
items.push(...pageItems);
|
|
219
|
+
if (items.length > limits.maxPaginationItems) {
|
|
220
|
+
throw new OpenApiToolError("ERR_PRISM_OPENAPI_RESPONSE_BOUNDS", "pagination exceeded maxPaginationItems");
|
|
221
|
+
}
|
|
222
|
+
const next = nextToken(record, nextPath);
|
|
223
|
+
if (next === undefined) return result(options, { status: response.status, pageCount, items });
|
|
224
|
+
token = next;
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const response = await fetchPage(url, headers, body);
|
|
229
|
+
const text = await readBoundedText(response, limits.maxResponseBytes);
|
|
230
|
+
const safe = options.redactor ? options.redactor.redact(text) : text;
|
|
231
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
232
|
+
let value: unknown = safe;
|
|
233
|
+
if (/json/i.test(contentType)) {
|
|
234
|
+
try {
|
|
235
|
+
value = JSON.parse(safe);
|
|
236
|
+
} catch {
|
|
237
|
+
value = safe;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return result(options, { status: response.status, body: value });
|
|
241
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@arnilo/prism-tools/openapi` — compile host-selected OpenAPI 3.1 operations into bounded tools.
|
|
3
|
+
*
|
|
4
|
+
* Config lists `specs`; each spec is read once at setup (a local JSON file, or an `https:` URL
|
|
5
|
+
* through the kernel's pinned fetch) and compiled into `<id>_<operationId>` tools. GET/HEAD
|
|
6
|
+
* operations declare `["network"]`; every other method declares `["network", "external"]`. A spec
|
|
7
|
+
* that fails to load or compile notes why and is skipped, and the other specs still register.
|
|
8
|
+
*
|
|
9
|
+
* Header values are ref-only (`env:`/`keyring:`) and resolved per request through
|
|
10
|
+
* `prism.credentials.resolve`, so the resolved value joins the agent's secret set and is redacted
|
|
11
|
+
* from events, notes, logs, errors, and tool results.
|
|
12
|
+
*
|
|
13
|
+
* ```ts
|
|
14
|
+
* import toolsOpenapi from "@arnilo/prism-tools/openapi";
|
|
15
|
+
* import { createAgent } from "@arnilo/prism";
|
|
16
|
+
*
|
|
17
|
+
* const agent = await createAgent({
|
|
18
|
+
* model,
|
|
19
|
+
* packages: [toolsOpenapi({ specs: [{ id: "petstore", source: "./petstore.json" }] })],
|
|
20
|
+
* });
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { definePackage, type PackageAPI, type PackageInstance, type ToolDefinition } from "@arnilo/prism";
|
|
25
|
+
import manifest from "../../package.json" with { type: "json" };
|
|
26
|
+
|
|
27
|
+
import { type ToolsOpenapiConfig, type ToolsOpenapiSpecConfig, toolsOpenapiConfigSchema } from "./config.js";
|
|
28
|
+
import { OpenApiToolError } from "./errors.js";
|
|
29
|
+
import { resolveOpenApiLimits } from "./limits.js";
|
|
30
|
+
import { firstServerUrl, loadSpecDocument, selectOperations } from "./specs.js";
|
|
31
|
+
import { createOpenApiTools } from "./tools.js";
|
|
32
|
+
import type { OpenApiCredentialResolver } from "./types.js";
|
|
33
|
+
|
|
34
|
+
export type { ToolsOpenapiConfig, ToolsOpenapiSpecConfig } from "./config.js";
|
|
35
|
+
export { toolsOpenapiConfigSchema } from "./config.js";
|
|
36
|
+
export { OpenApiToolError, type OpenApiToolErrorCode } from "./errors.js";
|
|
37
|
+
export type { OpenApiLimits, ResolvedOpenApiLimits } from "./limits.js";
|
|
38
|
+
export { DEFAULT_OPENAPI_LIMITS, HARD_OPENAPI_LIMITS, resolveOpenApiLimits } from "./limits.js";
|
|
39
|
+
export { firstServerUrl, listOperationIds, loadSpecDocument, selectOperations } from "./specs.js";
|
|
40
|
+
export { createOpenApiTools, effectsForMethod } from "./tools.js";
|
|
41
|
+
export type {
|
|
42
|
+
OpenApiCredentialInput,
|
|
43
|
+
OpenApiCredentialResolver,
|
|
44
|
+
OpenApiPagination,
|
|
45
|
+
OpenApiPolicyCheck,
|
|
46
|
+
OpenApiToolsOptions,
|
|
47
|
+
} from "./types.js";
|
|
48
|
+
|
|
49
|
+
function errorMessage(error: unknown): string {
|
|
50
|
+
return error instanceof Error ? error.message : String(error);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** A per-request resolver over one spec's ref-only header config. */
|
|
54
|
+
function createHeaderResolver(prism: PackageAPI<ToolsOpenapiConfig>, spec: ToolsOpenapiSpecConfig): OpenApiCredentialResolver | undefined {
|
|
55
|
+
const entries = Object.entries(spec.headers ?? {});
|
|
56
|
+
if (entries.length === 0) return undefined;
|
|
57
|
+
return async () => {
|
|
58
|
+
const headers: Record<string, string> = {};
|
|
59
|
+
for (const [name, ref] of entries) {
|
|
60
|
+
const secret = await prism.credentials.resolve(ref);
|
|
61
|
+
if (secret === undefined) {
|
|
62
|
+
throw new OpenApiToolError("ERR_PRISM_OPENAPI_DOCUMENT_BOUNDS", `unresolved credential ref for header ${name}`);
|
|
63
|
+
}
|
|
64
|
+
headers[name] = secret.value;
|
|
65
|
+
}
|
|
66
|
+
return { headers };
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const definition = definePackage<ToolsOpenapiConfig>({
|
|
71
|
+
name: "@arnilo/prism-tools/openapi",
|
|
72
|
+
version: manifest.version,
|
|
73
|
+
description:
|
|
74
|
+
"OpenAPI 3.1 operation compiler: pinned-origin, allow-listed, schema-bounded tools with per-method effects and ref-only header credentials.",
|
|
75
|
+
requires: { api: 1 },
|
|
76
|
+
capabilities: ["credentials"],
|
|
77
|
+
config: toolsOpenapiConfigSchema,
|
|
78
|
+
setup: async (prism) => {
|
|
79
|
+
const specs = prism.config.specs ?? [];
|
|
80
|
+
if (specs.length === 0) {
|
|
81
|
+
prism.status.note("warn", "openapi: config needs specs (at least one { id, source })");
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const limits = resolveOpenApiLimits(prism.config.limits);
|
|
85
|
+
for (const spec of specs) {
|
|
86
|
+
let tools: readonly ToolDefinition[];
|
|
87
|
+
try {
|
|
88
|
+
const document = await loadSpecDocument(spec, limits);
|
|
89
|
+
const server = spec.baseUrl ?? firstServerUrl(document);
|
|
90
|
+
if (server === undefined) {
|
|
91
|
+
throw new OpenApiToolError("ERR_PRISM_OPENAPI_SERVER_DRIFT", `spec ${spec.id} has no absolute servers url; set baseUrl`);
|
|
92
|
+
}
|
|
93
|
+
const operations = selectOperations(document, spec);
|
|
94
|
+
if (operations.length === 0) {
|
|
95
|
+
prism.status.note("warn", `openapi: spec ${spec.id} matched no operations`);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
tools = createOpenApiTools({
|
|
99
|
+
document,
|
|
100
|
+
operations,
|
|
101
|
+
server,
|
|
102
|
+
namePrefix: `${spec.id}_`,
|
|
103
|
+
limits,
|
|
104
|
+
credentials: createHeaderResolver(prism, spec),
|
|
105
|
+
});
|
|
106
|
+
} catch (error) {
|
|
107
|
+
prism.status.note("warn", `openapi: spec ${spec.id} skipped: ${errorMessage(error)}`);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
// Registration stays outside the try: a cross-spec tool-name collision fails closed.
|
|
111
|
+
for (const tool of tools) prism.tools.register(tool);
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
/** The package definition. Pass config through `packageConfig` or use {@link toolsOpenapi}. */
|
|
117
|
+
export default definition;
|
|
118
|
+
|
|
119
|
+
/** The package with config baked in, for `packages: [toolsOpenapi({ specs })]`. */
|
|
120
|
+
export function toolsOpenapi(config: ToolsOpenapiConfig): PackageInstance {
|
|
121
|
+
return definition(config);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export const packageName = "@arnilo/prism-tools/openapi";
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { OpenApiToolError } from "./errors.js";
|
|
2
|
+
|
|
3
|
+
export interface OpenApiLimits {
|
|
4
|
+
readonly maxDocumentBytes?: number;
|
|
5
|
+
readonly maxOperations?: number;
|
|
6
|
+
readonly maxSchemaDepth?: number;
|
|
7
|
+
readonly maxRefs?: number;
|
|
8
|
+
readonly maxBodyBytes?: number;
|
|
9
|
+
readonly maxResponseBytes?: number;
|
|
10
|
+
readonly maxPages?: number;
|
|
11
|
+
readonly maxPaginationItems?: number;
|
|
12
|
+
readonly maxRetries?: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface ResolvedOpenApiLimits {
|
|
16
|
+
readonly maxDocumentBytes: number;
|
|
17
|
+
readonly maxOperations: number;
|
|
18
|
+
readonly maxSchemaDepth: number;
|
|
19
|
+
readonly maxRefs: number;
|
|
20
|
+
readonly maxBodyBytes: number;
|
|
21
|
+
readonly maxResponseBytes: number;
|
|
22
|
+
readonly maxPages: number;
|
|
23
|
+
readonly maxPaginationItems: number;
|
|
24
|
+
readonly maxRetries: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const DEFAULT_OPENAPI_LIMITS: ResolvedOpenApiLimits = {
|
|
28
|
+
maxDocumentBytes: 2 * 1024 * 1024,
|
|
29
|
+
maxOperations: 256,
|
|
30
|
+
maxSchemaDepth: 32,
|
|
31
|
+
maxRefs: 1024,
|
|
32
|
+
maxBodyBytes: 1024 * 1024,
|
|
33
|
+
maxResponseBytes: 1024 * 1024,
|
|
34
|
+
maxPages: 20,
|
|
35
|
+
maxPaginationItems: 1000,
|
|
36
|
+
maxRetries: 0,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const HARD_OPENAPI_LIMITS: ResolvedOpenApiLimits = {
|
|
40
|
+
maxDocumentBytes: 16 * 1024 * 1024,
|
|
41
|
+
maxOperations: 1024,
|
|
42
|
+
maxSchemaDepth: 128,
|
|
43
|
+
maxRefs: 8192,
|
|
44
|
+
maxBodyBytes: 16 * 1024 * 1024,
|
|
45
|
+
maxResponseBytes: 16 * 1024 * 1024,
|
|
46
|
+
maxPages: 100,
|
|
47
|
+
maxPaginationItems: 10000,
|
|
48
|
+
maxRetries: 3,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export function resolveOpenApiLimits(input: OpenApiLimits = {}): ResolvedOpenApiLimits {
|
|
52
|
+
const resolved = { ...DEFAULT_OPENAPI_LIMITS, ...input };
|
|
53
|
+
for (const [key, value] of Object.entries(resolved)) {
|
|
54
|
+
const min = key === "maxRetries" ? 0 : 1;
|
|
55
|
+
if (!Number.isSafeInteger(value) || value < min) {
|
|
56
|
+
throw new OpenApiToolError("ERR_PRISM_OPENAPI_DOCUMENT_BOUNDS", `openapi limit ${key} must be a safe integer >= ${min}`);
|
|
57
|
+
}
|
|
58
|
+
if (value > HARD_OPENAPI_LIMITS[key as keyof ResolvedOpenApiLimits]) {
|
|
59
|
+
throw new OpenApiToolError("ERR_PRISM_OPENAPI_DOCUMENT_BOUNDS", `openapi limit ${key} exceeds its hard cap`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return resolved;
|
|
63
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import { type JsonObject } from "@arnilo/prism";
|
|
4
|
+
import { pinnedFetch } from "@arnilo/prism/net";
|
|
5
|
+
|
|
6
|
+
import type { ToolsOpenapiSpecConfig } from "./config.js";
|
|
7
|
+
import { OpenApiToolError } from "./errors.js";
|
|
8
|
+
import type { ResolvedOpenApiLimits } from "./limits.js";
|
|
9
|
+
|
|
10
|
+
const HTTP_METHODS = ["get", "put", "post", "delete", "options", "head", "patch", "trace"] as const;
|
|
11
|
+
|
|
12
|
+
function isHttpUrl(value: string): boolean {
|
|
13
|
+
return /^https?:\/\//i.test(value);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Read one spec once at setup: a local JSON file, or an `https:` URL through the kernel's
|
|
18
|
+
* DNS-pinned, redirect-free, byte-bounded fetch. YAML documents are not supported.
|
|
19
|
+
*/
|
|
20
|
+
export async function loadSpecDocument(spec: ToolsOpenapiSpecConfig, limits: ResolvedOpenApiLimits): Promise<JsonObject> {
|
|
21
|
+
let text: string;
|
|
22
|
+
if (isHttpUrl(spec.source)) {
|
|
23
|
+
const response = await pinnedFetch(new URL(spec.source), undefined, {
|
|
24
|
+
maxResponseBytes: limits.maxDocumentBytes,
|
|
25
|
+
errorPrefix: "OpenAPI spec",
|
|
26
|
+
});
|
|
27
|
+
text = await response.text();
|
|
28
|
+
} else {
|
|
29
|
+
const data = readFileSync(resolve(spec.source));
|
|
30
|
+
if (data.byteLength > limits.maxDocumentBytes) {
|
|
31
|
+
throw new OpenApiToolError("ERR_PRISM_OPENAPI_DOCUMENT_BOUNDS", `spec ${spec.id} exceeds maxDocumentBytes`);
|
|
32
|
+
}
|
|
33
|
+
text = data.toString("utf8");
|
|
34
|
+
}
|
|
35
|
+
let parsed: unknown;
|
|
36
|
+
try {
|
|
37
|
+
parsed = JSON.parse(text);
|
|
38
|
+
} catch {
|
|
39
|
+
throw new OpenApiToolError("ERR_PRISM_OPENAPI_DOCUMENT_BOUNDS", `spec ${spec.id} is not valid JSON`);
|
|
40
|
+
}
|
|
41
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
42
|
+
throw new OpenApiToolError("ERR_PRISM_OPENAPI_DOCUMENT_BOUNDS", `spec ${spec.id} must be a JSON object`);
|
|
43
|
+
}
|
|
44
|
+
return parsed as JsonObject;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** The first absolute `servers[].url`, used as the pinned origin when `baseUrl` is absent. */
|
|
48
|
+
export function firstServerUrl(document: JsonObject): string | undefined {
|
|
49
|
+
const servers = document.servers;
|
|
50
|
+
if (!Array.isArray(servers)) return undefined;
|
|
51
|
+
for (const entry of servers) {
|
|
52
|
+
const url = typeof entry === "object" && entry !== null ? (entry as Record<string, unknown>).url : undefined;
|
|
53
|
+
if (typeof url !== "string" || url.length === 0) continue;
|
|
54
|
+
try {
|
|
55
|
+
const parsed = new URL(url);
|
|
56
|
+
if (parsed.protocol === "https:" || parsed.protocol === "http:") return url;
|
|
57
|
+
} catch {
|
|
58
|
+
// Relative server URLs resolve against the pinned base, so they cannot supply one.
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Every `operationId` the document declares, in path/method order. */
|
|
65
|
+
export function listOperationIds(document: JsonObject): string[] {
|
|
66
|
+
const paths = document.paths;
|
|
67
|
+
if (typeof paths !== "object" || paths === null || Array.isArray(paths)) return [];
|
|
68
|
+
const ids: string[] = [];
|
|
69
|
+
for (const pathItem of Object.values(paths as Record<string, unknown>)) {
|
|
70
|
+
if (typeof pathItem !== "object" || pathItem === null) continue;
|
|
71
|
+
for (const method of HTTP_METHODS) {
|
|
72
|
+
const operation = (pathItem as Record<string, unknown>)[method];
|
|
73
|
+
if (typeof operation !== "object" || operation === null) continue;
|
|
74
|
+
const operationId = (operation as Record<string, unknown>).operationId;
|
|
75
|
+
if (typeof operationId === "string" && operationId.length > 0) ids.push(operationId);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return ids;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** `include` wins; otherwise every operation minus `exclude`. */
|
|
82
|
+
export function selectOperations(document: JsonObject, spec: ToolsOpenapiSpecConfig): string[] {
|
|
83
|
+
if (spec.include !== undefined) return [...spec.include];
|
|
84
|
+
const excluded = new Set(spec.exclude ?? []);
|
|
85
|
+
return listOperationIds(document).filter((id) => !excluded.has(id));
|
|
86
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import type { ToolCapabilityKind, ToolDefinition } from "@arnilo/prism";
|
|
2
|
+
import { compileOpenApiDocument } from "./compile.js";
|
|
3
|
+
import { executeOpenApiOperation } from "./execute.js";
|
|
4
|
+
import { resolveOpenApiLimits } from "./limits.js";
|
|
5
|
+
import type { OpenApiToolsOptions } from "./types.js";
|
|
6
|
+
|
|
7
|
+
const READ_EFFECTS: readonly ToolCapabilityKind[] = ["network"];
|
|
8
|
+
const MUTATING_EFFECTS: readonly ToolCapabilityKind[] = ["network", "external"];
|
|
9
|
+
|
|
10
|
+
/** GET/HEAD reach the network only; every other method also mutates an external system. */
|
|
11
|
+
export function effectsForMethod(method: string): readonly ToolCapabilityKind[] {
|
|
12
|
+
return method === "GET" || method === "HEAD" ? READ_EFFECTS : MUTATING_EFFECTS;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Compile host-selected OpenAPI 3.1 operations into bounded Prism `ToolDefinition`s.
|
|
17
|
+
*
|
|
18
|
+
* Only the explicitly listed `operationIds` compile; the pinned `server` origin must
|
|
19
|
+
* match every document/path/operation `servers` entry; schemas are resolved with
|
|
20
|
+
* depth/ref bounds and no external refs; mutating methods carry
|
|
21
|
+
* `{ kind: "external_mutation", idempotency: "required" }` so the core run loop gates
|
|
22
|
+
* approval and deduplicates via the effect store. Responses are untrusted data.
|
|
23
|
+
*/
|
|
24
|
+
export function createOpenApiTools(options: OpenApiToolsOptions): readonly ToolDefinition[] {
|
|
25
|
+
const limits = resolveOpenApiLimits(options.limits);
|
|
26
|
+
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
27
|
+
const prefix = options.namePrefix ?? "";
|
|
28
|
+
const compiled = compileOpenApiDocument({
|
|
29
|
+
document: options.document,
|
|
30
|
+
operations: options.operations,
|
|
31
|
+
server: options.server,
|
|
32
|
+
limits,
|
|
33
|
+
pagination: options.pagination,
|
|
34
|
+
});
|
|
35
|
+
return compiled.map((operation) => {
|
|
36
|
+
const toolName = `${prefix}${operation.toolName}`;
|
|
37
|
+
return {
|
|
38
|
+
name: toolName,
|
|
39
|
+
description: operation.description,
|
|
40
|
+
parameters: operation.parameters,
|
|
41
|
+
effect: operation.effect,
|
|
42
|
+
effects: effectsForMethod(operation.method),
|
|
43
|
+
execute: (args, context) =>
|
|
44
|
+
executeOpenApiOperation({
|
|
45
|
+
operation,
|
|
46
|
+
server: options.server,
|
|
47
|
+
toolName,
|
|
48
|
+
args,
|
|
49
|
+
context,
|
|
50
|
+
credentials: options.credentials,
|
|
51
|
+
policy: options.policy,
|
|
52
|
+
redactor: options.redactor,
|
|
53
|
+
pagination: options.pagination,
|
|
54
|
+
idempotencyKeyHeader: options.idempotencyKeyHeader,
|
|
55
|
+
limits,
|
|
56
|
+
fetchImpl,
|
|
57
|
+
}),
|
|
58
|
+
} satisfies ToolDefinition;
|
|
59
|
+
});
|
|
60
|
+
}
|