From 059ae49cab8190bc91e5c499d9b75235ac8f58e5 Mon Sep 17 00:00:00 2001 From: Devin Foley Date: Thu, 22 Oct 2026 15:14:26 -0700 Subject: [PATCH] feat(connections): add Unikraft Cloud connection (#17139) ## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Apps connections give agents governed access to external services through vault credentials, grants, action policies, the tool gateway, and audit. > - Unikraft Cloud runs unikernel micro-VMs, services, and volumes across regional metros, and it offers a sandbox plugin for commands and files. > - Unikraft is not in the catalog. It has no hosted MCP server, so the generic "Connect your own MCP server" flow cannot reach it. > - Daytona (#16483) added a native REST pattern for this case: a catalog generated from the provider's published OpenAPI specifications and executed through the gateway. > - This pull request adds Unikraft on that pattern and moves the shared native-provider code into one registry, so the shared connection code does not name each provider. > - The benefit is governed agent access to all published Unikraft actions, with the token kept in the vault and sent only to published Unikraft hosts. ## Linked Issues or Issue Description Refs #16483 **Problem or motivation** Teams that run workloads on Unikraft Cloud want their agents to list instances, read logs, deploy and scale services, and run commands in sandboxes. Unikraft is not in the Apps catalog. Unikraft has no hosted MCP server, so operators cannot connect it through the remote MCP flow. **Proposed solution** Add a native Unikraft connection. Generate its actions from Unikraft's official OpenAPI specifications for the platform API and the sandbox plugin. Pin the specifications to a reviewed commit. The operator connects an API token. The token stays in the vault and goes only to the five published metro hosts. Every action takes an optional metro. Actions without a metro use the connection's default metro, which the operator can change under Advanced. **Alternatives considered** A catalog-only AppDefinition was not possible because no hosted MCP endpoint exists. Wrapping the `unikraft` CLI was rejected because it would expose host filesystem behavior and CLI login state. A second copy of the Daytona bridge was rejected; the shared transport and catalog contract are now one module with a provider registry. **Roadmap alignment** This extends the existing Apps and Connections provider coverage. It uses the existing credential, grant, policy, gateway, and audit model. It does not overlap planned core work. ## What Changed - Added `scripts/generate-unikraft-tools.mjs` and its tests. The generator reads the platform and sandbox plugin specifications at commit `5dd107ea1cd8716b83cb86a83ed2a6ee46f99b5b`, resolves shared parameters, records source SHA-256 hashes, and writes `server/src/services/unikraft/operations.generated.json`. - The catalog has 105 actions: 89 platform operations and 16 sandbox plugin operations. The server-sent audit event stream is recorded in the inventory and is not advertised as an action. - Classification: GET operations and sandbox reads and waits are reads. Delete, stop, suspend, detach, unpin, and sandbox command execution, signals, and stdin are destructive. All other operations are writes. - Added `server/src/services/connectors/unikraft/client.ts` and its generated catalog. It validates arguments against the generated schemas, accepts only published metros, rejects path traversal, sends JSON bodies on GET and DELETE as the specification requires, and verifies the token with `GET /v1/users/quotas`. A sandbox wait that times out (documented HTTP 408) returns a still-running result instead of a failure. - Moved the bounded, credential-redacting HTTP sender and the catalog contract helpers from `daytona.ts` to `native-rest-client.ts`. Added `native-provider-catalogs.ts`. `tool-access.ts`, the `tool-access/catalog` and `token-broker` modules, `connectors/tool-policy.ts`, and `tool-gateway.ts` now use this registry instead of Daytona-specific checks. Daytona behavior does not change. - Added the Unikraft definition source, the generated definition, the brand mark and manifest entry, and the permission review (`liveProof: not-run`). - Added `doc/connections/UNIKRAFT.md` with a sequence diagram, endpoints, setup, actions, and limits. Updated the connections README, the permission audit, and both capability maps. - Tests: definition shape, database-backed gateway reads, writes, deletes, and sandbox commands for personal and shared grants, default and per-call metros, rejection of unpublished metros and traversal, Off policies, revocation, stale-contract blocking, credential export denial, schema validation for all actions, pinned-transport tests for GET and DELETE bodies, and a browser setup check. ## Verification - `pnpm exec vitest run packages/shared/src server/src/__tests__/unikraft-connection.test.ts server/src/__tests__/unikraft-http-boundary.test.ts server/src/__tests__/daytona-connection.test.ts server/src/__tests__/daytona-http-boundary.test.ts server/src/__tests__/tool-access-service.test.ts server/src/__tests__/tool-gateway-service.test.ts server/src/__tests__/tool-gateway.test.ts ui/src/pages/apps/AppsConnect.test.tsx ui/src/pages/apps/Browse.test.tsx ui/src/lib/app-brand-assets.test.ts` plus `connector-tool-policy` and `tool-catalog-publication`: all passed after the rebase onto the split tool-access modules. - `node --test scripts/generate-unikraft-tools.test.mjs scripts/generate-daytona-tools.test.mjs scripts/app-brand-validation.test.mjs`: 20 passed. - `node scripts/check-app-brand-assets.mjs`, `node scripts/check-maps.mjs`, and `git diff --check`: passed. - `pnpm --filter @paperclipai/shared typecheck`, `pnpm --filter @paperclipai/server typecheck`, and `pnpm --filter @paperclipai/ui typecheck`: clean. - `npx playwright test --config tests/e2e/playwright.config.ts tests/e2e/unikraft-connector.spec.ts tests/e2e/daytona-connector.spec.ts`: 2 passed. - `node scripts/generate-unikraft-tools.mjs` regenerates the committed catalog with the recorded hashes. - Manual: on a local instance, open Connectors, search for Unikraft, and select **Add key**. Confirm the icon, the password field, the key guidance, and the default metro select under **Change**. - Not verified: a real Unikraft account. Live provider read and write proof is pending. ## Risks - A Unikraft token acts for the whole organization in every metro. Unikraft offers no narrower token permissions. Assign only trusted agents and review action permissions. - Sandbox actions run arbitrary commands inside the selected instance. They are classified as destructive. - A timed-out write can still complete at the provider. The bridge does not retry writes automatically. - New metros fail closed until the specification is regenerated and reviewed. Legacy `*.kraft.cloud` hosts are not reachable. - The Daytona refactor moves code without behavior changes. All Daytona tests pass without edits. - The generated JSON snapshot is large because it holds the provider input contracts. No migration or lockfile change is required. ## Model Used - Claude Opus 5.5 (Anthropic), model ID `claude-opus-5-5`, 1M context window, used through Claude Code with tool use (shell, file editing, web research, browser tests). ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --- doc/connections/CONNECTOR-PERMISSION-AUDIT.md | 3 +- doc/connections/README.md | 2 +- doc/connections/UNIKRAFT.md | 186 + .../tool-method-permission-reviews.json | 18 + maps/agent-access-map/app-permissions.md | 6 + maps/agent-access-map/connection-setup.md | 7 + maps/feature-map/app-permissions.md | 6 + maps/feature-map/connection-setup.md | 7 + .../shared/src/app-definitions.generated.ts | 5 +- packages/shared/src/app-definitions.test.ts | 34 +- packages/shared/src/app-definitions.ts | 1 + .../shared/src/app-definitions/unikraft.json | 95 + .../app-definition-overrides/unikraft.json | 76 + scripts/generate-daytona-tools.mjs | 2 +- scripts/generate-unikraft-tools.mjs | 135 + scripts/generate-unikraft-tools.test.mjs | 61 + .../__tests__/connector-tool-policy.test.ts | 2 +- .../src/__tests__/unikraft-connection.test.ts | 198 + .../__tests__/unikraft-http-boundary.test.ts | 69 + server/src/services/connectors/tool-policy.ts | 13 +- .../services/connectors/unikraft/client.ts | 108 + .../unikraft/operations.generated.json | 11837 ++++++++++++++++ server/src/services/daytona.ts | 136 +- .../src/services/native-provider-catalogs.ts | 50 + server/src/services/native-rest-client.ts | 168 + server/src/services/tool-access.ts | 6 +- .../tool-access/catalog/descriptors.ts | 4 +- .../services/tool-access/catalog/discovery.ts | 9 +- .../services/tool-access/catalog/health.ts | 8 +- .../tool-access/catalog/publication.ts | 12 +- .../services/tool-access/catalog/reader.ts | 22 +- .../tool-access/token-broker/service.ts | 5 +- server/src/services/tool-gateway.ts | 20 +- tests/e2e/unikraft-connector.spec.ts | 26 + ui/public/brands/apps/manifest.json | 6 + ui/public/brands/apps/unikraft.svg | 1 + 36 files changed, 13170 insertions(+), 174 deletions(-) create mode 100644 doc/connections/UNIKRAFT.md create mode 100644 packages/shared/src/app-definitions/unikraft.json create mode 100644 scripts/app-definition-overrides/unikraft.json create mode 100644 scripts/generate-unikraft-tools.mjs create mode 100644 scripts/generate-unikraft-tools.test.mjs create mode 100644 server/src/__tests__/unikraft-connection.test.ts create mode 100644 server/src/__tests__/unikraft-http-boundary.test.ts create mode 100644 server/src/services/connectors/unikraft/client.ts create mode 100644 server/src/services/connectors/unikraft/operations.generated.json create mode 100644 server/src/services/native-provider-catalogs.ts create mode 100644 server/src/services/native-rest-client.ts create mode 100644 tests/e2e/unikraft-connector.spec.ts create mode 100644 ui/public/brands/apps/unikraft.svg diff --git a/doc/connections/CONNECTOR-PERMISSION-AUDIT.md b/doc/connections/CONNECTOR-PERMISSION-AUDIT.md index e5ccec2a711..9c70b98e7ca 100644 --- a/doc/connections/CONNECTOR-PERMISSION-AUDIT.md +++ b/doc/connections/CONNECTOR-PERMISSION-AUDIT.md @@ -11,7 +11,8 @@ ledger; the counts above are not restated. Later additions: [Neon](./NEON.md) (`mcp-oauth`, `mcp-api-key`; 2026-10-02), [Superagent](./SUPERAGENT.md) (`mcp-api-key`; 2026-10-06), [Gauge](./GAUGE.md) (`mcp-oauth`, `mcp-api-key`; 2026-10-08), [Ditto](./DITTO.md) -(`mcp-oauth`, `mcp-api-key`; 2026-10-10). +(`mcp-oauth`, `mcp-api-key`; 2026-10-10), [Unikraft](./UNIKRAFT.md) +(`api-token`; 2026-10-15). ## Shared credential failure diff --git a/doc/connections/README.md b/doc/connections/README.md index 5a06e5a4856..bb9022ce2e5 100644 --- a/doc/connections/README.md +++ b/doc/connections/README.md @@ -14,7 +14,7 @@ Long-term memory: [Experimental memory connectors](./MEMORY.md). Provider notes: [Google Workspace](./GOOGLE-WORKSPACE.md), [Gmail](./GMAIL.md), [Asana](./ASANA.md), [Sentry](./SENTRY.md), [PostHog](./POSTHOG.md), [Braintrust](./BRAINTRUST.md), [Neon](./NEON.md), [Superagent](./SUPERAGENT.md), [Gauge](./GAUGE.md), [Ditto](./DITTO.md), -[AgentMail](./AGENTMAIL.md), [Telem.AI](./TELEM.md), [iMessage Photon](./IMESSAGE-PHOTON.md), and +[Unikraft](./UNIKRAFT.md), [AgentMail](./AGENTMAIL.md), [Telem.AI](./TELEM.md), [iMessage Photon](./IMESSAGE-PHOTON.md), and [Enterpret](./ENTERPRET.md). Optional credential custody: [Vercel Connect](./VERCEL-CONNECT.md). diff --git a/doc/connections/UNIKRAFT.md b/doc/connections/UNIKRAFT.md new file mode 100644 index 00000000000..a8706e2e06a --- /dev/null +++ b/doc/connections/UNIKRAFT.md @@ -0,0 +1,186 @@ +# Unikraft connection + +Unikraft Cloud runs unikernel micro-VMs ("instances") across regional metros. +Its connection uses the normal Apps connection, selected personal/company +credential, agent installation, action policy, gateway, revocation and audit +paths. The API token stays in the instance vault. The connector does not borrow +`unikraft login` state, `UKC_TOKEN` environment variables, or another +connection's credential. + +Sign-in tokens are never reused as resource tokens; id.paperclip.ing never +stores resource tokens; no connections hub on the ID service. + +## Vendor + +| Item | Value | +| --- | --- | +| Product | Unikraft Cloud (formerly KraftCloud) | +| API reference | https://unikraft.com/docs/api/platform/v1 | +| Specification | https://github.com/unikraft-cloud/openapi (`platform.json`, `plugins/sandbox.json`) | +| Metros | https://unikraft.com/docs/platform/metros | +| Tokens | https://unikraft.com/docs/sdks/overview#authentication | +| Console | https://console.unikraft.com | +| Hosted MCP server | None. Unikraft's MCP guides describe hosting your own MCP server on Unikraft. | + +## Transport and auth + +| Axis | Value | +| --- | --- | +| Transport | `rest_api`, executed by Paperclip's native provider bridge | +| Authentication | `api_key`, sent as `Authorization: Bearer ` | +| OAuth client ownership | Not applicable; Unikraft offers no third-party OAuth | +| Credential source | Paperclip vault | +| Grant identity | `organization` or `user` | + +Unikraft Cloud has no global platform endpoint. Each metro serves its own API at +`https://api..unikraft.cloud/v1/...`. The reviewed specification +publishes five metros: `dal`, `fra`, `sfo`, `sin` and `was`. Every action accepts +an optional `metro` argument limited to that list; actions that omit it use the +connection's default metro (Frankfurt unless changed under **Advanced**). The +token is never sent to any other host. Legacy `*.kraft.cloud` hosts, which +Unikraft documents as nearing end of life, are not reachable through this +connection. + +The saved connection URL, `https://controlplane.unikraft.cloud/v1`, identifies +the connection only. Actions use the metro hosts above. + +## Connection flow (mandatory) + +```mermaid +sequenceDiagram + actor Operator + participant UI as Paperclip UI + participant Server as Paperclip server + participant Vault as Instance vault + participant Metro as Unikraft metro API + actor Agent + Operator->>UI: Connectors, Unikraft, Add key + UI->>Server: POST /api/companies/{companyId}/tools/apps/connect (galleryKey unikraft, token, optional default metro) + Server->>Vault: Store the token as a company or user secret + Server->>Metro: GET /v1/users/quotas with the stored token + Metro-->>Server: status success + Server-->>UI: Generated catalog of 105 actions + Operator->>UI: Choose agents and action permissions + Agent->>Server: Gateway tool call, for example unikraft_get_instances + Server->>Server: Check grant, revocation, policy and saved contract + Server->>Metro: Request to api.{metro}.unikraft.cloud with the vaulted token + Metro-->>Server: JSON result + Server-->>Agent: Result with the token redacted +``` + +Paperclip endpoints used: `GET /api/companies/{companyId}/tools/apps/unikraft/preflight`, +`POST /api/companies/{companyId}/tools/apps/connect`, +`POST /api/companies/{companyId}/tools/apps/{connectionId}/finish`, +`GET /api/tool-connections/{connectionId}/catalog`, and the run-bound tool gateway. +There is no OAuth handoff, so `GET /api/tools/oauth/callback` is not used. + +## Administrator setup (mandatory) + +1. Sign in to the [Unikraft Cloud console](https://console.unikraft.com) and + create an API token for the organization agents should manage. +2. In Paperclip choose **Connectors → Unikraft → Add key**, choose who can use + the credential, and paste the token. +3. Optional: under **Advanced**, change the default metro. +4. On the saved connection's **Permissions** screen, choose agents and set + actions to Allowed, Ask first, or Off. + +A token acts for its whole organization in every metro. Unikraft does not offer +narrower token permissions, so agent assignment and action permissions enforce +the capability boundary. Revoke a token in the Unikraft console, then remove or +reconnect the Paperclip connection. + +## Resource filters + +There are no provider-enforced resource filters. `requiredResourceFilters` +(`organization`, `metro`, `instance`) is reviewed metadata. The bridge enforces +only that requests go to a published metro and that path arguments cannot +traverse to another route. + +## Actions + +The catalog contains **105 actions** generated from Unikraft's published +specifications at commit `5dd107ea1cd8716b83cb86a83ed2a6ee46f99b5b`: +89 platform API operations and 16 sandbox plugin operations. + +| Group | Examples | Classification | +| --- | --- | --- | +| Reads, logs, metrics, history and waits | `unikraft_get_instances`, `unikraft_get_instance_logs_by_uuid`, `unikraft_wait_instances`, `unikraft_sandbox_read_file` | Read | +| Creation, updates, start, attach, clone and pinning | `unikraft_create_instance`, `unikraft_update_service_group_by_uuid`, `unikraft_start_instances`, `unikraft_sandbox_write_file` | Write | +| Deletion, stop, suspend, detach, unpin, and sandbox command execution, signals and stdin | `unikraft_delete_volume_by_uuid`, `unikraft_stop_instances`, `unikraft_sandbox_run_command` | Destructive | + +Platform actions keep Unikraft's operation names (`unikraft_`). +Sandbox plugin actions are `unikraft_sandbox_` and take an +`instance` (name or UUID); they call +`https://api..unikraft.cloud/v1/instances//plugins/sandbox`. +Many Unikraft operations select batch targets with a JSON body, including on +GET and DELETE; the bridge sends those bodies as published. +`unikraft_sandbox_wait_for_command_with_timeout` returns +`{ "status": "running", "timedOut": true }` when Unikraft reports that the +command is still running at the timeout (HTTP 408), so an agent can wait again. +The bridge lives in `server/src/services/connectors/unikraft/`. + +The audit event stream (`GET /v1/audit`) is server-sent events, not a +request/response action. It is recorded in the generated inventory and not +advertised as a tool. + +The specification sources and SHA-256 hashes are recorded in +`server/src/services/connectors/unikraft/operations.generated.json`. Regenerate with: + +```sh +node scripts/generate-unikraft-tools.mjs +node --test scripts/generate-unikraft-tools.test.mjs +``` + +The generator pins a reviewed commit because the specification repository has +no tags or releases. Updating it requires explicit regeneration and catalog +review: saved connections quarantine new and changed actions until reviewed. + +## Wizard path + +The ordinary key wizard: **Access → Connect**. Setup asks only for the token. +The default metro is an optional select under **Advanced**. Setup completes when +`GET /v1/users/quotas` on the default metro confirms the token. + +## Governance defaults + +- Risk tier S4: the normal catalog includes deletion and arbitrary command + execution inside sandboxes. +- Every discovered action is enabled when setup finishes, under the shared + connection access rules; operators restrict actions on Permissions. +- New and changed actions after a catalog update are quarantined until reviewed. +- Agents cannot export the token; generic credential export rejects Unikraft. +- Responses and request bodies are bounded at 8 MiB. Each action has one caller + deadline, capped at 30 seconds. Mutations are never retried automatically; a + timed-out write can still complete at the provider. + +## Manifest + +Defined in `scripts/app-definition-overrides/unikraft.json` and generated into +`packages/shared/src/app-definitions/unikraft.json`: slug `unikraft`, category +`developer`, one method `api-token` (`rest_api`, `api_key`, `customer`), and a +`metro` select tenant field. + +## Validation hook + +Database-backed tests exercise company and personal vault ownership, real +run-scoped gateway reads, writes, deletions and sandbox commands, default and +per-call metros, rejection of unpublished metros and path traversal, Off +policies, revocation, stale-contract blocking, credential export denial, all +generated argument schemas, and bounded/sanitized errors. HTTP boundary tests +send GET and DELETE JSON bodies through the real pinned transport to a local +server. Generator tests cover streaming endpoints, shared parameter references, +unsigned integer formats and risk classification. + +These fixtures do not qualify a real Unikraft account. Live provider proof is +pending an operator connecting a token and trying an allowed read and write +(`liveProof: not-run`). + +## Brand + +`ui/public/brands/apps/unikraft.svg` is Unikraft's gradient mark from the +official documentation site, copied from +`https://raw.githubusercontent.com/unikraft-cloud/docs/d8acfcefe8186badac84d5308d641a1c1b5b239e/public/unikraft.svg` +with editor metadata removed. One asset serves both themes. Unikraft publishes +no brand kit. + +Provider research verified 2026-10-15. diff --git a/doc/connections/tool-method-permission-reviews.json b/doc/connections/tool-method-permission-reviews.json index f5a516146b9..3d83685bbb9 100644 --- a/doc/connections/tool-method-permission-reviews.json +++ b/doc/connections/tool-method-permission-reviews.json @@ -2879,6 +2879,24 @@ "liveProof": "not-run", "evidenceLimit": "Public provider documentation and OAuth metadata only. Account-bound connect, discovery, read/write execution, run-scoped gateway, refresh/reconnect, removal, and secret handling remain to be qualified for this method.", "keyPermissions": "Create a Braintrust API key in Settings → API keys for the account and organization whose data agents should access. The key must authorize the reads and writes you intend to allow; Paperclip cannot increase or narrow its provider permissions. Evaluations and Topics processing can incur costs. Dataset edits can permanently delete rows; automation activation can enable retention or delivery." + }, + { + "app": "unikraft", + "method": "api-token", + "auth": "api_key", + "policy": "provider-key", + "requestedScopes": [], + "capability": "write", + "supportedActions": "All published Unikraft Cloud platform API operations (instances, checkpoints, templates, service groups, autoscale, volumes, images, certificates, and quotas) and sandbox plugin operations (commands and files) in every published metro. The audit event stream is not exposed.", + "evidence": [ + "https://unikraft.com/docs/sdks/overview#authentication", + "https://unikraft.com/docs/platform/metros", + "https://raw.githubusercontent.com/unikraft-cloud/openapi/5dd107ea1cd8716b83cb86a83ed2a6ee46f99b5b/platform.json", + "https://raw.githubusercontent.com/unikraft-cloud/openapi/5dd107ea1cd8716b83cb86a83ed2a6ee46f99b5b/plugins/sandbox.json" + ], + "reviewedAt": "2026-10-15", + "liveProof": "not-run", + "keyPermissions": "Create an API token in the Unikraft Cloud console. A token acts for its whole organization in every metro, including running sandbox commands and deleting resources. Unikraft does not offer narrower token permissions, so choose trusted agents and use action permissions to limit what they can do." } ] } diff --git a/maps/agent-access-map/app-permissions.md b/maps/agent-access-map/app-permissions.md index d3cacb64ece..7183dcb59bf 100644 --- a/maps/agent-access-map/app-permissions.md +++ b/maps/agent-access-map/app-permissions.md @@ -131,6 +131,12 @@ and stale same-name contracts before refresh; new/changed tools stay quarantined exercise this boundary through real run-bound gateway sessions with fixture provider responses. They do not qualify a live Daytona account or agent model. +Unikraft uses the same native-catalog contract: stale same-name saved entries are +rejected before refresh and new or changed actions stay quarantined. Its +[database-backed fixtures](../../server/src/__tests__/unikraft-connection.test.ts) +cover this through real run-bound gateway sessions with fixture provider +responses, not a live Unikraft account. + ## `test-action` Capability ID: `app-permissions/test-action`. diff --git a/maps/agent-access-map/connection-setup.md b/maps/agent-access-map/connection-setup.md index c800966e8f8..36b34ed7d92 100644 --- a/maps/agent-access-map/connection-setup.md +++ b/maps/agent-access-map/connection-setup.md @@ -137,6 +137,13 @@ gateway; they cannot export the key. [Database-backed fixtures](../../server/src cover selected vault ownership and gateway reads/writes with provider responses replaced by fixtures. Real provider authentication remains unqualified. +Unikraft native setup uses an organization API token in the selected personal or +company vault grant. It does not borrow `unikraft login` state or `UKC_TOKEN`. +Agents use assigned provider tools through the run-bound gateway, limited to the +published metro hosts, and cannot export the token. [Database-backed fixtures](../../server/src/__tests__/unikraft-connection.test.ts) +cover vault ownership and gateway reads, writes and sandbox commands with +fixture provider responses. Real provider authentication remains unqualified. + ## `handoff` Capability ID: `connection-setup/handoff`. diff --git a/maps/feature-map/app-permissions.md b/maps/feature-map/app-permissions.md index 025b6660d8e..02705f8c62c 100644 --- a/maps/feature-map/app-permissions.md +++ b/maps/feature-map/app-permissions.md @@ -85,6 +85,12 @@ remain in place. [Daytona regressions](../../server/src/__tests__/daytona-connec cover shared/personal catalog upgrades, failed refresh, permission preservation, run-bound file transfers, and revocation with fixture provider responses. +Unikraft actions follow the same review rules: deletion, stop/suspend/detach and +sandbox command execution are destructive, and catalog updates quarantine new or +changed actions. [Unikraft regressions](../../server/src/__tests__/unikraft-connection.test.ts) +cover classification, Off policies, revocation and stale contracts with fixture +provider responses. + ## Gotchas - Connected does not imply an agent has access or an action is allowed. diff --git a/maps/feature-map/connection-setup.md b/maps/feature-map/connection-setup.md index 679a090e769..0485d87af1a 100644 --- a/maps/feature-map/connection-setup.md +++ b/maps/feature-map/connection-setup.md @@ -137,6 +137,13 @@ account-bound proof. Live token expiry/refresh, remote OAuth revocation, and native CLI automatic capability delivery remain unqualified; public metadata and fixtures do not establish those results. +Unikraft follows the same key/access wizard. Setup asks only for the API token; +the default metro is an optional select under Advanced, and each action can name +another published metro. [Unikraft browser setup](../../tests/e2e/unikraft-connector.spec.ts) +checks the catalog, password field, access controls and the advanced metro. It +does not submit a token or qualify a real provider account. See +[Unikraft](../../doc/connections/UNIKRAFT.md) for token authority and metros. + ### `mcp-link` Automated: AppsConnect's generic MCP cases cover probe-driven auth, private or diff --git a/packages/shared/src/app-definitions.generated.ts b/packages/shared/src/app-definitions.generated.ts index 77f51bb2b8b..8d4e0208511 100644 --- a/packages/shared/src/app-definitions.generated.ts +++ b/packages/shared/src/app-definitions.generated.ts @@ -105,6 +105,7 @@ import a103 from "./app-definitions/local.json" with { type: "json" }; import a104 from "./app-definitions/daytona.json" with { type: "json" }; import a105 from "./app-definitions/monday.json" with { type: "json" }; import a106 from "./app-definitions/paperclip-notebook.json" with { type: "json" }; -import a107 from "./app-definitions/speko.json" with { type: "json" }; +import a107 from "./app-definitions/unikraft.json" with { type: "json" }; +import a108 from "./app-definitions/speko.json" with { type: "json" }; import type { AppDefinition } from "./types/app-definition.js"; -export const APP_DEFINITIONS=[a0,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20,a21,a22,a23,a24,a25,a26,a27,a28,a29,a30,a31,a32,a33,a34,a35,a36,a37,a38,a39,a40,a41,a42,a43,a44,a45,a46,a47,a48,a49,a50,a51,a52,a53,a54,a55,a56,a57,a58,a59,a60,a61,a62,a63,a64,a65,a66,a67,a68,a69,a70,a71,a72,a73,a74,a75,a76,a77,a78,a79,a80,a81,a82,a83,a84,a85,a86,a87,a88,a89,a90,a91,a92,a93,a94,a95,a96,a97,a98,a99,a100,a101,a102,a103,a104,a105,a106,a107] as AppDefinition[]; +export const APP_DEFINITIONS=[a0,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20,a21,a22,a23,a24,a25,a26,a27,a28,a29,a30,a31,a32,a33,a34,a35,a36,a37,a38,a39,a40,a41,a42,a43,a44,a45,a46,a47,a48,a49,a50,a51,a52,a53,a54,a55,a56,a57,a58,a59,a60,a61,a62,a63,a64,a65,a66,a67,a68,a69,a70,a71,a72,a73,a74,a75,a76,a77,a78,a79,a80,a81,a82,a83,a84,a85,a86,a87,a88,a89,a90,a91,a92,a93,a94,a95,a96,a97,a98,a99,a100,a101,a102,a103,a104,a105,a106,a107,a108] as AppDefinition[]; diff --git a/packages/shared/src/app-definitions.test.ts b/packages/shared/src/app-definitions.test.ts index 3b1febbbde9..5855c9d2c92 100644 --- a/packages/shared/src/app-definitions.test.ts +++ b/packages/shared/src/app-definitions.test.ts @@ -886,7 +886,7 @@ describe("AppDefinition catalog", () => { "ticktick", "xero", ]); - expect(APP_STORE_DEFINITIONS).toHaveLength(85); + expect(APP_STORE_DEFINITIONS).toHaveLength(86); const connectableSlugs = new Set( CONNECTABLE_APP_DEFINITIONS.map((entry) => entry.slug), ); @@ -1285,6 +1285,38 @@ describe("AppDefinition catalog", () => { expect(method.warnings).toHaveLength(2); } }); + it("connects Unikraft Cloud's published HTTP APIs with an organization API token", () => { + const unikraft = APP_DEFINITIONS.find((app) => app.slug === "unikraft")!; + expect(unikraft).toMatchObject({ + name: "Unikraft", + categories: ["developer"], + urlPatterns: ["https://console.unikraft.com/*", "https://*.unikraft.cloud/*"], + docsUrl: "https://unikraft.com/docs/api/platform/v1", + branding: { logoUrl: "/brands/apps/unikraft.svg" }, + }); + expect(APP_STORE_DEFINITIONS.some((app) => app.slug === "unikraft")).toBe(true); + expect(CONNECTABLE_APP_DEFINITIONS.some((app) => app.slug === "unikraft")).toBe(true); + expect(unikraft.methods.map((method) => method.key)).toEqual(["api-token"]); + const [method] = unikraft.methods; + expect(method).toMatchObject({ + transport: "rest_api", + auth: "api_key", + ownershipModes: ["customer"], + riskTier: "S4", + defaults: { serverUrl: "https://controlplane.unikraft.cloud/v1" }, + keyPlacement: { location: "header", name: "Authorization", prefix: "Bearer " }, + }); + expect(method!.credentialFields).toEqual([ + expect.objectContaining({ key: "authorization", type: "password", secret: true, required: true }), + ]); + // The key form renders credential helper text, not method warnings. + expect(method!.credentialFields?.[0]?.helperMd).toContain("running sandbox commands and deleting resources"); + // The default metro is optional: setup asks only for the token. + expect(method!.tenantFields).toEqual([ + expect.objectContaining({ key: "metro", type: "select", advanced: true, defaultValue: "fra" }), + ]); + expect(method!.tenantFields?.[0]?.options?.map((option) => option.value)).toEqual(["dal", "fra", "sfo", "sin", "was"]); + }); it("connects Gauge's hosted server with browser sign-in or an organization API key", () => { const gauge = APP_DEFINITIONS.find((app) => app.slug === "gauge")!; expect(gauge).toMatchObject({ diff --git a/packages/shared/src/app-definitions.ts b/packages/shared/src/app-definitions.ts index 4f325b202e3..278ec99f7eb 100644 --- a/packages/shared/src/app-definitions.ts +++ b/packages/shared/src/app-definitions.ts @@ -9,6 +9,7 @@ export const CONNECTABLE_APP_SLUGS = new Set([ "agentmail", "browser-use-cloud", "daytona", + "unikraft", "cognee", ...SELF_SERVE_MCP_CANDIDATES.map((entry) => entry.slug), "monday", diff --git a/packages/shared/src/app-definitions/unikraft.json b/packages/shared/src/app-definitions/unikraft.json new file mode 100644 index 00000000000..2a930d3e373 --- /dev/null +++ b/packages/shared/src/app-definitions/unikraft.json @@ -0,0 +1,95 @@ +{ + "schemaVersion": 1, + "slug": "unikraft", + "name": "Unikraft", + "description": "Manage Unikraft Cloud instances, services, volumes, images, certificates, autoscaling, and sandbox commands and files.", + "categories": [ + "developer" + ], + "featured": false, + "urlPatterns": [ + "https://console.unikraft.com/*", + "https://*.unikraft.cloud/*" + ], + "methods": [ + { + "key": "api-token", + "label": "Connect Unikraft", + "transport": "rest_api", + "auth": "api_key", + "ownershipModes": [ + "customer" + ], + "whenToUse": "Manage resources and run sandbox commands in your Unikraft Cloud organization.", + "defaults": { + "serverUrl": "https://controlplane.unikraft.cloud/v1" + }, + "guidanceMd": "Connect a Unikraft Cloud API token. All published platform API and sandbox plugin actions are available; your action policies control which ones agents can use. Each action can target any Unikraft Cloud metro, and uses the default metro when it does not name one.", + "riskTier": "S4", + "credentialFields": [ + { + "key": "authorization", + "label": "Unikraft API token", + "type": "password", + "required": true, + "placeholder": "Enter Unikraft API token", + "secret": true, + "helperMd": "Create an API token in the Unikraft Cloud console. A token acts for its whole organization in every metro, including running sandbox commands and deleting resources. Unikraft does not offer narrower token permissions, so choose trusted agents and use action permissions to limit what they can do." + } + ], + "keyPlacement": { + "location": "header", + "name": "Authorization", + "prefix": "Bearer " + }, + "tenantFields": [ + { + "key": "metro", + "label": "Default metro", + "type": "select", + "advanced": true, + "defaultValue": "fra", + "helperMd": "Used to verify the token and for actions that do not name a metro.", + "options": [ + { + "value": "dal", + "label": "Dallas (dal)" + }, + { + "value": "fra", + "label": "Frankfurt (fra)" + }, + { + "value": "sfo", + "label": "San Francisco (sfo)" + }, + { + "value": "sin", + "label": "Singapore (sin)" + }, + { + "value": "was", + "label": "Washington, DC (was)" + } + ] + } + ], + "requiredResourceFilters": [ + "organization", + "metro", + "instance" + ], + "consoleLinks": { + "keys": "https://console.unikraft.com", + "docs": "https://unikraft.com/docs/sdks/overview#authentication" + }, + "warnings": [ + "This connection can run commands in sandboxes, read and write sandbox files, start and stop instances, and delete instances, volumes, and other resources. Select trusted agents and set action permissions before use. Your Unikraft API token stays in the vault." + ] + } + ], + "docsUrl": "https://unikraft.com/docs/api/platform/v1", + "branding": { + "logoUrl": "/brands/apps/unikraft.svg" + } +} diff --git a/scripts/app-definition-overrides/unikraft.json b/scripts/app-definition-overrides/unikraft.json new file mode 100644 index 00000000000..9d737bfaa9b --- /dev/null +++ b/scripts/app-definition-overrides/unikraft.json @@ -0,0 +1,76 @@ +{ + "schemaVersion": 1, + "slug": "unikraft", + "name": "Unikraft", + "description": "Manage Unikraft Cloud instances, services, volumes, images, certificates, autoscaling, and sandbox commands and files.", + "categories": [ + "developer" + ], + "featured": false, + "urlPatterns": [ + "https://console.unikraft.com/*", + "https://*.unikraft.cloud/*" + ], + "methods": [ + { + "key": "api-token", + "label": "Connect Unikraft", + "transport": "rest_api", + "auth": "api_key", + "ownershipModes": [ + "customer" + ], + "whenToUse": "Manage resources and run sandbox commands in your Unikraft Cloud organization.", + "defaults": { + "serverUrl": "https://controlplane.unikraft.cloud/v1" + }, + "guidanceMd": "Connect a Unikraft Cloud API token. All published platform API and sandbox plugin actions are available; your action policies control which ones agents can use. Each action can target any Unikraft Cloud metro, and uses the default metro when it does not name one.", + "riskTier": "S4", + "credentialFields": [ + { + "key": "authorization", + "label": "Unikraft API token", + "type": "password", + "required": true, + "placeholder": "Enter Unikraft API token", + "secret": true + } + ], + "keyPlacement": { + "location": "header", + "name": "Authorization", + "prefix": "Bearer " + }, + "tenantFields": [ + { + "key": "metro", + "label": "Default metro", + "type": "select", + "advanced": true, + "defaultValue": "fra", + "helperMd": "Used to verify the token and for actions that do not name a metro.", + "options": [ + { "value": "dal", "label": "Dallas (dal)" }, + { "value": "fra", "label": "Frankfurt (fra)" }, + { "value": "sfo", "label": "San Francisco (sfo)" }, + { "value": "sin", "label": "Singapore (sin)" }, + { "value": "was", "label": "Washington, DC (was)" } + ] + } + ], + "requiredResourceFilters": [ + "organization", + "metro", + "instance" + ], + "consoleLinks": { + "keys": "https://console.unikraft.com", + "docs": "https://unikraft.com/docs/sdks/overview#authentication" + }, + "warnings": [ + "This connection can run commands in sandboxes, read and write sandbox files, start and stop instances, and delete instances, volumes, and other resources. Select trusted agents and set action permissions before use. Your Unikraft API token stays in the vault." + ] + } + ], + "docsUrl": "https://unikraft.com/docs/api/platform/v1" +} diff --git a/scripts/generate-daytona-tools.mjs b/scripts/generate-daytona-tools.mjs index 3a0fc13ddb4..d4cbf966aca 100644 --- a/scripts/generate-daytona-tools.mjs +++ b/scripts/generate-daytona-tools.mjs @@ -12,7 +12,7 @@ const snake = value => value.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^a /** Retain only reachable input definitions, rather than copying the whole API * schema into every action. Provider response schemas are not tool inputs. */ -function inputSchema(value, schemas) { +export function inputSchema(value, schemas) { const definitions = {}; function visit(node) { if (Array.isArray(node)) return node.map(visit); diff --git a/scripts/generate-unikraft-tools.mjs b/scripts/generate-unikraft-tools.mjs new file mode 100644 index 00000000000..bf17a88e55e --- /dev/null +++ b/scripts/generate-unikraft-tools.mjs @@ -0,0 +1,135 @@ +import { createHash } from "node:crypto"; +import { readFile, writeFile, mkdir } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath, pathToFileURL } from "node:url"; +import { inputSchema } from "./generate-daytona-tools.mjs"; + +// The official specification repository has no tags or releases. Pin a +// reviewed commit so a provider update needs explicit regeneration and review. +const commit = "5dd107ea1cd8716b83cb86a83ed2a6ee46f99b5b"; +const sources = { + platform: `https://raw.githubusercontent.com/unikraft-cloud/openapi/${commit}/platform.json`, + sandbox: `https://raw.githubusercontent.com/unikraft-cloud/openapi/${commit}/plugins/sandbox.json`, +}; +const verbs = new Set(["get", "post", "put", "patch", "delete"]); +const snake = value => value.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1_$2") + .replace(/[^a-zA-Z0-9]+/g, "_").toLowerCase(); +// Requests that wait, read, or list without changing provider state. +const readOnlyPosts = new Set(["ReadFile", "ReadRawFile", "WaitForCommand", "WaitForCommandWithTimeout"]); +// Reversible but disruptive lifecycle changes, and code execution in a sandbox. +const destructiveOperations = /^(Stop|Suspend|Detach|Unpin)|^(RunCommand|SignalCommand|WriteCommandStdin)$/; + +function parameterDefinition(parameter, spec) { + if (!parameter.$ref) return parameter; + const prefix = "#/components/parameters/"; + if (!parameter.$ref.startsWith(prefix)) throw new Error(`Unsupported parameter reference: ${parameter.$ref}`); + const resolved = spec.components?.parameters?.[parameter.$ref.slice(prefix.length)]; + if (!resolved) throw new Error(`Missing parameter: ${parameter.$ref}`); + return resolved; +} + +/** Ajv does not define OpenAPI's unsigned integer formats. Keep their sign + * constraint instead of an unknown format that would not validate anything. + * Some unsigned fields document -1 as a sentinel (for example, the longest + * drain time); keep that value valid. */ +function unsignedFormats(node) { + if (Array.isArray(node)) return node.forEach(unsignedFormats); + if (!node || typeof node !== "object") return; + if (/^uint(8|16|32|64)$/.test(node.format ?? "")) { + delete node.format; + node.minimum ??= /(^|\s)-1\b/.test(node.description ?? "") ? -1 : 0; + } + Object.values(node).forEach(unsignedFormats); +} + +function streamingOperation(operation) { + return Object.values(operation.responses ?? {}).some(response => Object.hasOwn(response.content ?? {}, "text/event-stream")); +} + +export function generateUnikraftCatalog(specs) { + const metros = (specs.platform?.servers ?? []).map(server => new URL(server.url).hostname.match(/^api\.([a-z0-9]+)\.unikraft\.cloud$/)?.[1]) + .filter(Boolean).sort(); + const tools = []; + const streaming = []; + for (const [kind, spec] of Object.entries(specs)) { + for (const [route, item] of Object.entries(spec.paths)) { + for (const [method, operation] of Object.entries(item)) { + if (!verbs.has(method)) continue; + if (!operation.operationId) throw new Error(`Missing operationId: ${kind} ${method} ${route}`); + const name = kind === "platform" ? `unikraft_${snake(operation.operationId)}` : `unikraft_${kind}_${snake(operation.operationId)}`; + // Server-sent event streams hold a connection open; they are not + // request/response actions. Keep them in the inventory so an update + // cannot silently lose an operation. + if (streamingOperation(operation)) { + streaming.push({ name, kind, path: route, method: method.toUpperCase(), transport: "server-sent-events" }); + continue; + } + const properties = {}, required = [], parameters = []; + const groups = { path: { type: "object", properties: {}, required: [], additionalProperties: false }, + query: { type: "object", properties: {}, required: [], additionalProperties: false }, + headers: { type: "object", properties: {}, required: [], additionalProperties: false } }; + for (const reference of [...(item.parameters ?? []), ...(operation.parameters ?? [])]) { + const parameter = parameterDefinition(reference, spec); + const group = parameter.in === "header" ? "headers" : parameter.in; + if (!groups[group]) throw new Error(`Unsupported parameter: ${name} ${parameter.in}`); + // Authorization comes from the selected connection, never an argument. + if (group === "headers" && /^(authorization|cookie|host)$/i.test(parameter.name)) continue; + groups[group].properties[parameter.name] = { ...(parameter.schema ?? {}), + ...(parameter.description ? { description: parameter.description } : {}) }; + if (parameter.required) groups[group].required.push(parameter.name); + parameters.push({ name: parameter.name, group, style: parameter.style ?? "form", explode: parameter.explode ?? true }); + } + for (const [group, schema] of Object.entries(groups)) { + if (!Object.keys(schema.properties).length) continue; + properties[group] = schema; + if (schema.required.length) required.push(group); + } + properties.metro = { type: "string", enum: metros, description: "Unikraft Cloud metro. Defaults to the connection's default metro." }; + if (kind === "sandbox") { + properties.instance = { type: "string", minLength: 1, maxLength: 200, description: "Name or UUID of the instance running the sandbox plugin." }; + required.push("instance"); + } + const content = operation.requestBody?.content ?? {}; + const bodyContentType = Object.hasOwn(content, "application/json") ? "application/json" : Object.keys(content)[0]; + if (bodyContentType && bodyContentType !== "application/json") throw new Error(`Unsupported request body: ${name} ${bodyContentType}`); + if (bodyContentType) { + properties.body = content[bodyContentType].schema ?? {}; + if (operation.requestBody.required) required.push("body"); + } + const readOnly = method === "get" || readOnlyPosts.has(operation.operationId); + const destructive = !readOnly && (method === "delete" || destructiveOperations.test(operation.operationId)); + const schema = inputSchema({ type: "object", additionalProperties: false, properties, required }, spec.components?.schemas ?? {}); + unsignedFormats(schema); + tools.push({ name, kind, path: route, method: method.toUpperCase(), parameters, + ...(bodyContentType ? { bodyContentType } : {}), + title: operation.summary || operation.operationId, + description: operation.description || operation.summary || operation.operationId, + inputSchema: schema, + annotations: { readOnlyHint: readOnly, destructiveHint: destructive, idempotentHint: readOnly, openWorldHint: true } }); + } + } + } + if (!metros.length) throw new Error("The platform specification lists no Unikraft Cloud metros"); + const names = tools.map(tool => tool.name); + if (new Set(names).size !== names.length) throw new Error("Duplicate Unikraft action name"); + return { metros, tools, streaming }; +} + +async function main() { + const inputIndex = process.argv.indexOf("--input-dir"); + const inputDir = inputIndex < 0 ? undefined : process.argv[inputIndex + 1]; + const specs = {}, provenance = {}; + for (const [kind, url] of Object.entries(sources)) { + const raw = inputDir ? await readFile(path.join(inputDir, `unikraft-${kind}-openapi.json`)) + : Buffer.from(await (await fetch(url)).arrayBuffer()); + specs[kind] = JSON.parse(raw.toString("utf8")); + provenance[kind] = { url, sha256: createHash("sha256").update(raw).digest("hex") }; + } + const output = { schemaVersion: 1, sources: provenance, ...generateUnikraftCatalog(specs) }; + const target = fileURLToPath(new URL("../server/src/services/connectors/unikraft/operations.generated.json", import.meta.url)); + await mkdir(path.dirname(target), { recursive: true }); + await writeFile(target, JSON.stringify(output, null, 2) + "\n"); + console.log(`Generated ${output.tools.length} Unikraft actions across ${output.metros.length} metros; recorded ${output.streaming.length} streaming endpoints.`); +} + +if (process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href) await main(); diff --git a/scripts/generate-unikraft-tools.test.mjs b/scripts/generate-unikraft-tools.test.mjs new file mode 100644 index 00000000000..2e7cabee672 --- /dev/null +++ b/scripts/generate-unikraft-tools.test.mjs @@ -0,0 +1,61 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; +import { generateUnikraftCatalog } from "./generate-unikraft-tools.mjs"; + +const servers = [{ url: "https://api.fra.unikraft.cloud" }, { url: "https://api.dal.unikraft.cloud" }]; + +test("accounts for every HTTP operation and server-sent event stream without dropping methods", () => { + const result = generateUnikraftCatalog({ platform: { servers, paths: { + "/v1/instances": { get: { operationId: "GetInstances" }, post: { operationId: "CreateInstance" } }, + "/v1/audit": { get: { operationId: "SubscribeAuditEvents", responses: { default: { content: { "text/event-stream": {} } } } } }, + } } }); + assert.deepEqual(result.metros, ["dal", "fra"]); + assert.deepEqual(result.tools.map(t => t.name), ["unikraft_get_instances", "unikraft_create_instance"]); + assert.deepEqual(result.streaming.map(s => s.name), ["unikraft_subscribe_audit_events"]); + assert.deepEqual(result.tools[0].inputSchema.properties.metro.enum, ["dal", "fra"]); +}); + +test("resolves shared parameters, keeps GET bodies and replaces unsigned formats", () => { + const result = generateUnikraftCatalog({ platform: { servers, paths: { "/v1/instances/log": { get: { + operationId: "GetInstanceLogs", + parameters: [{ $ref: "#/components/parameters/Uuid" }, { name: "limit", in: "query", schema: { type: "integer", format: "uint32" } }, + { name: "Authorization", in: "header", schema: { type: "string" } }], + requestBody: { required: true, content: { "application/json": { schema: { $ref: "#/components/schemas/Target" } } } }, + } } }, components: { + parameters: { Uuid: { name: "uuid", in: "query", schema: { type: "array", items: { type: "string" } } } }, + schemas: { Target: { type: "object", properties: { offset: { type: "integer", format: "uint64" }, + drain_timeout_ms: { type: "integer", format: "uint64", description: "No draining if 0. Use -1 for the largest possible value." } } } }, + } } }); + const [tool] = result.tools; + assert.deepEqual(tool.parameters.map(p => p.name), ["uuid", "limit"]); + assert.equal(tool.inputSchema.properties.headers, undefined); + assert.deepEqual(tool.inputSchema.properties.query.properties.limit, { type: "integer", minimum: 0 }); + assert.deepEqual(tool.inputSchema.definitions.Target.properties.offset, { type: "integer", minimum: 0 }); + // A documented -1 sentinel stays valid. + assert.equal(tool.inputSchema.definitions.Target.properties.drain_timeout_ms.minimum, -1); + assert.equal(tool.bodyContentType, "application/json"); + assert.ok(tool.inputSchema.required.includes("body")); + assert.equal(tool.annotations.readOnlyHint, true); +}); + +test("targets a sandbox instance and treats execution, disruption and deletion as destructive", () => { + const result = generateUnikraftCatalog({ platform: { servers, paths: { + "/v1/instances/stop": { put: { operationId: "StopInstances" } }, + "/v1/instances/start": { put: { operationId: "StartInstances" } }, + "/v1/volumes/detach": { put: { operationId: "DetachVolumes" } }, + "/v1/volumes/{uuid}": { delete: { operationId: "DeleteVolumeByUUID" } }, + } }, sandbox: { paths: { + "/commands": { post: { operationId: "RunCommand" } }, + "/fs/read": { post: { operationId: "ReadFile" } }, + "/fs/write": { post: { operationId: "WriteFile" } }, + } } }); + const risk = Object.fromEntries(result.tools.map(t => [t.name, + t.annotations.destructiveHint ? "destructive" : t.annotations.readOnlyHint ? "read" : "write"])); + assert.deepEqual(risk, { + unikraft_stop_instances: "destructive", unikraft_start_instances: "write", unikraft_detach_volumes: "destructive", + unikraft_delete_volume_by_uuid: "destructive", unikraft_sandbox_run_command: "destructive", + unikraft_sandbox_read_file: "read", unikraft_sandbox_write_file: "write", + }); + const sandbox = result.tools.find(t => t.name === "unikraft_sandbox_run_command"); + assert.ok(sandbox.inputSchema.required.includes("instance")); +}); diff --git a/server/src/__tests__/connector-tool-policy.test.ts b/server/src/__tests__/connector-tool-policy.test.ts index 9c094e3d27a..873ff3ecacc 100644 --- a/server/src/__tests__/connector-tool-policy.test.ts +++ b/server/src/__tests__/connector-tool-policy.test.ts @@ -41,7 +41,7 @@ describe("reviewed connector risk dispatch", () => { }); describe("reviewed connector catalog policies", () => { - it.each(["railway", "enterpret", "daytona", "braintrust"])("declares the initial drift policy for %s", source => { + it.each(["railway", "enterpret", "daytona", "unikraft", "braintrust"])("declares the initial drift policy for %s", source => { expect(connectorToolPolicy(source).quarantineNewEntries).toBe(true); }); diff --git a/server/src/__tests__/unikraft-connection.test.ts b/server/src/__tests__/unikraft-connection.test.ts new file mode 100644 index 00000000000..5980682d8f2 --- /dev/null +++ b/server/src/__tests__/unikraft-connection.test.ts @@ -0,0 +1,198 @@ +import { randomUUID } from "node:crypto"; +import { and, eq } from "drizzle-orm"; +import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; +import { agents, companies, companyMemberships, companySecrets, connectionGrants, createDb, + ensurePostgresDatabase, applyPendingMigrations, heartbeatRuns, issues, toolCatalogEntries, toolConnections } from "@paperclipai/db"; +import { toolAccessService } from "../services/tool-access.js"; +import { createToolGatewayService } from "../services/tool-gateway.js"; +import { UNIKRAFT_TOOLS, executeUnikraftTool, unikraftClient } from "../services/connectors/unikraft/client.js"; +import { getEmbeddedPostgresTestSupport, startEmbeddedPostgresTestDatabase } from "./helpers/embedded-postgres.js"; + +const external = process.env.PAPERCLIP_TEST_DATABASE_URL; +const support = external ? { supported: true } : await getEmbeddedPostgresTestSupport(); +const actor = { actorType: "user" as const, actorId: "unikraft-reviewer" }; +const credential = "unikraft-fixture-token"; +(support.supported ? describe : describe.skip)("Unikraft governed provider actions", () => { + let db: ReturnType; + let temp: Awaited> | undefined; + beforeAll(async () => { + if (external) { + const url = new URL(external); url.pathname = "/unikraft_" + randomUUID().replaceAll("-", ""); + await ensurePostgresDatabase(external, url.pathname.slice(1)); + await applyPendingMigrations(url.toString()); db = createDb(url.toString()); + } else { temp = await startEmbeddedPostgresTestDatabase("unikraft-connector-"); db = createDb(temp.connectionString); } + }, 30000); + afterAll(async () => { await db?.$client.end(); await temp?.cleanup(); }); + + async function fixture(options: { personal?: boolean; metro?: string } = {}) { + const [company] = await db.insert(companies).values({ name: "Unikraft fixture", issuePrefix: `U${randomUUID().slice(0, 6)}` }).returning(); + await db.insert(companyMemberships).values({ companyId: company.id, principalType: "user", principalId: actor.actorId, membershipRole: "admin", status: "active" }); + const instanceId = randomUUID(); + const request = vi.fn(async (url: string, init: RequestInit) => { + expect(new Headers(init.headers).get("Authorization")).toBe(`Bearer ${credential}`); + expect(["error", "manual"]).toContain(init.redirect); + const parsed = new URL(url); + expect(parsed.hostname).toMatch(/^api\.(dal|fra|sfo|sin|was)\.unikraft\.cloud$/); + const metro = parsed.hostname.split(".")[1]; + const body = typeof init.body === "string" ? JSON.parse(init.body) : undefined; + if (parsed.pathname === "/v1/users/quotas") return Response.json({ status: "success", data: { quotas: [{ uuid: randomUUID() }] }, op_time_us: 1 }); + if (parsed.pathname === "/v1/instances" && init.method === "GET") { + return Response.json({ status: "success", data: { instances: [{ uuid: instanceId, name: "web", metro, selector: body ?? null }] }, op_time_us: 1 }); + } + if (parsed.pathname === "/v1/instances" && init.method === "POST") return Response.json({ status: "success", data: { instances: [{ uuid: instanceId, ...body }] }, op_time_us: 1 }); + if (parsed.pathname === `/v1/instances/${instanceId}` && init.method === "DELETE") return Response.json({ status: "success", data: { instances: [{ uuid: instanceId, state: "deleted" }] }, op_time_us: 1 }); + if (parsed.pathname === "/v1/instances/web/plugins/sandbox/commands" && init.method === "POST") { + return Response.json({ uuid: randomUUID(), cmd: body.cmd, stdout: `ran with ${credential}` }); + } + throw new Error(`Unexpected provider request ${init.method} ${url}`); + }); + const service = toolAccessService(db, { remoteHttpRequest: request, remoteHttpEndpointLookup: async () => [{ address: "8.8.8.8", family: 4 }] }); + const connected = await service.connectGalleryApp(company.id, { galleryKey: "unikraft", methodKey: "api-token", + ...(options.personal ? { grantKind: "user" as const } : {}), ...(options.metro ? { configValues: { metro: options.metro } } : {}), + credentialValues: { "credentials.authorization": credential } }, actor); + const [agent] = await db.insert(agents).values({ companyId: company.id, name: "Operator", role: "engineer", adapterType: "process" }).returning(); + await service.finishGalleryAppConnection(company.id, connected.connectionId, { enabledCatalogEntryIds: connected.catalog.map(c => c.id), + askFirstCatalogEntryIds: [], access: { agentIds: [agent.id] } }, actor); + const [issue] = await db.insert(issues).values({ companyId: company.id, title: "Provider work", assigneeAgentId: agent.id }).returning(); + const [run] = await db.insert(heartbeatRuns).values({ companyId: company.id, agentId: agent.id, issueId: issue.id, + scopeKind: "issue", invocationSource: "on_demand", status: "running", responsibleUserId: actor.actorId, contextSnapshot: { issueId: issue.id } }).returning(); + const gateway = createToolGatewayService(db, { remoteHttpRequest: request }); + const session = await gateway.createSession({ companyId: company.id, agentId: agent.id, runId: run.id, issueId: issue.id }); + const tools = await gateway.listToolsForSession(session.token); + const call = (name: string, parameters: unknown) => { + const tool = tools.find(t => t.upstreamToolName === name); + expect(tool, name).toBeTruthy(); + return gateway.executeTool({ sessionToken: session.token, tool: tool!.name, parameters, idempotencyKey: randomUUID() }); + }; + return { company, service, connected, agent, request, call, gateway, session, instanceId }; + } + + it.each([false, true])("uses the selected vault identity for real gateway reads, writes and sandbox commands (personal=%s)", async personal => { + const f = await fixture({ personal }); + const [grant] = await db.select().from(connectionGrants).where(eq(connectionGrants.connectionId, f.connected.connectionId)); + const [secret] = await db.select().from(companySecrets).where(eq(companySecrets.id, grant.credentialSecretRefs[0].secretId)); + expect(grant.kind).toBe(personal ? "user" : "organization"); + expect(secret.scope).toBe(personal ? "user" : "company"); + expect(secret.ownerUserId).toBe(personal ? actor.actorId : null); + expect(f.connected.catalog).toHaveLength(UNIKRAFT_TOOLS.length); + expect(f.connected.catalog.find(c => c.toolName === "unikraft_get_instances")).toMatchObject({ isReadOnly: true }); + expect(f.connected.catalog.find(c => c.toolName === "unikraft_create_instance")).toMatchObject({ isReadOnly: false, isDestructive: false }); + for (const name of ["unikraft_delete_instance_by_uuid", "unikraft_stop_instances", "unikraft_sandbox_run_command"]) { + expect(f.connected.catalog.find(c => c.toolName === name), name).toMatchObject({ isDestructive: true, isReadOnly: false }); + } + expect(JSON.stringify(await f.service.getConnection(f.connected.connectionId))).not.toContain(credential); + // The token is verified against the default metro. + expect(f.request.mock.calls.some(([url]) => url === "https://api.fra.unikraft.cloud/v1/users/quotas")).toBe(true); + // Batch selectors travel as a JSON body even on GET. + const listed = JSON.stringify(await f.call("unikraft_get_instances", { body: [{ name: "web" }] })); + expect(listed).toContain(f.instanceId); expect(listed).toContain('"metro":"fra"'); expect(listed).toContain('"name":"web"'); + expect(JSON.stringify(await f.call("unikraft_get_instances", { metro: "sin" }))).toContain('"metro":"sin"'); + expect(JSON.stringify(await f.call("unikraft_create_instance", { body: { image: "nginx:latest" } }))).toContain("nginx:latest"); + expect(JSON.stringify(await f.call("unikraft_delete_instance_by_uuid", { path: { uuid: f.instanceId } }))).toContain("deleted"); + const ran = JSON.stringify(await f.call("unikraft_sandbox_run_command", { instance: "web", body: { cmd: "echo done" } })); + expect(ran).toContain("echo done"); expect(ran).not.toContain(credential); + }, 30000); + + it("uses the connection's default metro for verification and actions that do not name one", async () => { + const f = await fixture({ metro: "was" }); + expect(f.request.mock.calls.some(([url]) => url === "https://api.was.unikraft.cloud/v1/users/quotas")).toBe(true); + expect(JSON.stringify(await f.call("unikraft_get_instances", {}))).toContain('"metro":"was"'); + }); + + it("rejects unpublished metros, unknown arguments, path traversal and foreign bindings before forwarding the token", async () => { + const f = await fixture(); f.request.mockClear(); + await expect(f.call("unikraft_get_instances", { metro: "attacker.example" })).rejects.toThrow(); + await expect(f.call("unikraft_get_instances", { command: "printenv" })).rejects.toThrow(); + await expect(f.call("unikraft_sandbox_run_command", { instance: "../../users/quotas", body: { cmd: "true" } })).rejects.toThrow("Invalid Unikraft path"); + expect(f.request).not.toHaveBeenCalled(); + await expect(executeUnikraftTool({ binding: { companyId: randomUUID(), agentId: f.agent.id }, + connection: { companyId: f.company.id, config: {} }, headers: { Authorization: `Bearer ${credential}` }, request: f.request, + toolName: "unikraft_get_instances", parameters: {} })).rejects.toThrow("Connection not found"); + await expect(executeUnikraftTool({ binding: { companyId: f.company.id, agentId: f.agent.id }, + connection: { companyId: f.company.id, config: { methodConfig: { metro: "evil" } } }, headers: { Authorization: `Bearer ${credential}` }, + request: f.request, toolName: "unikraft_get_instances", parameters: {} })).rejects.toThrow("Choose a Unikraft Cloud metro"); + expect(f.request).not.toHaveBeenCalled(); + }); + + it("rechecks revocation and Off policies for a previously created gateway session", async () => { + const f = await fixture(); + await f.service.finishGalleryAppConnection(f.company.id, f.connected.connectionId, { + enabledCatalogEntryIds: f.connected.catalog.filter(c => c.toolName !== "unikraft_sandbox_run_command").map(c => c.id), + askFirstCatalogEntryIds: [], access: { agentIds: [f.agent.id] }, + }, actor); + f.request.mockClear(); + await expect(f.call("unikraft_sandbox_run_command", { instance: "web", body: { cmd: "true" } })).rejects.toThrow(); + expect(f.request).not.toHaveBeenCalled(); + await db.update(toolConnections).set({ enabled: false }).where(and(eq(toolConnections.id, f.connected.connectionId), eq(toolConnections.companyId, f.company.id))); + await expect(f.call("unikraft_get_instances", {})).rejects.toThrow(); + expect(f.request).not.toHaveBeenCalled(); + }); + + it("blocks a stale same-name contract and quarantines it on refresh", async () => { + const f = await fixture(); + const changed = f.connected.catalog.find(entry => entry.toolName === "unikraft_sandbox_run_command")!; + await db.update(toolCatalogEntries).set({ versionHash: "earlier-provider-contract", schemaHash: "earlier-schema", riskLevel: "read" }) + .where(eq(toolCatalogEntries.id, changed.id)); + f.request.mockClear(); + expect((await f.gateway.listToolsForSession(f.session.token)).some(tool => tool.upstreamToolName === changed.toolName)).toBe(false); + await expect(f.call(changed.toolName, { instance: "web", body: { cmd: "true" } })).rejects.toThrow(); + expect(f.request).not.toHaveBeenCalled(); + const refreshed = await f.service.refreshCatalog(f.connected.connectionId, actor, { enableAllByDefault: true }); + expect(refreshed.catalog.find(entry => entry.toolName === changed.toolName)?.status).toBe("quarantined"); + }); + + it("does not export the token to agents", async () => { + const f = await fixture(); + const [run] = await db.select().from(heartbeatRuns).where(eq(heartbeatRuns.agentId, f.agent.id)); + await expect(f.service.mintConnectionTokenForAgent({ connectionId: f.connected.connectionId, companyId: f.company.id, + agentId: f.agent.id, runId: run.id, body: {} as never })).rejects.toThrow("Unikraft credentials stay in the governed tool gateway"); + }); +}); + +describe("Unikraft provider client", () => { + it("validates every published action schema before provider dispatch", async () => { + const request = vi.fn(async () => { throw new Error("Unexpected provider request"); }); + expect(UNIKRAFT_TOOLS).toHaveLength(105); + for (const tool of UNIKRAFT_TOOLS) { + await expect(executeUnikraftTool({ binding: { companyId: "company", agentId: "agent" }, + connection: { companyId: "company", config: {} }, headers: { Authorization: `Bearer ${credential}` }, + request, toolName: tool.name, parameters: null }), tool.name).rejects.toThrow("Invalid Unikraft action arguments"); + } + expect(request).not.toHaveBeenCalled(); + }); + it("sanitizes provider denials and rejects an unconfirmed token", async () => { + await expect(unikraftClient({ Authorization: `Bearer ${credential}` }, async () => new Response(credential, { status: 401 })).probe()) + .rejects.toThrow("Unikraft denied access"); + await expect(unikraftClient({ Authorization: `Bearer ${credential}` }, async () => Response.json({ status: "error" })).probe()) + .rejects.toThrow("did not confirm"); + }); + it("accepts the documented -1 drain timeout for the longest drain", async () => { + const request = vi.fn(async (_url: string, init: RequestInit) => Response.json({ status: "success", echo: JSON.parse(init.body as string) })); + for (const [toolName, parameters] of [ + ["unikraft_stop_instances", { body: [{ name: "web", drain_timeout_ms: -1 }] }], + ["unikraft_suspend_instance_by_uuid", { path: { uuid: randomUUID() }, body: { drain_timeout_ms: -1 } }], + ] as const) { + await executeUnikraftTool({ binding: { companyId: "company", agentId: "agent" }, connection: { companyId: "company", config: {} }, + headers: { Authorization: `Bearer ${credential}` }, request, toolName, parameters }); + } + expect(request).toHaveBeenCalledTimes(2); + await expect(executeUnikraftTool({ binding: { companyId: "company", agentId: "agent" }, connection: { companyId: "company", config: {} }, + headers: { Authorization: `Bearer ${credential}` }, request, toolName: "unikraft_stop_instances", + parameters: { body: [{ name: "web", drain_timeout_ms: -2 }] } })).rejects.toThrow("Invalid Unikraft action arguments"); + }); + it("reports a timed-out sandbox wait as a still-running command, not a provider failure", async () => { + const commandId = randomUUID(); + const request = vi.fn(async () => new Response(null, { status: 408 })); + const call = (toolName: string) => executeUnikraftTool({ binding: { companyId: "company", agentId: "agent" }, + connection: { companyId: "company", config: {} }, headers: { Authorization: `Bearer ${credential}` }, request, toolName, + parameters: { instance: "web", path: { uuid: commandId }, ...(toolName.endsWith("with_timeout") ? { body: { timeout_s: 0.01 } } : {}) } }); + await expect(call("unikraft_sandbox_wait_for_command_with_timeout")).resolves.toMatchObject({ status: "running", timedOut: true }); + expect(request.mock.calls[0]![0]).toBe(`https://api.fra.unikraft.cloud/v1/instances/web/plugins/sandbox/commands/${commandId}/wait_timeout`); + // Other operations keep treating 408 as a failure. + await expect(call("unikraft_sandbox_get_command_by_uuid")).rejects.toThrow("Unikraft could not complete this operation"); + }); + it("bounds response bytes", async () => { + await expect(unikraftClient({ Authorization: `Bearer ${credential}` }, async () => new Response("x".repeat(8 * 1024 * 1024 + 1))).probe()) + .rejects.toThrow("exceeded the limit"); + }); +}); diff --git a/server/src/__tests__/unikraft-http-boundary.test.ts b/server/src/__tests__/unikraft-http-boundary.test.ts new file mode 100644 index 00000000000..f004fde48dd --- /dev/null +++ b/server/src/__tests__/unikraft-http-boundary.test.ts @@ -0,0 +1,69 @@ +import { createServer, type Server } from "node:http"; +import { connect, type AddressInfo, type Socket } from "node:net"; +import { afterEach, describe, expect, it } from "vitest"; +import { executeUnikraftTool } from "../services/connectors/unikraft/client.js"; +import { guardedRemoteHttpFetch } from "../services/remote-http-fetch.js"; + +const credential = "unikraft-http-fixture-token"; +const servers: Server[] = [], sockets: Socket[] = []; +afterEach(async () => { + sockets.splice(0).forEach(socket => socket.destroy()); + await Promise.all(servers.splice(0).map(server => new Promise(resolve => server.close(() => resolve())))); +}); + +async function provider() { + const received: { method: string; url: string; host: string; contentType?: string; body: string }[] = []; + const server = createServer((req, res) => { + const chunks: Buffer[] = []; + req.on("data", chunk => chunks.push(chunk)); + req.on("end", () => { + received.push({ method: req.method!, url: req.url!, host: req.headers.host!, contentType: req.headers["content-type"], + body: Buffer.concat(chunks).toString("utf8") }); + res.setHeader("Content-Type", "application/json"); + res.end(JSON.stringify({ status: "success", data: { instances: [] }, op_time_us: 1 })); + }); + }); + servers.push(server); + server.on("connection", socket => sockets.push(socket)); + await new Promise(resolve => server.listen(0, "127.0.0.1", resolve)); + const port = (server.address() as AddressInfo).port; + // Preserve the real pinned node:http path, including its body serializer. + // Only the network/TLS leg is a controlled local stand-in. + const request = (url: string, init: RequestInit) => { + const endpoint = new URL(url); + endpoint.protocol = "http:"; endpoint.port = String(port); + return guardedRemoteHttpFetch(endpoint, init, { + allowPrivateNetwork: false, lookup: async () => [{ address: "93.184.216.34", family: 4 }], + socketFactory: target => { + const socket = connect({ host: "127.0.0.1", port }); + sockets.push(socket); + Object.defineProperty(socket, "remoteAddress", { get: () => target.address, configurable: true }); + return socket; + }, + responseTimeoutMs: 5000, error: (message, code) => Object.assign(new Error(message), { code }), + }); + }; + const call = (toolName: string, parameters: unknown) => executeUnikraftTool({ + binding: { companyId: "company", agentId: "agent" }, connection: { companyId: "company", config: {} }, + headers: { Authorization: `Bearer ${credential}` }, request, toolName, parameters, + signal: AbortSignal.timeout(3000), deadlineAt: Date.now() + 3000, + }); + return { call, received }; +} + +describe("Unikraft production HTTP boundary", () => { + it.each([ + ["unikraft_get_instance_logs", "GET", { metro: "dal", query: { name: ["web"] }, body: [{ name: "web" }] }], + ["unikraft_delete_instances", "DELETE", { body: [{ name: "web" }] }], + ] as const)("sends %s JSON selectors as a %s request body through the pinned transport", async (toolName, method, parameters) => { + const p = await provider(); + await p.call(toolName, parameters); + expect(p.received).toHaveLength(1); + const [received] = p.received; + expect(received.method).toBe(method); + expect(received.host).toMatch(new RegExp(`^api\\.${parameters.metro ?? "fra"}\\.unikraft\\.cloud:`)); + expect(received.contentType).toBe("application/json"); + expect(JSON.parse(received.body)).toEqual([{ name: "web" }]); + if (method === "GET") expect(received.url).toBe("/v1/instances/log?name=web"); + }); +}); diff --git a/server/src/services/connectors/tool-policy.ts b/server/src/services/connectors/tool-policy.ts index 6b5cfe63d93..c8444474943 100644 --- a/server/src/services/connectors/tool-policy.ts +++ b/server/src/services/connectors/tool-policy.ts @@ -1,5 +1,5 @@ import type { ToolRiskLevel } from "@paperclipai/shared"; -import { daytonaRisk, isDaytonaConnection } from "../daytona.js"; +import { NATIVE_PROVIDER_CATALOGS } from "../native-provider-catalogs.js"; import { classifyRailwayToolRisk, isRailwayEndpoint } from "../railway.js"; import { braintrustRisk } from "./braintrust/risk.js"; import { enterpretRisk } from "./enterpret/risk.js"; @@ -34,12 +34,13 @@ const reviewedPolicies: ReviewedToolPolicy[] = [ // review and drift handling, without enabling the native API bridge. matchesEndpoint: connection => isRailwayEndpoint(connection.config.url), }, - { - sourceTemplateKey: "daytona", + // Native REST providers (Daytona, Unikraft) classify from their generated contract. + ...NATIVE_PROVIDER_CATALOGS.map((provider): ReviewedToolPolicy => ({ + sourceTemplateKey: provider.slug, quarantineNewEntries: true, - classifyRisk: tool => daytonaRisk(tool.name), - matchesConnection: isDaytonaConnection, - }, + classifyRisk: tool => provider.risk(tool.name), + matchesConnection: provider.isConnection, + })), { sourceTemplateKey: "enterpret", quarantineNewEntries: true, classifyRisk: enterpretRisk }, { sourceTemplateKey: "braintrust", quarantineNewEntries: true, classifyRisk: braintrustRisk }, ]; diff --git a/server/src/services/connectors/unikraft/client.ts b/server/src/services/connectors/unikraft/client.ts new file mode 100644 index 00000000000..edeea4e2f93 --- /dev/null +++ b/server/src/services/connectors/unikraft/client.ts @@ -0,0 +1,108 @@ +import { NATIVE_REST_MAX_BYTES, nativeCatalog, nativeRestFailure as failure, nativeRestPathValue, nativeRestRecord as record, + nativeRestSender, type NativeCatalogEntry, type NativeRestRequest } from "../../native-rest-client.js"; +import catalog from "./operations.generated.json" with { type: "json" }; + +export const UNIKRAFT_METHOD = "api-token"; +// Unikraft Cloud has no global platform endpoint; each metro serves its own API. +// The global control-plane URL identifies the connection; actions use the +// reviewed metro hosts below. +export const UNIKRAFT_CONNECTION_URL = "https://controlplane.unikraft.cloud/v1"; +export const UNIKRAFT_METROS: readonly string[] = catalog.metros; +export const UNIKRAFT_DEFAULT_METRO = "fra"; +type Operation = typeof catalog.tools[number]; +const contract = nativeCatalog(catalog.tools); + +export const UNIKRAFT_TOOLS = contract.tools; +export const isUnikraftToolName = contract.isToolName; +export const unikraftRisk = contract.risk; +export const isCurrentUnikraftCatalogEntry = (entry: NativeCatalogEntry) => contract.isCurrentEntry(entry); + +export function isUnikraftConnection(connection: { transport: string; config?: Record }) { + return connection.transport === "rest_api" && connection.config?.sourceTemplateKey === "unikraft" + && connection.config.connectionMethodKey === UNIKRAFT_METHOD && connection.config.url === UNIKRAFT_CONNECTION_URL; +} + +/** Only metros published in the reviewed specification receive the token. */ +export function unikraftApiUrl(metro: unknown) { + if (typeof metro !== "string" || !UNIKRAFT_METROS.includes(metro)) { + throw failure(422, "unikraft_metro_invalid", `Choose a Unikraft Cloud metro: ${UNIKRAFT_METROS.join(", ")}.`); + } + return `https://api.${metro}.unikraft.cloud`; +} + +function pathValue(value: unknown) { + return nativeRestPathValue(value, () => failure(422, "unikraft_arguments_invalid", "Invalid Unikraft path argument.")); +} + +/** A provider-focused REST bridge. It never reads host files, calls host shell + * commands, borrows CLI login state, or looks up Paperclip tasks. */ +export function unikraftClient(headers: Record, request: NativeRestRequest, defaultMetro: unknown = UNIKRAFT_DEFAULT_METRO, + signal: AbortSignal = AbortSignal.timeout(30_000), deadlineAt: number = Date.now() + 30_000) { + const authorization = headers.Authorization; + if (!authorization || !/^Bearer [^\r\n]+$/.test(authorization)) { + throw failure(422, "unikraft_credential_missing", "Reconnect Unikraft to restore its API token."); + } + const { send } = nativeRestSender({ provider: "Unikraft", code: "unikraft", authorization, request, signal, deadlineAt, + deniedMessage: "Unikraft denied access. Check that this token belongs to the intended organization and has not been revoked." }); + + async function probe() { + const value = record(await send(`${unikraftApiUrl(defaultMetro)}/v1/users/quotas`, { method: "GET" })); + if (value.status !== "success") throw failure(502, "unikraft_invalid_response", "Unikraft did not confirm this API token."); + return { metro: defaultMetro as string }; + } + + async function execute(operation: Operation, parameters: Record) { + let base = unikraftApiUrl(parameters.metro ?? defaultMetro); + if (operation.kind === "sandbox") base += `/v1/instances/${pathValue(parameters.instance)}/plugins/sandbox`; + const route = operation.path.replace(/\{([^}]+)\}/g, (_match, key: string) => pathValue(record(parameters.path)[key])); + const url = new URL(base + route); + const projectedHeaders: Record = {}; + for (const parameter of operation.parameters) { + if (parameter.group === "path") continue; + const value = record(parameters[parameter.group])[parameter.name]; + if (value === undefined || value === null) continue; + if (parameter.group === "headers") { + const text = String(value); + if (/[\r\n]/.test(text)) throw failure(422, "unikraft_arguments_invalid", "Invalid Unikraft header argument."); + projectedHeaders[parameter.name] = text; + } else if (Array.isArray(value)) { + if (parameter.explode) value.forEach(item => url.searchParams.append(parameter.name, String(item))); + else url.searchParams.append(parameter.name, value.join(",")); + } else url.searchParams.append(parameter.name, String(value)); + } + let body: string | undefined; + // Unikraft selects batch targets with JSON bodies, including on GET and + // DELETE. The pinned transport sends them as ordinary request bodies. + if (parameters.body !== undefined) { + body = JSON.stringify(parameters.body); + if (Buffer.byteLength(body) > NATIVE_REST_MAX_BYTES) throw failure(422, "unikraft_input_limit", "The request body exceeds the limit."); + projectedHeaders["Content-Type"] = "application/json"; + } + // The sandbox plugin documents 408 as "the command is still running when + // the wait timed out", an ordinary outcome rather than a failure. + const expected = operation.kind === "sandbox" && operation.path === "/commands/{uuid}/wait_timeout" + ? { 408: { status: "running", timedOut: true, message: "The command is still running. Wait again or check its logs." } } : {}; + // No automatic retry. A transport failure cannot prove a write did not run. + return send(url.toString(), { method: operation.method, headers: projectedHeaders, body }, {}, expected); + } + return { probe, execute }; +} + +type Binding = { companyId: string; agentId: string | null }; +type Connection = { companyId: string; config: Record }; +export async function executeUnikraftTool(input: { + binding: Binding; connection: Connection; headers: Record; + request: NativeRestRequest; toolName: string; parameters: unknown; + signal?: AbortSignal; + deadlineAt?: number; +}) { + if (!input.binding.agentId || input.connection.companyId !== input.binding.companyId) { + throw failure(404, "unikraft_connection_not_found", "Connection not found."); + } + const operation = contract.operation(input.toolName); + if (!operation) throw failure(404, "unikraft_tool_not_found", "Unikraft action not found."); + if (!contract.validate(operation.name, input.parameters)) throw failure(422, "unikraft_arguments_invalid", "Invalid Unikraft action arguments."); + const config = record(input.connection.config.methodConfig); + return unikraftClient(input.headers, input.request, config.metro ?? UNIKRAFT_DEFAULT_METRO, input.signal, input.deadlineAt) + .execute(operation, record(input.parameters)); +} diff --git a/server/src/services/connectors/unikraft/operations.generated.json b/server/src/services/connectors/unikraft/operations.generated.json new file mode 100644 index 00000000000..3a572def95e --- /dev/null +++ b/server/src/services/connectors/unikraft/operations.generated.json @@ -0,0 +1,11837 @@ +{ + "schemaVersion": 1, + "sources": { + "platform": { + "url": "https://raw.githubusercontent.com/unikraft-cloud/openapi/5dd107ea1cd8716b83cb86a83ed2a6ee46f99b5b/platform.json", + "sha256": "b9e3d4f304bdaf93e36089a13d44196ad0977d91f8e69fd010c5ddaf7d7a760b" + }, + "sandbox": { + "url": "https://raw.githubusercontent.com/unikraft-cloud/openapi/5dd107ea1cd8716b83cb86a83ed2a6ee46f99b5b/plugins/sandbox.json", + "sha256": "ac687a0f04d0fa286aa41fa577fe2e8870b55de6b9b60f83f5d6958ff012a2cb" + } + }, + "metros": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "tools": [ + { + "name": "unikraft_create_certificate", + "kind": "platform", + "path": "/v1/certificates", + "method": "POST", + "parameters": [], + "bodyContentType": "application/json", + "title": "Create Certificate", + "description": "Upload a new certificate.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "$ref": "#/definitions/Certificates.CreateCertificateRequest" + } + }, + "required": [ + "body" + ], + "definitions": { + "Certificates.CreateCertificateRequest": { + "type": "object", + "required": [ + "chain", + "pkey" + ], + "properties": { + "name": { + "type": "string", + "description": "The name of the certificate.\n\nThis is a human-readable name that can be used to identify the certificate.\nThe name must be unique within the context of your account. If no name is\nspecified, a random name is generated for you. The name can also be used\nto identify the certificate in API calls." + }, + "cn": { + "type": "string", + "description": "The common name (CN) of the certificate.\n\nDeprecated: Use `common_name` instead.", + "deprecated": true + }, + "common_name": { + "type": "string", + "description": "The common name (CN) of the certificate.\n\nThis must be a fully-qualified domain name (FQDN). Exactly one of `cn`\nor `common_name` must be specified." + }, + "chain": { + "type": "string", + "description": "The certificate chain in PEM format. Required for user-uploaded certificates." + }, + "pkey": { + "type": "string", + "description": "The private key in PEM format. Required for user-uploaded certificates." + } + }, + "description": "The request message for creating/uploading a new certificate." + } + } + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_get_certificates", + "kind": "platform", + "path": "/v1/certificates", + "method": "GET", + "parameters": [ + { + "name": "uuid", + "group": "query", + "style": "form", + "explode": true + }, + { + "name": "name", + "group": "query", + "style": "form", + "explode": true + }, + { + "name": "details", + "group": "query", + "style": "form", + "explode": false + }, + { + "name": "count", + "group": "query", + "style": "form", + "explode": false + }, + { + "name": "from", + "group": "query", + "style": "form", + "explode": false + }, + { + "name": "order", + "group": "query", + "style": "form", + "explode": false + }, + { + "name": "sortby", + "group": "query", + "style": "form", + "explode": false + } + ], + "bodyContentType": "application/json", + "title": "List Certificates", + "description": "List certificates.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "query": { + "type": "object", + "properties": { + "uuid": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The UUID of the resource." + }, + "name": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The name of the resource." + }, + "details": { + "type": "boolean" + }, + "count": { + "type": "integer", + "minimum": 0 + }, + "from": { + "type": "string" + }, + "order": { + "$ref": "#/definitions/Common.PaginationOrder" + }, + "sortby": { + "$ref": "#/definitions/Common.PaginationSortBy" + } + }, + "required": [], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "type": "array", + "items": { + "$ref": "#/definitions/Common.NameOrUUID" + } + } + }, + "required": [], + "definitions": { + "Common.PaginationOrder": { + "type": "string", + "enum": [ + "asc", + "desc" + ], + "description": "The sort order used by list endpoints." + }, + "Common.PaginationSortBy": { + "type": "string", + "enum": [ + "create_time" + ], + "description": "The sort field used by list endpoints." + }, + "Common.NameOrUUID": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "The UUID of the resource." + }, + "name": { + "type": "string", + "description": "The name of the resource." + } + }, + "description": "An identifier for a resource — either a name or a UUID, but not both.", + "oneOf": [ + { + "required": [ + "uuid" + ], + "not": { + "required": [ + "name" + ] + } + }, + { + "required": [ + "name" + ], + "not": { + "required": [ + "uuid" + ] + } + } + ] + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + { + "name": "unikraft_delete_certificates", + "kind": "platform", + "path": "/v1/certificates", + "method": "DELETE", + "parameters": [], + "bodyContentType": "application/json", + "title": "Delete Certificates", + "description": "Delete certificates by ID(s).", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "type": "array", + "items": { + "$ref": "#/definitions/Common.NameOrUUID" + } + } + }, + "required": [ + "body" + ], + "definitions": { + "Common.NameOrUUID": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "The UUID of the resource." + }, + "name": { + "type": "string", + "description": "The name of the resource." + } + }, + "description": "An identifier for a resource — either a name or a UUID, but not both.", + "oneOf": [ + { + "required": [ + "uuid" + ], + "not": { + "required": [ + "name" + ] + } + }, + { + "required": [ + "name" + ], + "not": { + "required": [ + "uuid" + ] + } + } + ] + } + } + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_update_certificates", + "kind": "platform", + "path": "/v1/certificates", + "method": "PUT", + "parameters": [], + "bodyContentType": "application/json", + "title": "Update Certificates", + "description": "Update certificates.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "type": "array", + "items": { + "$ref": "#/definitions/Certificates.UpdateCertificatesRequestItem" + } + } + }, + "required": [ + "body" + ], + "definitions": { + "Certificates.UpdateCertificatesRequestItem": { + "type": "object", + "required": [ + "chain", + "pkey" + ], + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "The UUID of the resource." + }, + "name": { + "type": "string", + "description": "The name of the resource." + }, + "chain": { + "type": "string", + "description": "The new certificate chain. This is the public chain of the certificate in\nPEM format. The chain should include the certificate and any intermediate\ncertificates." + }, + "pkey": { + "type": "string", + "description": "The new private key. This is the private key of the certificate in PEM\nformat. The private key must match the public key in the certificate\nchain." + } + }, + "description": "A single update operation to be applied to a certificate." + } + } + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_get_certificate_by_uuid", + "kind": "platform", + "path": "/v1/certificates/{uuid}", + "method": "GET", + "parameters": [ + { + "name": "uuid", + "group": "path", + "style": "form", + "explode": true + } + ], + "title": "Get Certificate by UUID", + "description": "Get a certificate by UUID.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "uuid" + ], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + } + }, + "required": [ + "path" + ] + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + { + "name": "unikraft_delete_certificate_by_uuid", + "kind": "platform", + "path": "/v1/certificates/{uuid}", + "method": "DELETE", + "parameters": [ + { + "name": "uuid", + "group": "path", + "style": "form", + "explode": true + } + ], + "title": "Delete Certificate by UUID", + "description": "Delete a certificate by UUID.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "uuid" + ], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + } + }, + "required": [ + "path" + ] + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_update_certificate_by_uuid", + "kind": "platform", + "path": "/v1/certificates/{uuid}", + "method": "PUT", + "parameters": [ + { + "name": "uuid", + "group": "path", + "style": "form", + "explode": true + } + ], + "bodyContentType": "application/json", + "title": "Update Certificate by UUID", + "description": "Update a certificate by UUID.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "uuid" + ], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "$ref": "#/definitions/Certificates.UpdateCertificateByUUIDRequestBody" + } + }, + "required": [ + "path", + "body" + ], + "definitions": { + "Certificates.UpdateCertificateByUUIDRequestBody": { + "type": "object", + "required": [ + "chain", + "pkey" + ], + "properties": { + "chain": { + "type": "string", + "description": "The new certificate chain. This is the public chain of the certificate in\nPEM format. The chain should include the certificate and any intermediate\ncertificates." + }, + "pkey": { + "type": "string", + "description": "The new private key. This is the private key of the certificate in PEM\nformat. The private key must match the public key in the certificate\nchain." + } + }, + "description": "The request body for updating a certificate by its UUID or name." + } + } + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_healthz", + "kind": "platform", + "path": "/v1/healthz", + "method": "GET", + "parameters": [], + "title": "Health Check", + "description": "Return the status of a full-system health check.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + } + }, + "required": [] + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + { + "name": "unikraft_get_image_store", + "kind": "platform", + "path": "/v1/image-store", + "method": "GET", + "parameters": [ + { + "name": "digest", + "group": "query", + "style": "form", + "explode": false + }, + { + "name": "tag", + "group": "query", + "style": "form", + "explode": false + } + ], + "bodyContentType": "application/json", + "title": "Get Images in Store", + "description": "Retrieve all images in store.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "query": { + "type": "object", + "properties": { + "digest": { + "type": "string" + }, + "tag": { + "type": "string" + } + }, + "required": [], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "type": "array", + "items": { + "$ref": "#/definitions/Images.GetImagesRequestTagOrDigest" + } + } + }, + "required": [], + "definitions": { + "Images.GetImagesRequestTagOrDigest": { + "type": "object", + "properties": { + "digest": { + "type": "string" + }, + "tag": { + "type": "string" + } + }, + "oneOf": [ + { + "required": [ + "digest" + ] + }, + { + "required": [ + "tag" + ] + } + ] + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + { + "name": "unikraft_get_images", + "kind": "platform", + "path": "/v1/images", + "method": "GET", + "parameters": [ + { + "name": "digest", + "group": "query", + "style": "form", + "explode": false + }, + { + "name": "tag", + "group": "query", + "style": "form", + "explode": false + } + ], + "bodyContentType": "application/json", + "title": "Get Images", + "description": "Retrieve all images.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "query": { + "type": "object", + "properties": { + "digest": { + "type": "string" + }, + "tag": { + "type": "string" + } + }, + "required": [], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "type": "array", + "items": { + "$ref": "#/definitions/Images.GetImagesRequestTagOrDigest" + } + } + }, + "required": [], + "definitions": { + "Images.GetImagesRequestTagOrDigest": { + "type": "object", + "properties": { + "digest": { + "type": "string" + }, + "tag": { + "type": "string" + } + }, + "oneOf": [ + { + "required": [ + "digest" + ] + }, + { + "required": [ + "tag" + ] + } + ] + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + { + "name": "unikraft_pin_images", + "kind": "platform", + "path": "/v1/images", + "method": "POST", + "parameters": [], + "bodyContentType": "application/json", + "title": "Pin Images", + "description": "Pull and pin one or more images so they stay cached and are never\nevicted, without relying on an on-demand pull to succeed at instance\nstart. If a pull fails, the agent's error is returned in the entry's\n`message` field.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "type": "array", + "items": { + "$ref": "#/definitions/Images.PinImageRequestItem" + } + } + }, + "required": [ + "body" + ], + "definitions": { + "Images.PinImageRequestItem": { + "type": "object", + "required": [ + "url", + "timeout_s" + ], + "properties": { + "url": { + "type": "string", + "description": "The image URL to pull and pin." + }, + "credentials": { + "type": "string", + "description": "Optional credentials for authenticating to an OCI registry.\nOnly valid for OCI registry URLs; the platform rejects this\nfield for non-OCI schemes." + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Optional HTTP headers to send when fetching the image." + }, + "pull_policy": { + "allOf": [ + { + "$ref": "#/definitions/Instances.PullPolicy" + } + ], + "description": "Controls when the image is pulled relative to what is already cached on\nthe node. If unset, this is inferred from the URL." + }, + "timeout_s": { + "type": "integer", + "description": "Number of seconds to wait for the pull to complete. Required and must\nbe non-zero; `-1` waits up to the platform's maximum timeout." + }, + "merge_requests": { + "type": "boolean", + "description": "Avoid duplicate pulls by merging with any in-flight request for the\nsame image. Defaults to `true`." + }, + "autokill": { + "allOf": [ + { + "$ref": "#/definitions/Images.PinImageRequestItemAutokill" + } + ], + "description": "Automatically unpin the image after a period of inactivity." + } + }, + "description": "The request item for pinning a single image." + }, + "Instances.PullPolicy": { + "type": "string", + "enum": [ + "always", + "if_not_present", + "never" + ], + "description": "PullPolicy defines when an image should be pulled." + }, + "Images.PinImageRequestItemAutokill": { + "type": "object", + "required": [ + "time_ms" + ], + "properties": { + "time_ms": { + "type": "integer", + "description": "Automatically unpin the image after this many milliseconds of\ninactivity. `0` (the default) disables this.", + "minimum": 0 + } + }, + "description": "Automatically unpin the image after a period of inactivity." + } + } + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_unpin_images", + "kind": "platform", + "path": "/v1/images", + "method": "DELETE", + "parameters": [], + "bodyContentType": "application/json", + "title": "Unpin Images", + "description": "Unpin one or more images by UUID, making them eligible for normal\ncache eviction again. This does not delete the image.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "type": "array", + "items": { + "$ref": "#/definitions/Images.UnpinImageRequestItem" + } + } + }, + "required": [ + "body" + ], + "definitions": { + "Images.UnpinImageRequestItem": { + "type": "object", + "required": [ + "uuid" + ], + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "The UUID of the image to unpin. Only UUID is supported; name, URL,\ntag, and digest are not." + } + }, + "description": "The request item for unpinning a single image." + } + } + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_create_instance", + "kind": "platform", + "path": "/v1/instances", + "method": "POST", + "parameters": [], + "bodyContentType": "application/json", + "title": "Create Instance", + "description": "Create an instance in Unikraft Cloud.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "$ref": "#/definitions/Instances.CreateInstanceRequest" + } + }, + "required": [ + "body" + ], + "definitions": { + "Instances.CreateInstanceRequest": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of the instance.\n\nIf not provided, a random name will be generated. The name must be\nunique." + }, + "image": { + "anyOf": [ + { + "type": "string" + }, + { + "$ref": "#/definitions/Instances.ImageSpec" + } + ], + "description": "The image to use for the instance.\n\nEither an image or a template must be specified. Accepts either a plain\nimage reference string (`\"nginx:latest\"`) or an object carrying additional\npull configuration (`{\"url\": \"nginx:latest\", \"pull_policy\": \"always\"}`)." + }, + "args": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The arguments to pass to the instance when it starts." + }, + "env": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Environment variables to set for the instance." + }, + "memory_mb": { + "type": "integer", + "description": "Memory in MB to allocate for the instance. Default is 128." + }, + "service_group": { + "allOf": [ + { + "$ref": "#/definitions/Instances.CreateInstanceRequestServiceGroup" + } + ], + "description": "The service group configuration when creating an instance.\n\nWhen creating an instance, either a previously created (persistent)\nservice group can be referenced (either through its name or UUID), or a\nnew (ephemeral) service group can be created for the instance by\nspecifying the list of services it should expose and optionally the\ndomains it should use. Not used by template instances." + }, + "volumes": { + "type": "array", + "items": { + "$ref": "#/definitions/Instances.CreateInstanceRequestVolume" + }, + "description": "Volumes to attach to the instance.\n\nThis list can contain both existing and new volumes to create as part of\nthe instance creation. Existing volumes can be referenced by their name or\nUUID. New volumes can be created by specifying a name, size in MiB, and\nmount point in the instance. The mount point is the directory in the\ninstance where the volume will be mounted." + }, + "autostart": { + "type": "boolean", + "description": "Whether the instance should start automatically on creation.\nMust be set to true when `timeout_s` is specified." + }, + "replicas": { + "type": "integer", + "description": "Number of additional replicas to create. The total number of instances\ncreated is `replicas + 1`. Defaults to 0." + }, + "restart_policy": { + "allOf": [ + { + "$ref": "#/definitions/Instances.InstanceRestartPolicy" + } + ], + "description": "Restart policy for the instance. This defines how the instance\nshould behave when it stops or crashes. Cannot be combined with\nthe `delete-on-stop` feature." + }, + "scale_to_zero": { + "allOf": [ + { + "$ref": "#/definitions/Instances.CreateInstanceScaleToZero" + } + ], + "description": "Scale-to-zero configuration for the instance. Requires\n`service_group` to be set. Cannot be combined with the\n`delete-on-stop` feature." + }, + "vcpus": { + "type": "integer", + "description": "Number of vCPUs to allocate for the instance. Defaults to 1." + }, + "wait_timeout_ms": { + "type": "integer", + "description": "Deprecated: Use `timeout_s` instead. Timeout in milliseconds to\nwait for all new instances to reach running state. Requires\n`autostart` to be set. If `timeout_s` is not set, this value is\nconverted by rounding up to the next full second. No wait\nperformed for a value of 0.", + "deprecated": true + }, + "features": { + "type": "array", + "items": { + "$ref": "#/definitions/Instances.InstanceFeature" + }, + "description": "Features to enable for the instance. Features are specific\nconfigurations or capabilities that can be enabled for the\ninstance. The `scale-to-zero` and `delete-on-stop` features are\nmutually exclusive." + }, + "timeout_s": { + "type": "integer", + "description": "Timeout in seconds to wait for all new instances to reach running\nstate. Requires `autostart` to be set. If you autostart your\nnew instance, you can wait for it to finish starting with a\nblocking API call if you specify a wait timeout greater than\nzero. No wait performed for a value of 0." + }, + "roms": { + "type": "array", + "items": { + "$ref": "#/definitions/Instances.CreateInstanceRequestRom" + }, + "description": "Read-Only Memory (ROM) blobs to attach to the instance.\nUnikraft Cloud supports the ability to attach Read-Only Memory (ROM) blobs\nto instances. It allows you to create a general-purpose base image and\nthen customize individual instances by attaching code or data as separate\nROM blobs." + }, + "plugins": { + "type": "array", + "items": { + "$ref": "#/definitions/Instances.CreateInstanceRequestPlugin" + }, + "description": "Plugins to attach to the instance. Plugins let you attach small helper\nprograms to an instance and reach each one over a direct, authenticated\nHTTP endpoint. Each plugin loads from its own ROM image, mounts at\n`/uk/plugins/`, and is reachable at\n`.../v1/instances//plugins//`. At most 8 plugins\nmay be attached to an instance." + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Tags to associate with the instance." + }, + "annotations": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Annotations to associate with the instance.\n\nUnlike tags, annotations also reach the guest: they are included in the\ninstance's startdata, and selected keys can be injected into the console\nlog output.\n\nKeys follow the Kubernetes annotation key syntax, `[/]`:\nthe optional prefix is a non-wildcard DNS subdomain of at most 253\ncharacters, and the name is at most 63 characters of `[-_.a-zA-Z0-9]`\nstarting and ending with an alphanumeric. Values are unconstrained apart\nfrom ASCII control characters. An instance holds at most 256 annotations.\n\nWhen the instance inherits annotations from a template, branch, or\ncheckpoint, the given annotations are merged into them rather than\nreplacing them. On a key clash the value given here wins." + }, + "template": { + "allOf": [ + { + "$ref": "#/definitions/Instances.CreateInstanceRequestTemplate" + } + ], + "description": "Template instances.\nAn existing instance can be saved as a template. This template is then\nused to create new instances that inherit the exact configuration and\nstate the original instance had when the template was created." + }, + "sched_priority": { + "allOf": [ + { + "$ref": "#/definitions/Common.SchedPriority" + } + ], + "description": "The scheduling priority for the instance. Only settable by\nusers with scheduling priority override permissions." + }, + "schedules": { + "type": "array", + "items": { + "$ref": "#/definitions/Instances.Schedule" + }, + "description": "Schedules for the instance. Scheduled operations let you automatically\nstart, stop, delete, or exec a command in the instance on a calendar-\nbased schedule. For `exec` schedules, set the `args` field to the command\nand its arguments. Each instance stores its own schedules, and cloning\npreserves them." + }, + "autokill": { + "allOf": [ + { + "$ref": "#/definitions/Instances.CreateInstanceRequestAutokill" + } + ], + "description": "Automatic delete-on-idle/request-limit configuration. Not used for\ntemplate instances." + }, + "hostname": { + "type": "string", + "description": "The hostname of the instance.\n\nIf not provided, the hostname will be set to the instance name. The\nhostname must be a valid DNS label (e.g., \"my-instance\") and is used for\ninternal DNS resolution within the Unikraft Cloud network." + }, + "dependencies": { + "type": "array", + "items": { + "$ref": "#/definitions/Common.NameOrUUID" + }, + "description": "Dependencies of the instance.\n\nA list of instance identifiers (name or UUID) that this instance depends\non. Dependencies define startup ordering and can be used to ensure that\nprerequisite instances are running before this instance starts." + }, + "branch_from": { + "allOf": [ + { + "$ref": "#/definitions/Common.NameOrUUID" + } + ], + "description": "Reference to an existing instance to branch from. The instance can be\nrunning or stopped. If the source instance is running, a snapshot will be\ntaken asynchronously and the new instance will wait for it to complete\nbefore starting. Mutually exclusive with `image` and `template`." + }, + "checkpoint": { + "allOf": [ + { + "$ref": "#/definitions/Common.NameOrUUID" + } + ], + "description": "Reference to an existing checkpoint to create the instance from. The\ncheckpoint must be in the `checkpoint` state. The new instance will be\ncreated with the same configuration and state as the checkpoint. Mutually\nexclusive with `image`, `template`, and `branch_from`." + }, + "gateway": { + "type": "string", + "description": "The default gateway to configure inside the guest." + }, + "nameserver": { + "type": "string", + "description": "The DNS resolver to configure inside the guest." + }, + "network_interfaces": { + "type": "array", + "items": { + "$ref": "#/definitions/Instances.CreateInstanceRequestNetworkInterface" + }, + "description": "Network interfaces to attach to the instance." + }, + "type": { + "allOf": [ + { + "$ref": "#/definitions/Instances.InstanceType" + } + ], + "description": "The type of virtual machine to use for the instance. Defaults to `micro`,\nwhich runs on Firecracker. `full` runs on QEMU instead and is required\nfor GPU passthrough (see `gpus`) and, in the future, Windows VMs. QEMU-\nbacked instances currently do not support scale-to-zero, templates,\nbranching, or checkpointing, and only support block-based volumes (no\nvirtiofs). Requires a plan with full VM support and cannot be combined\nwith `template`, `branch_from`, or `checkpoint`." + }, + "gpus": { + "type": "integer", + "minimum": 0, + "maximum": 1, + "description": "Number of GPUs to attach to the instance. Currently restricted to at most\n1. Requires `type` to be `full` and a plan with GPU support. A GPU stays\nassigned to the instance, even while stopped, until the instance is\ndeleted. Cannot be combined with `template`, `branch_from`, or\n`checkpoint`." + } + }, + "description": "The request message for creating a new instance." + }, + "Instances.ImageSpec": { + "type": "object", + "required": [ + "url" + ], + "properties": { + "url": { + "type": "string", + "description": "The image URL" + }, + "credentials": { + "type": "string", + "description": "Optional credentials for authenticating to an OCI registry.\nOnly valid for OCI registry URLs; the platform rejects this\nfield for non-OCI schemes." + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Optional HTTP headers to send when fetching the image." + }, + "pull_policy": { + "allOf": [ + { + "$ref": "#/definitions/Instances.PullPolicy" + } + ], + "description": "Controls when the image is pulled relative to what is already cached on\nthe node." + } + }, + "description": "ImageSpec describes an image to use for an instance." + }, + "Instances.PullPolicy": { + "type": "string", + "enum": [ + "always", + "if_not_present", + "never" + ], + "description": "PullPolicy defines when an image should be pulled." + }, + "Instances.CreateInstanceRequestServiceGroup": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "The UUID of the resource." + }, + "name": { + "type": "string", + "description": "The name of the resource." + }, + "domains": { + "type": "array", + "items": { + "$ref": "#/definitions/Instances.CreateInstanceRequestDomain" + } + }, + "services": { + "type": "array", + "items": { + "$ref": "#/definitions/Services.Service" + } + }, + "soft_limit": { + "type": "integer", + "minimum": 0 + }, + "hard_limit": { + "type": "integer", + "minimum": 0 + } + }, + "description": "The service group configuration when creating an instance. If no existing\n(persistent) service group is specified via its identifier, a new\n(ephemeral) service group can be created by specifying the services it\nshould expose. A service defines the configuration settings of an exposed\nport by the instance. A service is a combination of a public port, an\ninternal port, and a set of handlers that define how the service will handle\nincoming connections.", + "oneOf": [ + { + "required": [ + "uuid" + ] + }, + { + "required": [ + "name" + ] + } + ] + }, + "Instances.CreateInstanceRequestDomain": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "description": "Publicly accessible domain name.\n\nIf this name ends in a period `.` it must be a valid Full Qualified\nDomain Name (FQDN), e.g. `example.com.`; otherwise it will become a\nsubdomain of the target metro, e.g. `example` becomes\n`example.fra0.unikraft.app`." + }, + "certificate": { + "allOf": [ + { + "$ref": "#/definitions/Common.NameOrUUID" + } + ], + "description": "A reference to an existing certificate which can be used for the\nspecified domain. If unspecified, Unikraft Cloud will\nautomatically generate a new certificate for the domain based on Let's\nEncrypt and seek to accomplish a DNS-01 challenge." + } + }, + "description": "The domain configuration for the service group. A domain defines a publicly\naccessible domain name for the instance. If the domain name ends with a\nperiod `.`, it must be a valid Fully Qualified Domain Name (FQDN), otherwise\nit will become a subdomain of the target metro. The domain can be associated\nwith an existing certificate by specifying the certificate's name or UUID.\nIf no certificate is specified and a FQDN is provided, Unikraft Cloud will\nautomatically generate a new certificate for the domain based on Let's\nEncrypt and seek to accomplish a DNS-01 challenge." + }, + "Common.NameOrUUID": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "The UUID of the resource." + }, + "name": { + "type": "string", + "description": "The name of the resource." + } + }, + "description": "An identifier for a resource — either a name or a UUID, but not both.", + "oneOf": [ + { + "required": [ + "uuid" + ], + "not": { + "required": [ + "name" + ] + } + }, + { + "required": [ + "name" + ], + "not": { + "required": [ + "uuid" + ] + } + } + ] + }, + "Services.Service": { + "type": "object", + "required": [ + "port" + ], + "properties": { + "port": { + "type": "integer", + "description": "This is the public-facing port that the service will be accessible from\non the Internet.", + "minimum": 0 + }, + "destination_port": { + "type": "integer", + "description": "The port number that the instance is listening on. This is the internal\nport which Unikraft Cloud will forward traffic to.", + "minimum": 0 + }, + "protocol": { + "$ref": "#/definitions/Services.ServiceProtocol" + }, + "ip": { + "type": "string" + }, + "handlers": { + "type": "array", + "items": { + "$ref": "#/definitions/Services.ConnectionHandler" + }, + "description": "Connection handlers to use for the service. Handlers define how the\nservice will handle incoming connections and forward traffic from the\nInternet to your application. For example, a service can be configured\nto terminate TLS connections, redirect HTTP traffic, or enable HTTP mode\nfor load balancing. You configure the handlers for every published\nservice port individually." + } + }, + "description": "A service connects a public-facing port to an internal destination port on\nwhich an application instance listens on. Additional handlers can be\ndefined for each published port in order to define how the service will\nhandle incoming connections and forward traffic from the Internet to your\napplication. For example, a service can be configured to terminate TLS\nconnections, redirect HTTP traffic, or enable HTTP mode for load balancing." + }, + "Services.ServiceProtocol": { + "type": "string", + "enum": [ + "tcp", + "udp" + ], + "description": "Protocol for a service." + }, + "Services.ConnectionHandler": { + "type": "string", + "enum": [ + "tls", + "http", + "redirect" + ], + "description": "Connection handlers to use for the service.\n\nHandlers define how the service will handle incoming connections and\nforward traffic from the Internet to your application. For example, a\nservice can be configured to terminate TLS connections, redirect HTTP\ntraffic, or enable HTTP mode for load balancing. You configure the handlers\nfor every published service port individually.\n\nThere are currently 3 supported handlers:\n\n| Handler | Description |\n|------------|-------------|\n| `tls` | Terminate the TLS connection at the Unikraft Cloud gateway using our wildcard certificate issued for the kraft.cloud domain. The gateway forwards the unencrypted traffic to your application. |\n| `http` | Enable HTTP mode on the load balancer to load balance on the level of individual HTTP requests. In this mode, only HTTP connections are accepted. If this option is not set the load balancer works in TCP mode and distributes TCP connections. |\n| `redirect` | Redirect traffic from the source port to the destination port. |\n\nNote that there is a set of constraints when publishing ports:\n\n- Port 80: MUST have \"http\" and MUST not have \"tls\" set;\n- Port 443: MUST have http and tls set;\n- The `redirect` handler can only be set on port 80 (HTTP) to redirect to\nport 443 (HTTPS);\n- All other ports MUST have tls and MUST not have http set." + }, + "Instances.CreateInstanceRequestVolume": { + "type": "object", + "required": [ + "at" + ], + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "The UUID of the resource." + }, + "name": { + "type": "string", + "description": "The name of the resource." + }, + "size_mb": { + "type": "integer", + "minimum": 0 + }, + "host_path": { + "type": "string" + }, + "at": { + "type": "string" + }, + "readonly": { + "type": "boolean" + }, + "quota_policy": { + "type": "string" + }, + "filesystem": { + "type": "string" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "uid": { + "type": "integer", + "minimum": 0 + }, + "gid": { + "type": "integer", + "minimum": 0 + }, + "access_mode": { + "$ref": "#/definitions/Volumes.VolumeAccessMode" + }, + "args": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "description": "A volume defines a storage volume that can be attached to the instance.", + "oneOf": [ + { + "required": [ + "size_mb" + ] + }, + { + "required": [ + "host_path" + ] + } + ] + }, + "Volumes.VolumeAccessMode": { + "type": "string", + "enum": [ + "rwo", + "rox", + "rwx" + ], + "description": "VolumeAccessMode defines the access mode of a volume, which controls sharing\nbehavior and caching strategy." + }, + "Instances.InstanceRestartPolicy": { + "type": "string", + "enum": [ + "never", + "always", + "on-failure" + ], + "description": "The restart policy of an instance.\n\nWhen an instance stops either because the application exits or the instance\ncrashes, Unikraft Cloud can auto-restart your instance. Auto-restarts are\nperformed according to the restart policy configured for a particular\ninstance. The policy can have the following values:\n\n| Policy | Description |\n|--------------|-------------|\n| `never` | Never restart the instance (default). |\n| `always` | Always restart the instance when the stop is initiated from within the instance (i.e., the application exits or the instance crashes). |\n| `on-failure` | Only restart the instance if it crashes. |\n\nWhen an instance stops, the stop reason and the configured restart policy\nare evaluated to decide if a restart should be performed. Unikraft Cloud\nuses an exponential back-off delay (immediate, 5s, 10s, 20s, 40s, ..., 5m)\nto slow down restarts in tight crash loops. If an instance runs without\nproblems for 10s the back-off delay is reset and the restart sequence ends.\nThe `restart.attempt` attribute reported in counts the number of restarts\nperformed in the current sequence. The `restart.next_at` field indicates\nwhen the next restart will take place if a back-off delay is in effect. A\nmanual start or stop of the instance aborts the restart sequence and resets\nthe back-off delay." + }, + "Instances.CreateInstanceScaleToZero": { + "type": "object", + "properties": { + "policy": { + "allOf": [ + { + "$ref": "#/definitions/Instances.InstanceScaleToZeroPolicy" + } + ], + "description": "The specific policy to use for scaling the instance to zero." + }, + "stateful": { + "type": "boolean", + "description": "Whether the instance should be stateful when scaled to zero. If set to\ntrue, the instance will retain its state (e.g., RAM contents) when scaled\nto zero. This is useful for instances that need to maintain their state\nacross scale-to-zero operations. If set to false, the instance will lose\nits state when scaled to zero, and it will be restarted from scratch when\nscaled back up." + }, + "cooldown_time_ms": { + "type": "integer", + "description": "The cooldown time in milliseconds before the instance can be scaled to\nzero again. This is useful to prevent rapid scaling to zero and back up,\nwhich can lead to performance issues or resource exhaustion." + }, + "notify_time_ms": { + "type": "integer", + "description": "The notification time in milliseconds before the instance is scaled to\nzero. This allows the instance to perform any necessary cleanup or state\nsaving before being scaled down." + } + }, + "description": "Scale-to-zero configuration when creating an instance." + }, + "Instances.InstanceScaleToZeroPolicy": { + "type": "string", + "enum": [ + "off", + "on", + "idle" + ], + "description": "Scale-to-zero policy types to use when creating an instance.\n\nThe policy can be one of the following values:\n\n| Policy | Description |\n|--------|-------------|\n| `on` | The instance should be scaled-to-zero (when possible). |\n| `off` | The instance should never be scaled-to-zero. |\n| `idle` | The instance should be scaled-to-zero even with established but idle TCP connections. |" + }, + "Instances.InstanceFeature": { + "type": "string", + "enum": [ + "delete-on-stop", + "nested-virt" + ], + "description": "Features are specific configurations or capabilities that can be enabled for\nthe instance.\n\nThe list of available features to enable for the instance:\n\n| Feature | Description |\n|--------------------|-------------|\n| `delete-on-stop` | The instance will be deleted when it is stopped. This is useful for instances that are not needed after they are stopped, such as temporary or ephemeral instances. Cannot be combined with a `restart_policy` other than `never`. |\n| `nested-virt` | Expose virtualization extensions to the guest, so that it can run virtual machines of its own. Requires the `nested_virt` permission. |" + }, + "Instances.CreateInstanceRequestRom": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "description": "The name of the ROM to use for the instance configuration." + }, + "image": { + "anyOf": [ + { + "type": "string" + }, + { + "$ref": "#/definitions/Instances.ImageSpec" + } + ], + "description": "The image of the ROM to use for the instance configuration. Mutually\nexclusive with `files`. Accepts either a plain image reference string\n(`\"nginx:latest\"`) or an object carrying additional pull configuration\n(`{\"url\": \"nginx:latest\", \"pull_policy\": \"always\"}`)." + }, + "at": { + "type": "string", + "description": "The path at which the ROM should be automatically mounted inside the\ninstance. When set, the platform mounts the ROM device at the specified\npath so the guest does not need to mount it manually. When omitted, the\nROM is exposed as a raw block device and the guest is responsible for\nmounting it." + }, + "files": { + "type": "array", + "items": { + "$ref": "#/definitions/Common.InlineFile" + }, + "description": "Inline files to use as the ROM content. When specified, the platform\ncreates an EROFS image from the provided files. Mutually exclusive with\n`image`." + } + }, + "description": "A ROM to attach when creating an instance." + }, + "Common.InlineFile": { + "type": "object", + "required": [ + "path", + "data" + ], + "properties": { + "path": { + "type": "string", + "description": "The file path within the image." + }, + "encoding": { + "allOf": [ + { + "$ref": "#/definitions/Common.InlineDataEncoding" + } + ], + "description": "The encoding of the data field. Defaults to \"text\"." + }, + "data": { + "type": "string", + "description": "The file data, encoded according to the encoding field." + } + }, + "description": "An inline file entry represents a single file within an image." + }, + "Common.InlineDataEncoding": { + "type": "string", + "enum": [ + "text", + "base64" + ], + "description": "Encoding type for inline file data." + }, + "Instances.CreateInstanceRequestPlugin": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "description": "The plugin name. It becomes the `` segment in the plugin\nendpoint (`.../plugins//`). A plugin name has a\nmaximum length of 63 characters and contains only letters (`a`-`z`,\n`A`-`Z`), digits (`0`-`9`), hyphen (`-`), and underscore (`_`)." + }, + "image": { + "anyOf": [ + { + "type": "string" + }, + { + "$ref": "#/definitions/Instances.ImageSpec" + } + ], + "description": "The plugin's image. The platform loads the image, mounts it at\n`/uk/plugins/`, and runs its `init` program when the plugin\nstarts. Accepts either a plain image reference string\n(`\"user/myplugin:latest\"`) or an object carrying additional pull\nconfiguration (`{\"url\": \"user/myplugin:latest\", \"pull_policy\": \"always\"}`).\nExactly one of `image` and `rom` must be set." + }, + "rom": { + "anyOf": [ + { + "type": "string" + }, + { + "$ref": "#/definitions/Instances.ImageSpec" + } + ], + "description": "The plugin's image, under its former name. The platform still accepts\nit and adds a deprecation warning to the response. Exactly one of\n`image` and `rom` must be set.\n\nDeprecated: Use `image` instead.", + "deprecated": true + }, + "config": { + "description": "Arbitrary JSON configuration that the platform passes to the plugin's\n`init` program on `STDIN`. Any JSON value works, including a string, a\nnumber, or an object." + } + }, + "description": "A helper program attached to the instance and reachable over a direct,\nauthenticated HTTP endpoint. A plugin runs inside the instance next to\nthe main application, loads from its own ROM image, and answers requests\nthat the Unikraft Cloud API forwards to it.", + "oneOf": [ + { + "required": [ + "image" + ] + }, + { + "required": [ + "rom" + ] + } + ] + }, + "Instances.CreateInstanceRequestTemplate": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "The UUID of the resource." + }, + "name": { + "type": "string", + "description": "The name of the resource." + }, + "prepare": { + "type": "boolean", + "description": "Whether the instance needs to run in order to reach template state" + }, + "create_args": { + "allOf": [ + { + "$ref": "#/definitions/Instances.Instance" + } + ], + "description": "Configuration parameters to apply when building the new instance from the\nsource template." + }, + "prepare_timeout_s": { + "type": "integer", + "description": "Timeout in seconds for preparing the template before the preparation is\naborted. Only applies when `prepare` is set. A value of 0 means no\ntimeout." + }, + "autokill": { + "allOf": [ + { + "$ref": "#/definitions/Instances.TemplateAutokill" + } + ], + "description": "Automatic delete-on-idle configuration for the template. Only applies\nwhen `prepare` is set." + } + }, + "description": "Template configuration when creating an instance." + }, + "Instances.Instance": { + "type": "object", + "required": [ + "uuid", + "name", + "created_at", + "state", + "image", + "memory_mb", + "vcpus", + "restart_policy", + "type" + ], + "properties": { + "status": { + "allOf": [ + { + "$ref": "#/definitions/Common.ResponseStatus" + } + ], + "description": "Indicates whether the operation was successful for this item." + }, + "message": { + "type": "string", + "description": "An optional message providing additional information." + }, + "error": { + "type": "integer", + "description": "An optional error code." + }, + "uuid": { + "type": "string", + "format": "uuid", + "description": "The UUID of the resource." + }, + "name": { + "type": "string", + "description": "The human-readable name of the resource." + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "The time the instance was created." + }, + "state": { + "allOf": [ + { + "$ref": "#/definitions/Instances.InstanceState" + } + ], + "description": "The state of the instance. This indicates the current state of the\ninstance, such as whether it is running, stopped, or in an error state." + }, + "private_fqdn": { + "type": "string", + "description": "The internal hostname of the instance. This address can be used privately\nwithin the Unikraft Cloud network to access the instance. It is not\naccessible from the public Internet." + }, + "image": { + "type": "string", + "description": "The image used to create the instance. This is a reference to the\nUnikraft image that was used to create the instance." + }, + "memory_mb": { + "type": "integer", + "description": "The amount of memory in megabytes allocated for the instance. This is the\ntotal amount of memory that is available to the instance for its\noperations.", + "minimum": 0 + }, + "vcpus": { + "type": "integer", + "description": "The number of vCPUs allocated for the instance. This is the total\nnumber of virtual CPUs that are available to the instance for its\noperations.", + "minimum": 0 + }, + "args": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The arguments passed to the instance when it was started. This is a\nlist of command-line arguments that were provided to the instance at\nstartup. These arguments can be used to configure the behavior of the\ninstance and its applications." + }, + "env": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Environment variables set for the instance." + }, + "start_count": { + "type": "integer", + "description": "The total number of times the instance has been started. This is a counter\nthat increments each time the instance is started, regardless of whether it\nwas manually stopped or restarted. This can be useful for tracking the\nusage of the instance over time and/or for debugging purposes.\n\nNot used for template instances.", + "minimum": 0 + }, + "restart_count": { + "type": "integer", + "description": "The total number of times the instance has been restarted. This is a counter\nthat increments each time the instance has been restarted. This can be\nuseful for tracking the usage of the instance over time and/or for\ndebugging purposes.\nNot used for template instances.", + "minimum": 0 + }, + "started_at": { + "type": "string", + "format": "date-time", + "description": "The time the instance was started. This is the timestamp when the\ninstance was last started.\nNot used for template instances." + }, + "stopped_at": { + "type": "string", + "format": "date-time", + "description": "The time the instance was stopped. This is the timestamp when the\ninstance was last stopped. If the instance is currently running, this\nfield will be empty.\nNot used for template instances." + }, + "uptime_ms": { + "type": "integer", + "description": "The total amount of time the instance has been running in milliseconds.\nNot used for template instances.", + "minimum": 0 + }, + "retained_until": { + "type": "string", + "format": "date-time", + "description": "Time when the instance will be permanently deleted (for deleted instances)." + }, + "vmm_start_time_us": { + "type": "integer", + "description": "(Developer-only). The time taken between the main controller and the\nbeginning of execution of the VMM (Virtual Machine Monitor) measured in\nmicroseconds. This field is primarily used for debugging and performance\nanalysis purposes.\nNot used for template instances.", + "minimum": 0 + }, + "vmm_load_time_us": { + "type": "integer", + "description": "(Developer-only). The time it took the VMM (Virtual Machine Monitor) to\nload the instance's kernel and initramfs into VM memory measured in\nmicroseconds. This field is primarily used for debugging and performance\nanalysis purposes.\nNot used for template instances.", + "minimum": 0 + }, + "vmm_ready_time_us": { + "type": "integer", + "description": "(Developer-only). The time taken for the VMM (Virtual Machine Monitor) to\nbecome ready to execute the instance measured in microseconds. This is the\ntime from when the VMM started until it was ready to execute the instance's\ncode. This field is primarily used for debugging and performance analysis\npurposes.\nNot used for template instances.", + "minimum": 0 + }, + "boot_time_us": { + "type": "integer", + "description": "The boot time of the instance in microseconds. We take a pragmatic\napproach is to define the boot time. We calculate this as the difference\nin time between the moment the virtualization toolstack is invoked to\nrespond to a VM boot request and the moment the OS starts executing user\ncode (i.e., the end of the guest OS boot process). This is essentially the\ntime that a user would experience in a deployment, minus the application\ninitialization time, which we leave out since it is independent from the\nOS.\nNot used for template instances.", + "minimum": 0 + }, + "net_time_us": { + "type": "integer", + "description": "This is the time it took for the user-level application to start listening\non a non-localhost port measured in microseconds. This is the time from\nwhen the instance started until it reasonably ready to start responding to\nnetwork requests. This is useful for measuring the time it takes for the\ninstance to become operationally ready.\nNot used for template instances.", + "minimum": 0 + }, + "template_time_us": { + "type": "integer", + "description": "Template creation time in microseconds.", + "minimum": 0 + }, + "stop_reason": { + "type": "integer", + "description": "The instance stop reason.\n\nProvides reason as to why an instance is stopped or in the process of\nshutting down. The stop reason is a bitmask that tells you the origin of\nthe shutdown:\n\n| Bit | 4 | 3 | 2 | 1 | 0 (LSB) |\n|---------|------------|------------|------------|------------|--------------|\n| Purpose | [F]orced | [U]ser | [P]latform | [A]pp | [K]ernel |\n\n- **Forced**: This was a force stop. A forced stop does not give the\ninstance a chance to perform a clean shutdown. Bits 0\n(Kernel) and 1 (App) can thus never be set for forced\nshutdowns. Consequently, there won't be an `exit_code` or\n`stop_code`.\n- **User**: Stop initiated by user, e.g. via an API call.\n- **Platform**: Stop initiated by platform, e.g. an autoscale policy.\n- **App**: The Application exited. The `exit_code` field will be set.\n- **Kernel**: The kernel exited. The `stop_code` field will be set.\n\nFor example, the stop reason will contain the following values in the given\nscenarios:\n\n| Value | Bitmask | Aliases | Scenario |\n|-------|---------|---------|----------|\n| 28 | `11100` | `FUP--` | Forced user-initiated shutdown. |\n| 15 | `01111` | `-UPAK` | Regular user-initiated shutdown. The application and kernel have exited. The exit_code and stop_code indicate if the application and kernel shut down cleanly. |\n| 13 | `01101` | `-UP-K` | The user initiated a shutdown but the application was forcefully killed by the kernel during shutdown. This can be the case if the image does not support a clean application exit or the application crashed after receiving a termination signal. The exit_code won’t be present in this scenario. |\n| 7 | `00111` | `--PAK` | Unikraft Cloud initiated the shutdown, for example, due to scale-to-zero. The application and kernel have exited. The exit_code and stop_code indicate if the application and kernel shut down cleanly. |\n| 3 | `00011` | `---AK` | The application exited. The exit_code and stop_code indicate if the application and kernel shut down cleanly. |\n| 1 | `00001` | `----K` | The instance likely expierenced a fatal crash and the stop_code contains more information about the cause of the crash. |\n| 0 | `00000` | `-----` | The stop reason is unknown. |\nNot used for template instances.", + "minimum": 0 + }, + "exit_code": { + "type": "integer", + "description": "The application exit code.\n\nThis is the code which the application returns upon leaving its main entry\npoint. The encoding of the exit code is application specific. See the\ndocumentation of the application for more details. Usually, an exit code\nof `0` indicates success / no failure.\nNot used for template instances.", + "minimum": 0 + }, + "stop_code": { + "type": "integer", + "description": "The kernel stop code.\n\nThis value encodes multiple details about the stop irrespective of the\napplication.\n\n```\nMSB LSB\n┌──────────────┬──────────┬──────────┬───────────┬────────┐\n│ 31 ────── 24 │ 23 ── 16 │ 15 │ 14 ──── 8 │ 7 ── 0 │\n├──────────────┼──────────┼──────────┼───────────┼────────┤\n│ reserved[^1] │ errno │ shutdown │ initlevel │ reason │\n└──────────────┴──────────┴──────────┴───────────┴────────┘\n```\n\n- **errno**: The application errno, using Linux's errno.h values.\n(Optional, can be 0.)\n- **shutdown**: Whether the shutdown originated from the inittable (0) or\nfrom the termtable (1).\n- **initlevel**: The initlevel at the time of the stop.\n- **reason**: The reason for the stop. See `StopCodeReason`.\n\n[^1]: Reserved for future use.\nNot used for template instances.", + "minimum": 0 + }, + "restart_policy": { + "allOf": [ + { + "$ref": "#/definitions/Instances.InstanceRestartPolicy" + } + ], + "description": "The restart configuration for the instance.\n\nWhen an instance stops either because the application exits or the instance\ncrashes, Unikraft Cloud can auto-restart your instance. Auto-restarts are\nperformed according to the restart policy configured for a particular\ninstance.\n\nThe policy can have the following values:\n\n| Policy | Description |\n|--------------|-------------|\n| `never` | Never restart the instance (default). |\n| `always` | Always restart the instance when the stop is initiated from within the instance (i.e., the application exits or the instance crashes). |\n| `on-failure` | Only restart the instance if it crashes. |\n\nWhen an instance stops, the stop reason and the configured restart policy\nare evaluated to decide if a restart should be performed. Unikraft Cloud\nuses an exponential back-off delay (immediate, 5s, 10s, 20s, 40s, ..., 5m)\nto slow down restarts in tight crash loops. If an instance runs without\nproblems for 10s the back-off delay is reset and the restart sequence ends.\n\nThe `restart.attempt` attribute reported in counts the number of restarts\nperformed in the current sequence. The `restart.next_at` field indicates\nwhen the next restart will take place if a back-off delay is in effect.\n\nA manual start or stop of the instance aborts the restart sequence and\nresets the back-off delay." + }, + "scale_to_zero": { + "allOf": [ + { + "$ref": "#/definitions/Instances.InstanceScaleToZero" + } + ], + "description": "The scale-to-zero configuration for the instance.\n\nWith conventional cloud platforms you need to keep at least one instance\nrunning at all times to be able to respond to incoming requests. Performing\na just-in-time cold boot is simply too time-consuming and would create a\nresponse latency of multiple seconds. This is not the case with Unikraft\nCloud. Instances on Unikraft Cloud are able to cold boot within\nmilliseconds, which allows us to perform low-latency scale-to-zero.\n\nTo enable scale-to-zero for an instance it is sufficient to add a\n`scale_to_zero` configuration block. Unikraft Cloud will then put the\ninstance into standby if there is no traffic to your service within the\nwindow of a cooldown period. When there is new traffic coming in, it is\nautomatically started again.\n\nIf you have a heavyweight application that takes long to cold boot or has\nbad first request latency (e.g., with JIT compilation) consider to enable\nstateful scale-to-zero." + }, + "service_group": { + "allOf": [ + { + "$ref": "#/definitions/Instances.InstanceServiceGroup" + } + ], + "description": "The service group configuration for the instance." + }, + "services": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Services exposed by this instance (format: \"protocol:port\")." + }, + "volumes": { + "type": "array", + "items": { + "$ref": "#/definitions/Instances.InstanceVolume" + }, + "description": "The list of volumes attached to the instance." + }, + "network_interfaces": { + "type": "array", + "items": { + "$ref": "#/definitions/Instances.InstanceNetworkInterface" + }, + "description": "The network interfaces of the instance.\nNot used for template instances." + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The tags associated with the instance." + }, + "annotations": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "The annotations associated with the instance.\n\nKeys follow the Kubernetes annotation key syntax, `[/]`: the\noptional prefix is a non-wildcard DNS subdomain of at most 253 characters,\nand the name is at most 63 characters of `[-_.a-zA-Z0-9]` starting and\nending with an alphanumeric. Values are unconstrained apart from ASCII\ncontrol characters, which are rejected because they would corrupt the\nconsole log output annotations can be forwarded to.\n\nAn instance holds at most 256 annotations." + }, + "snapshot": { + "allOf": [ + { + "$ref": "#/definitions/Instances.InstanceSnapshot" + } + ], + "description": "The snapshot of the instance, if exists." + }, + "delete_lock": { + "type": "boolean", + "description": "If set to true, the instance cannot be deleted until the lock is removed." + }, + "features": { + "type": "array", + "items": { + "$ref": "#/definitions/Instances.InstanceFeature" + } + }, + "restart": { + "allOf": [ + { + "$ref": "#/definitions/Instances.InstanceRestartAttempt" + } + ], + "description": "The current restart attempt for the instance.\nNot used for template instances." + }, + "roms": { + "type": "array", + "items": { + "$ref": "#/definitions/Instances.InstanceRom" + }, + "description": "Read-Only Memory (ROM) blobs to attach to the instance.\nUnikraft Cloud supports the ability to attach Read-Only Memory (ROM) blobs\nto instances. It allows you to create a general-purpose base image and\nthen customize individual instances by attaching code or data as separate\nROM blobs." + }, + "plugins": { + "type": "array", + "items": { + "$ref": "#/definitions/Instances.InstancePlugin" + }, + "description": "Plugins attached to the instance. Plugins let you attach small helper\nprograms to an instance and reach each one over a direct, authenticated\nHTTP endpoint. Each plugin loads from its own ROM image, mounts at\n`/uk/plugins/`, and is reachable at\n`.../v1/instances//plugins//`. At most 8\nplugins may be attached to an instance." + }, + "schedules": { + "type": "array", + "items": { + "$ref": "#/definitions/Instances.Schedule" + }, + "description": "Scheduled operations for this instance.\n\nEach schedule defines a calendar expression and an action (`start`,\n`stop`, `delete`, or `exec`) to perform at matching times. When the\naction is `exec`, the `args` field of the schedule specifies the command\nto run inside the instance." + }, + "autokill": { + "allOf": [ + { + "$ref": "#/definitions/Instances.InstanceAutokill" + } + ], + "description": "Automatic delete-on-idle/request-limit configuration.\nNot used for template instances." + }, + "updates": { + "type": "array", + "items": { + "$ref": "#/definitions/Instances.InstancePendingUpdate" + }, + "description": "Queued property changes awaiting application." + }, + "sched_priority": { + "allOf": [ + { + "$ref": "#/definitions/Common.SchedPriority" + } + ], + "description": "The scheduling priority for the instance. Only present for\nusers with scheduling priority override permissions." + }, + "template_autokill": { + "allOf": [ + { + "$ref": "#/definitions/Instances.InstanceTemplateAutokill" + } + ], + "description": "Template-specific automatic delete-on-idle configuration.\nNot used for non-template instances." + }, + "checkpoint_autokill": { + "allOf": [ + { + "$ref": "#/definitions/Instances.InstanceTemplateAutokill" + } + ], + "description": "Checkpoint-specific automatic delete-on-idle configuration.\nOnly used for checkpoint instances." + }, + "private_ip": { + "type": "string", + "description": "The private IP address of the instance." + }, + "gateway": { + "type": "string", + "description": "The default gateway configured inside the guest." + }, + "nameserver": { + "type": "string", + "description": "The DNS resolver configured inside the guest." + }, + "type": { + "allOf": [ + { + "$ref": "#/definitions/Instances.InstanceType" + } + ], + "description": "The type of virtual machine used to run the instance." + }, + "gpus": { + "type": "array", + "items": { + "$ref": "#/definitions/Instances.InstanceGpu" + }, + "description": "GPUs attached to the instance. Only present for instances of type\n`full`." + } + }, + "description": "Instance with per-item response envelope fields merged in." + }, + "Common.ResponseStatus": { + "type": "string", + "enum": [ + "success", + "error", + "partial_success" + ], + "description": "The response status of an API request." + }, + "Instances.InstanceState": { + "type": "string", + "enum": [ + "stopped", + "starting", + "running", + "draining", + "stopping", + "template", + "standby", + "deleted", + "checkpoint" + ], + "description": "The current state of an instance." + }, + "Instances.InstanceScaleToZero": { + "type": "object", + "properties": { + "enabled": { + "type": "boolean", + "description": "Indicates whether scale-to-zero is enabled for the instance." + }, + "policy": { + "allOf": [ + { + "$ref": "#/definitions/Instances.InstanceScaleToZeroPolicy" + } + ], + "description": "The specific policy to use for scaling the instance to zero." + }, + "stateful": { + "type": "boolean", + "description": "Whether the instance should be stateful when scaled to zero. If set to\ntrue, the instance will retain its state (e.g., RAM contents) when scaled\nto zero. This is useful for instances that need to maintain their state\nacross scale-to-zero operations. If set to false, the instance will lose\nits state when scaled to zero, and it will be restarted from scratch when\nscaled back up." + }, + "cooldown_time_ms": { + "type": "integer", + "description": "The cooldown time in milliseconds before the instance can be scaled to\nzero again. This is useful to prevent rapid scaling to zero and back up,\nwhich can lead to performance issues or resource exhaustion." + }, + "notify_time_ms": { + "type": "integer", + "description": "The notification time in milliseconds before the instance is scaled to\nzero. This allows the instance to perform any necessary cleanup or state\nsaving before being scaled down." + } + }, + "description": "Scale-to-zero defines the configuration for scaling the instance to zero.\nWhen an instance is scaled-to-zero it can be either stopped (and fully\nshutdown) or paused wherein the state of the instance is preserved (e.g.,\nRAM contents) and the instance can be resumed later without losing its\nstate, i.e. \"stateful\"." + }, + "Instances.InstanceServiceGroup": { + "type": "object", + "required": [ + "uuid", + "name" + ], + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "The UUID of the resource." + }, + "name": { + "type": "string", + "description": "The human-readable name of the resource." + }, + "domains": { + "type": "array", + "items": { + "$ref": "#/definitions/Instances.ServiceGroupInstanceDomain" + }, + "description": "The domain configuration for the service group." + } + }, + "description": "The service group configuration for the instance. This is a reference to the\nservice group that the instance is part of. The service group defines the\nservices (e.g. ports, connection handling) that the instance exposes and how\nthey are configured." + }, + "Instances.ServiceGroupInstanceDomain": { + "type": "object", + "required": [ + "fqdn" + ], + "properties": { + "fqdn": { + "type": "string", + "description": "Publicly accessible domain name.\n\nIf this name ends in a period `.` it must be a valid Full Qualified\nDomain Name (FQDN), otherwise it will become a subdomain of the target\nmetro." + }, + "certificate": { + "allOf": [ + { + "$ref": "#/definitions/Common.NameOrUUID" + } + ], + "description": "The certificate associated with the domain.\n\nThe certificate is used to secure the domain with TLS/SSL. If no\ncertificate is specified, Unikraft Cloud will automatically generate a\nnew certificate for the domain based on Let's Encrypt and seek to\naccomplish a DNS-01 challenge." + } + }, + "description": "The domain configuration for the service group. Domain names are completely\nspecified with all labels in the hierarchy of the DNS, having no parts\nomitted. The domain can be associated with an existing certificate by\nspecifying the certificate's name or UUID. If no certificate is specified\nand a FQDN is provided, Unikraft Cloud will automatically generate a new\ncertificate for the domain based on Let's Encrypt and seek to accomplish a\nDNS-01 challenge." + }, + "Instances.InstanceVolume": { + "type": "object", + "required": [ + "uuid", + "name", + "at", + "readonly" + ], + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "The UUID of the resource." + }, + "name": { + "type": "string", + "description": "The human-readable name of the resource." + }, + "at": { + "type": "string", + "description": "The mount point of the volume in the instance. This is the directory in\nthe instance where the volume will be mounted." + }, + "readonly": { + "type": "boolean", + "description": "Whether the volume is read-only or not." + } + }, + "description": "A volume defines a storage which can be attached to the instance. Volumes\ncan be used to store persistent data which should remain available even if\nthe instance is stopped or restarted." + }, + "Instances.InstanceNetworkInterface": { + "type": "object", + "required": [ + "uuid", + "private_ip", + "mac" + ], + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + }, + "private_ip": { + "type": "string", + "description": "The private IP address of the network interface. This is the internal IP\naddress that is used for communication between instances within the same\nnetwork." + }, + "mac": { + "type": "string", + "description": "The MAC address of the network interface." + }, + "name": { + "type": "string", + "description": "The interface name. If omitted, Unikraft Cloud generates one as\n-ethX, falling back to eth- when the\ninstance name is too long." + }, + "tap_name": { + "type": "string", + "description": "The TAP device to attach the interface. Provide it together with ip." + }, + "autoconfig": { + "type": "boolean", + "description": "Whether the interface is automatically configured inside the guest\n(IP address, routes, etc.). When absent or true, autoconfiguration\nis enabled. Present and false when the guest is expected to\nconfigure the interface manually." + }, + "relay": { + "allOf": [ + { + "$ref": "#/definitions/Instances.InstanceNetworkInterfaceRelay" + } + ], + "description": "Relay configuration for this interface." + } + }, + "description": "A network interface attached to an instance." + }, + "Instances.InstanceNetworkInterfaceRelay": { + "type": "object", + "required": [ + "uuid", + "name", + "relay_dns" + ], + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "UUID of the relay interface." + }, + "name": { + "type": "string", + "description": "Name of the relay interface." + }, + "relay_dns": { + "type": "boolean", + "description": "Whether DNS traffic is relayed through this interface.\nDefaults to true." + } + }, + "description": "An object that routes all traffic through another interface." + }, + "Instances.InstanceSnapshot": { + "type": "object", + "required": [ + "uuid" + ], + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + } + }, + "description": "A snapshot reference for an instance." + }, + "Instances.InstanceRestartAttempt": { + "type": "object", + "required": [ + "attempt" + ], + "properties": { + "attempt": { + "type": "integer", + "description": "Current restart attempt number. This is incremented each time the instance\nis restarted automatically by the platform.", + "minimum": 0 + }, + "next_at": { + "type": "string", + "format": "date-time", + "description": "Timestamp of the next scheduled restart attempt." + } + }, + "description": "Restart attempt information for an instance." + }, + "Instances.InstanceRom": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "description": "The name of the ROM to use for the instance configuration." + }, + "image": { + "type": "string", + "description": "The image of the ROM to use for the instance configuration. Mutually\nexclusive with `files`." + }, + "at": { + "type": "string", + "description": "The path at which the ROM should be automatically mounted inside the\ninstance. When set, the platform mounts the ROM device at the specified\npath so the guest does not need to mount it manually. When omitted, the\nROM is exposed as a raw block device and the guest is responsible for\nmounting it." + }, + "files": { + "type": "array", + "items": { + "$ref": "#/definitions/Common.InlineFile" + }, + "description": "Inline files to use as the ROM content. When specified, the platform\ncreates an EROFS image from the provided files. Mutually exclusive with\n`image`." + } + }, + "description": "A ROM attached to an instance." + }, + "Instances.InstancePlugin": { + "type": "object", + "required": [ + "name", + "image" + ], + "properties": { + "name": { + "type": "string", + "description": "The plugin name. It becomes the `` segment in the plugin\nendpoint (`.../plugins//`). A plugin name has a\nmaximum length of 63 characters and contains only letters (`a`-`z`,\n`A`-`Z`), digits (`0`-`9`), hyphen (`-`), and underscore (`_`)." + }, + "image": { + "type": "string", + "description": "The plugin's image, given as an image reference string such as\n`user/myplugin:latest`. The platform loads the image, mounts it at\n`/uk/plugins/`, and runs its `init` program when the\nplugin starts." + }, + "config": { + "description": "Arbitrary JSON configuration that the platform passes to the plugin's\n`init` program on `STDIN`. Any JSON value works, including a string, a\nnumber, or an object." + } + }, + "description": "A helper program attached to the instance and reachable over a direct,\nauthenticated HTTP endpoint. A plugin runs inside the instance next to\nthe main application, loads from its own ROM image, and answers requests\nthat the Unikraft Cloud API forwards to it." + }, + "Instances.Schedule": { + "type": "object", + "required": [ + "name", + "when", + "action" + ], + "properties": { + "name": { + "type": "string", + "description": "The name of the schedule.\n\nMust be unique within an instance." + }, + "when": { + "type": "string", + "description": "The calendar expression specifying when the action should be performed.\n\nUses systemd calendar event syntax.\nSee https://www.man7.org/linux/man-pages/man7/systemd.time.7.html" + }, + "action": { + "allOf": [ + { + "$ref": "#/definitions/Instances.ScheduleAction" + } + ], + "description": "The action to perform at the scheduled time." + }, + "next_at": { + "type": "string", + "format": "date-time", + "description": "The timestamp of when the next scheduled action will occur.\n\nThis field is populated only in responses (not settable in requests).\nOmitted if no next execution is scheduled.", + "readOnly": true + }, + "args": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The command to execute when the action is `exec`.\n\nRequired when `action` is `SCHEDULE_ACTION_EXEC`, ignored otherwise.\nEach element is a separate argument; the first element is the executable." + } + }, + "description": "A schedule defines when an action should be performed on an instance.\n\nEach schedule specifies a name, a calendar expression following systemd\ncalendar event syntax, and an action (start, stop, delete, or exec).\n\nCalendar expressions format:\n\n[weekday] [[year-]month-day] [hour:minute[:second]]\n\nSupported syntax:\n\n- `*` - Any value\n- `5` - Exact value\n- `1..5` - Range\n- `1..5/2` - Range with step\n- `1,2,5` - Comma-separated list\n\nExample: `*-*-* 09:00:00` - Every day at 09:00 UTC\nExample: `Sat,Sun *-*-* 20:00:00` - Every Saturday and Sunday at 20:00 UTC" + }, + "Instances.ScheduleAction": { + "type": "string", + "enum": [ + "start", + "stop", + "delete", + "exec" + ], + "description": "The action to perform on a scheduled operation." + }, + "Instances.InstanceAutokill": { + "type": "object", + "properties": { + "time_ms": { + "type": "integer", + "description": "Time in milliseconds after the instance was stopped before it is deleted.\nA value of 0 disables time-based autokill.", + "minimum": 0 + }, + "num_requests": { + "type": "integer", + "description": "Maximum number of requests/connections the instance serves before it is\ndeleted. A value of 0 disables request-based autokill.", + "minimum": 0 + } + }, + "description": "Automatic delete-on-idle/request-limit configuration for non-template\ninstances. Not used for template instances." + }, + "Instances.InstancePendingUpdate": { + "type": "object", + "required": [ + "prop", + "op", + "value", + "status" + ], + "properties": { + "prop": { + "allOf": [ + { + "$ref": "#/definitions/Instances.MutableInstanceProperty" + } + ], + "description": "The property being updated." + }, + "op": { + "allOf": [ + { + "$ref": "#/definitions/Instances.MutableInstanceOperation" + } + ], + "description": "The patch operation type." + }, + "value": { + "description": "The new value for the property. Type depends on the property being updated." + }, + "status": { + "type": "string", + "enum": [ + "pending", + "failed" + ], + "description": "The status of this update." + }, + "error": { + "type": "string", + "description": "Error message. Only present when status is \"failed\"." + } + }, + "description": "A queued property change awaiting application on an instance." + }, + "Instances.MutableInstanceProperty": { + "type": "string", + "enum": [ + "image", + "args", + "env", + "memory_mb", + "vcpus", + "scale_to_zero", + "tags", + "delete_lock", + "schedules", + "autokill", + "hostname", + "roms", + "dependencies", + "sched_priority", + "plugins", + "annotations" + ], + "description": "The mutable properties of an instance that can be updated." + }, + "Instances.MutableInstanceOperation": { + "type": "string", + "enum": [ + "set", + "add", + "del" + ], + "description": "Mutable instance operations." + }, + "Common.SchedPriority": { + "type": "string", + "enum": [ + "normal", + "medium", + "high", + "admin" + ], + "description": "SchedPriority defines the scheduling priority for an instance.\nUser requires the `override_vm_priority` permission to change it.\n\nThe list of available scheduling priorities:\n\n| Priority | Description |\n|----------|-------------|\n| `normal` | Default scheduling priority. |\n| `medium` | Medium scheduling priority. |\n| `high` | High scheduling priority. |\n| `admin` | Admin scheduling priority. |" + }, + "Instances.InstanceTemplateAutokill": { + "type": "object", + "properties": { + "time_ms": { + "type": "integer", + "description": "Time in milliseconds after the template was last used for cloning before\nit is deleted. A value of 0 disables template autokill.", + "minimum": 0 + } + }, + "description": "Autokill configuration for a template instance." + }, + "Instances.InstanceType": { + "type": "string", + "enum": [ + "micro", + "full" + ], + "description": "The type of virtual machine used to run an instance.\n\n| Type | Description |\n|---------|-------------|\n| `micro` | A lightweight microVM (default). Boots in milliseconds and is suitable for most workloads. |\n| `full` | A full virtual machine with broader hardware support, such as GPU passthrough. Requires a plan with full VM support. |" + }, + "Instances.InstanceGpu": { + "type": "object", + "required": [ + "uuid", + "model" + ], + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "The UUID of the GPU." + }, + "model": { + "type": "string", + "description": "The GPU model, given as its PCI vendor and device ID in the form\n`:`." + } + }, + "description": "A GPU attached to the instance." + }, + "Instances.TemplateAutokill": { + "type": "object", + "properties": { + "time_ms": { + "type": "integer", + "description": "Time in milliseconds after the template was last used for cloning before\nit is deleted. A value of 0 disables template autokill.", + "minimum": 0 + } + }, + "description": "Automatic delete-on-idle configuration for the template instance. Only\napplies when `prepare` is set." + }, + "Instances.CreateInstanceRequestAutokill": { + "type": "object", + "properties": { + "time_ms": { + "type": "integer", + "description": "Time in milliseconds after the instance was stopped before it is deleted.\nA value of 0 disables time-based autokill.", + "minimum": 0 + }, + "num_requests": { + "type": "integer", + "description": "Maximum number of requests/connections the instance serves before it is\ndeleted. A value of 0 disables request-based autokill.", + "minimum": 0 + } + }, + "description": "Automatic delete-on-idle/request-limit configuration for non-template instances." + }, + "Instances.CreateInstanceRequestNetworkInterface": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The interface name. If omitted, Unikraft Cloud generates one as\n-ethX, falling back to eth- when the\ninstance name is too long." + }, + "tap_name": { + "type": "string", + "description": "The TAP device to attach the interface to. Provide it together\nwith ip." + }, + "ip": { + "type": "string", + "description": "The interface IP address in CIDR notation. Provide it together\nwith tap_name to bring your own interface." + }, + "mac": { + "type": "string", + "description": "The interface MAC address. Provide it together with tap_name. Must be\na unicast address outside the platform's address pool." + }, + "autoconfig": { + "type": "boolean", + "description": "Whether the guest configures the interface itself. Defaults to true." + }, + "relay": { + "allOf": [ + { + "$ref": "#/definitions/Instances.NetworkInterfaceRelay" + } + ], + "description": "Relay configuration for this interface." + } + }, + "description": "Network interface configuration for a new instance." + }, + "Instances.NetworkInterfaceRelay": { + "type": "object", + "properties": { + "relay_dns": { + "type": "boolean", + "description": "Whether the relay forwards DNS requests. Set to false\nto let the default DNS server handle them instead.\nDefaults to true." + }, + "uuid": { + "type": "string", + "format": "uuid", + "description": "The UUID of the resource." + }, + "name": { + "type": "string", + "description": "The name of the resource." + } + }, + "description": "An object that routes all traffic through another interface.", + "oneOf": [ + { + "required": [ + "uuid" + ] + }, + { + "required": [ + "name" + ] + } + ] + } + } + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_get_instances", + "kind": "platform", + "path": "/v1/instances", + "method": "GET", + "parameters": [ + { + "name": "uuid", + "group": "query", + "style": "form", + "explode": true + }, + { + "name": "name", + "group": "query", + "style": "form", + "explode": true + }, + { + "name": "details", + "group": "query", + "style": "form", + "explode": false + }, + { + "name": "count", + "group": "query", + "style": "form", + "explode": false + }, + { + "name": "from", + "group": "query", + "style": "form", + "explode": false + }, + { + "name": "order", + "group": "query", + "style": "form", + "explode": false + }, + { + "name": "sortby", + "group": "query", + "style": "form", + "explode": false + }, + { + "name": "tags", + "group": "query", + "style": "form", + "explode": false + } + ], + "bodyContentType": "application/json", + "title": "List Instances", + "description": "Get one or many instances with their current status and configuration.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "query": { + "type": "object", + "properties": { + "uuid": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The UUID of the resource." + }, + "name": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The name of the resource." + }, + "details": { + "type": "boolean" + }, + "count": { + "type": "integer", + "minimum": 0 + }, + "from": { + "type": "string" + }, + "order": { + "$ref": "#/definitions/Common.PaginationOrder" + }, + "sortby": { + "$ref": "#/definitions/Common.PaginationSortBy" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "type": "array", + "items": { + "$ref": "#/definitions/Common.NameOrUUID" + } + } + }, + "required": [], + "definitions": { + "Common.PaginationOrder": { + "type": "string", + "enum": [ + "asc", + "desc" + ], + "description": "The sort order used by list endpoints." + }, + "Common.PaginationSortBy": { + "type": "string", + "enum": [ + "create_time" + ], + "description": "The sort field used by list endpoints." + }, + "Common.NameOrUUID": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "The UUID of the resource." + }, + "name": { + "type": "string", + "description": "The name of the resource." + } + }, + "description": "An identifier for a resource — either a name or a UUID, but not both.", + "oneOf": [ + { + "required": [ + "uuid" + ], + "not": { + "required": [ + "name" + ] + } + }, + { + "required": [ + "name" + ], + "not": { + "required": [ + "uuid" + ] + } + } + ] + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + { + "name": "unikraft_delete_instances", + "kind": "platform", + "path": "/v1/instances", + "method": "DELETE", + "parameters": [], + "bodyContentType": "application/json", + "title": "Delete Instances", + "description": "Delete instances by ID(s).", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "type": "array", + "items": { + "$ref": "#/definitions/Instances.DeleteInstanceRequestItem" + } + } + }, + "required": [ + "body" + ], + "definitions": { + "Instances.DeleteInstanceRequestItem": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "The UUID of the resource." + }, + "name": { + "type": "string", + "description": "The name of the resource." + }, + "timeout_s": { + "type": "integer", + "description": "Timeout in seconds to wait for the instance to be deleted. No wait\nperformed for a value of 0." + }, + "dont_retain": { + "type": "boolean", + "description": "Delete immediately without retention. If the instance is already\nbeing retained, this will force its deletion. Ignored if retention\nfor instances is not configured." + } + }, + "description": "The request item for deleting an instance by its UUID or name." + } + } + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_update_instances", + "kind": "platform", + "path": "/v1/instances", + "method": "PATCH", + "parameters": [], + "bodyContentType": "application/json", + "title": "Update Instances", + "description": "Update instances by ID(s).", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "type": "array", + "items": { + "$ref": "#/definitions/Instances.UpdateInstancesRequestItem" + } + } + }, + "required": [ + "body" + ], + "definitions": { + "Instances.UpdateInstancesRequestItem": { + "type": "object", + "required": [ + "prop", + "op" + ], + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "The UUID of the resource." + }, + "name": { + "type": "string", + "description": "The name of the resource." + }, + "id": { + "type": "string", + "description": "A client-provided identifier for tracking this operation in the response." + }, + "prop": { + "allOf": [ + { + "$ref": "#/definitions/Instances.MutableInstanceProperty" + } + ], + "description": "The property to modify." + }, + "op": { + "allOf": [ + { + "$ref": "#/definitions/Instances.MutableInstanceOperation" + } + ], + "description": "The operation to perform on the property." + }, + "value": { + "description": "The value for the update operation. The type depends on the property and operation:\n- For \"image\": image reference string, or object with image url,\ncredentials, headers and pull policy\n- For \"args\": string or array of strings\n- For \"env\": object (for SET/ADD) or string/array of strings (for DEL)\n- For \"memory_mb\": integer\n- For \"vcpus\": integer\n- For \"scale_to_zero\": object with cooldown_time_ms, policy, and stateful fields\n- For \"tags\": array of strings\n- For \"delete_lock\": boolean\n- For \"schedules\": array of schedule objects (with name, when, action, and optional args fields)\nfor SET/ADD, or array of schedule names for DEL. Use action \"exec\" together with args to\nexecute a command at the scheduled time.\n- For \"autokill\": object with time_ms and num_requests fields\n- For \"hostname\": string (valid DNS label)\n- For \"roms\": array of ROM objects (with name and image fields) for SET/ADD, or array of ROM names for DEL\n- For \"plugins\": array of plugin objects (with name, image, and optional config fields)\nfor SET/ADD. The deprecated `rom` field is accepted in place of `image`.\n- For \"dependencies\": array of instance identifiers (name or UUID)\n- For \"sched_priority\": SchedPriority enum value (\"normal\", \"medium\", \"high\", \"admin\")\n- For \"annotations\": object (for SET/ADD) or string/array of strings (for DEL)" + } + }, + "description": "A single update operation to be applied to an instance." + }, + "Instances.MutableInstanceProperty": { + "type": "string", + "enum": [ + "image", + "args", + "env", + "memory_mb", + "vcpus", + "scale_to_zero", + "tags", + "delete_lock", + "schedules", + "autokill", + "hostname", + "roms", + "dependencies", + "sched_priority", + "plugins", + "annotations" + ], + "description": "The mutable properties of an instance that can be updated." + }, + "Instances.MutableInstanceOperation": { + "type": "string", + "enum": [ + "set", + "add", + "del" + ], + "description": "Mutable instance operations." + } + } + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_get_checkpoint_instances", + "kind": "platform", + "path": "/v1/instances/checkpoints", + "method": "GET", + "parameters": [ + { + "name": "uuid", + "group": "query", + "style": "form", + "explode": true + }, + { + "name": "name", + "group": "query", + "style": "form", + "explode": true + }, + { + "name": "details", + "group": "query", + "style": "form", + "explode": false + }, + { + "name": "count", + "group": "query", + "style": "form", + "explode": false + }, + { + "name": "from", + "group": "query", + "style": "form", + "explode": false + }, + { + "name": "order", + "group": "query", + "style": "form", + "explode": false + }, + { + "name": "sortby", + "group": "query", + "style": "form", + "explode": false + }, + { + "name": "tags", + "group": "query", + "style": "form", + "explode": false + } + ], + "bodyContentType": "application/json", + "title": "List Checkpoint Instances", + "description": "Get one or more checkpoint instances by their UUID(s) or name(s).", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "query": { + "type": "object", + "properties": { + "uuid": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The UUID of the resource." + }, + "name": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The name of the resource." + }, + "details": { + "type": "boolean" + }, + "count": { + "type": "integer", + "minimum": 0 + }, + "from": { + "type": "string" + }, + "order": { + "$ref": "#/definitions/Common.PaginationOrder" + }, + "sortby": { + "$ref": "#/definitions/Common.PaginationSortBy" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "type": "array", + "items": { + "$ref": "#/definitions/Common.NameOrUUID" + } + } + }, + "required": [ + "body" + ], + "definitions": { + "Common.PaginationOrder": { + "type": "string", + "enum": [ + "asc", + "desc" + ], + "description": "The sort order used by list endpoints." + }, + "Common.PaginationSortBy": { + "type": "string", + "enum": [ + "create_time" + ], + "description": "The sort field used by list endpoints." + }, + "Common.NameOrUUID": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "The UUID of the resource." + }, + "name": { + "type": "string", + "description": "The name of the resource." + } + }, + "description": "An identifier for a resource — either a name or a UUID, but not both.", + "oneOf": [ + { + "required": [ + "uuid" + ], + "not": { + "required": [ + "name" + ] + } + }, + { + "required": [ + "name" + ], + "not": { + "required": [ + "uuid" + ] + } + } + ] + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + { + "name": "unikraft_create_checkpoint_instances", + "kind": "platform", + "path": "/v1/instances/checkpoints", + "method": "POST", + "parameters": [], + "bodyContentType": "application/json", + "title": "Create Checkpoint Instances", + "description": "Create a checkpoint from an existing instance. A checkpoint captures the\nstate of an instance at a specific point in time. Checkpoints can be\ncreated from running, stopped, or standby instances.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "type": "array", + "items": { + "$ref": "#/definitions/Instances.CreateCheckpointInstancesRequestItem" + } + } + }, + "required": [ + "body" + ], + "definitions": { + "Instances.CreateCheckpointInstancesRequestItem": { + "type": "object", + "required": [ + "from" + ], + "properties": { + "from": { + "allOf": [ + { + "$ref": "#/definitions/Common.NameOrUUID" + } + ], + "description": "The source instance to create a checkpoint from (by name or UUID)." + }, + "name": { + "type": "string", + "description": "The name of the checkpoint. If not provided, a name will be generated." + }, + "timeout_s": { + "type": "integer", + "description": "Timeout in seconds to wait for the checkpoint to be created.\nNo wait performed for a value of 0." + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Tags to associate with the checkpoint." + }, + "autokill": { + "allOf": [ + { + "$ref": "#/definitions/Instances.CheckpointAutokill" + } + ], + "description": "Automatic delete-on-idle configuration for the new checkpoint." + } + }, + "description": "A single checkpoint creation request." + }, + "Common.NameOrUUID": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "The UUID of the resource." + }, + "name": { + "type": "string", + "description": "The name of the resource." + } + }, + "description": "An identifier for a resource — either a name or a UUID, but not both.", + "oneOf": [ + { + "required": [ + "uuid" + ], + "not": { + "required": [ + "name" + ] + } + }, + { + "required": [ + "name" + ], + "not": { + "required": [ + "uuid" + ] + } + } + ] + }, + "Instances.CheckpointAutokill": { + "type": "object", + "properties": { + "time_ms": { + "type": "integer", + "description": "Time in milliseconds after the checkpoint was last used for restoring\nbefore it is deleted. A value of 0 disables checkpoint autokill.", + "minimum": 0 + } + }, + "description": "Automatic delete-on-idle configuration for the checkpoint instance." + } + } + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_delete_checkpoint_instances", + "kind": "platform", + "path": "/v1/instances/checkpoints", + "method": "DELETE", + "parameters": [], + "bodyContentType": "application/json", + "title": "Delete Checkpoint Instances", + "description": "Delete the specified checkpoint instance(s) by ID(s) (name or UUID).\nAfter this call the IDs of the checkpoint instances are no longer valid.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "type": "array", + "items": { + "$ref": "#/definitions/Common.NameOrUUID" + } + } + }, + "required": [ + "body" + ], + "definitions": { + "Common.NameOrUUID": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "The UUID of the resource." + }, + "name": { + "type": "string", + "description": "The name of the resource." + } + }, + "description": "An identifier for a resource — either a name or a UUID, but not both.", + "oneOf": [ + { + "required": [ + "uuid" + ], + "not": { + "required": [ + "name" + ] + } + }, + { + "required": [ + "name" + ], + "not": { + "required": [ + "uuid" + ] + } + } + ] + } + } + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_update_checkpoint_instances", + "kind": "platform", + "path": "/v1/instances/checkpoints", + "method": "PATCH", + "parameters": [], + "bodyContentType": "application/json", + "title": "Update Checkpoint Instances", + "description": "Update (modify) one or more checkpoint instances by ID(s) (name or UUID).", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "type": "array", + "items": { + "$ref": "#/definitions/Instances.UpdateCheckpointInstancesRequestItem" + } + } + }, + "required": [ + "body" + ], + "definitions": { + "Instances.UpdateCheckpointInstancesRequestItem": { + "type": "object", + "required": [ + "prop", + "op" + ], + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "The UUID of the resource." + }, + "name": { + "type": "string", + "description": "The name of the resource." + }, + "id": { + "type": "string", + "description": "A client-provided identifier for tracking this operation in the response." + }, + "prop": { + "allOf": [ + { + "$ref": "#/definitions/Instances.MutableCheckpointInstanceProperty" + } + ], + "description": "The property to modify." + }, + "op": { + "allOf": [ + { + "$ref": "#/definitions/Instances.MutableCheckpointInstanceOperation" + } + ], + "description": "The operation to perform on the property." + }, + "value": { + "description": "The value for the update operation. The type depends on the property and operation:\n- For \"tags\": array of strings\n- For \"delete_lock\": boolean\n- For \"autokill\": object with time_ms field" + } + }, + "description": "A single update operation to be applied to a checkpoint instance." + }, + "Instances.MutableCheckpointInstanceProperty": { + "type": "string", + "enum": [ + "tags", + "delete_lock", + "autokill" + ], + "description": "The mutable properties of a checkpoint instance that can be updated." + }, + "Instances.MutableCheckpointInstanceOperation": { + "type": "string", + "enum": [ + "set", + "add", + "del" + ], + "description": "The operations available on a checkpoint instance's properties." + } + } + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_get_checkpoint_history", + "kind": "platform", + "path": "/v1/instances/checkpoints/history", + "method": "GET", + "parameters": [ + { + "name": "uuid", + "group": "query", + "style": "form", + "explode": true + }, + { + "name": "name", + "group": "query", + "style": "form", + "explode": true + } + ], + "bodyContentType": "application/json", + "title": "Get Checkpoint History", + "description": "Get the checkpoint history of one or more checkpoint instances.\nReturns the ordered list of checkpoints in each checkpoint's history.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "query": { + "type": "object", + "properties": { + "uuid": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The UUID of the resource." + }, + "name": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The name of the resource." + } + }, + "required": [], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "type": "array", + "items": { + "$ref": "#/definitions/Common.NameOrUUID" + } + } + }, + "required": [ + "body" + ], + "definitions": { + "Common.NameOrUUID": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "The UUID of the resource." + }, + "name": { + "type": "string", + "description": "The name of the resource." + } + }, + "description": "An identifier for a resource — either a name or a UUID, but not both.", + "oneOf": [ + { + "required": [ + "uuid" + ], + "not": { + "required": [ + "name" + ] + } + }, + { + "required": [ + "name" + ], + "not": { + "required": [ + "uuid" + ] + } + } + ] + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + { + "name": "unikraft_get_checkpoint_instance_by_uuid", + "kind": "platform", + "path": "/v1/instances/checkpoints/{uuid}", + "method": "GET", + "parameters": [ + { + "name": "uuid", + "group": "path", + "style": "form", + "explode": true + }, + { + "name": "details", + "group": "query", + "style": "form", + "explode": false + } + ], + "title": "Get Checkpoint Instance by UUID", + "description": "Get a single checkpoint instance by its UUID.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "uuid" + ], + "additionalProperties": false + }, + "query": { + "type": "object", + "properties": { + "details": { + "type": "boolean" + } + }, + "required": [], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + } + }, + "required": [ + "path" + ] + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + { + "name": "unikraft_delete_checkpoint_instance_by_uuid", + "kind": "platform", + "path": "/v1/instances/checkpoints/{uuid}", + "method": "DELETE", + "parameters": [ + { + "name": "uuid", + "group": "path", + "style": "form", + "explode": true + } + ], + "title": "Delete Checkpoint Instance by UUID", + "description": "Delete a specified checkpoint instance by its UUID. After this call the\nUUID of the checkpoint instance is no longer valid.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "uuid" + ], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + } + }, + "required": [ + "path" + ] + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_update_checkpoint_instance_by_uuid", + "kind": "platform", + "path": "/v1/instances/checkpoints/{uuid}", + "method": "PATCH", + "parameters": [ + { + "name": "uuid", + "group": "path", + "style": "form", + "explode": true + } + ], + "bodyContentType": "application/json", + "title": "Update Checkpoint Instance by UUID", + "description": "Update (modify) a checkpoint instance by its UUID.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "uuid" + ], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "$ref": "#/definitions/Instances.UpdateCheckpointInstanceByUUIDRequestBody" + } + }, + "required": [ + "path", + "body" + ], + "definitions": { + "Instances.UpdateCheckpointInstanceByUUIDRequestBody": { + "type": "object", + "required": [ + "prop", + "op" + ], + "properties": { + "id": { + "type": "string", + "description": "A client-provided identifier for tracking this operation in the response." + }, + "prop": { + "allOf": [ + { + "$ref": "#/definitions/Instances.MutableCheckpointInstanceProperty" + } + ], + "description": "The property to modify." + }, + "op": { + "allOf": [ + { + "$ref": "#/definitions/Instances.MutableCheckpointInstanceOperation" + } + ], + "description": "The operation to perform on the property." + }, + "value": { + "description": "The value for the update operation. The type depends on the property and operation:\n- For \"tags\": array of strings\n- For \"delete_lock\": boolean\n- For \"autokill\": object with time_ms field" + } + }, + "description": "Request body for updating a single checkpoint instance by UUID." + }, + "Instances.MutableCheckpointInstanceProperty": { + "type": "string", + "enum": [ + "tags", + "delete_lock", + "autokill" + ], + "description": "The mutable properties of a checkpoint instance that can be updated." + }, + "Instances.MutableCheckpointInstanceOperation": { + "type": "string", + "enum": [ + "set", + "add", + "del" + ], + "description": "The operations available on a checkpoint instance's properties." + } + } + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_get_checkpoint_history_by_uuid", + "kind": "platform", + "path": "/v1/instances/checkpoints/{uuid}/history", + "method": "GET", + "parameters": [ + { + "name": "uuid", + "group": "path", + "style": "form", + "explode": true + } + ], + "title": "Get Checkpoint History by UUID", + "description": "Get the checkpoint history of a checkpoint instance by its UUID.\nReturns the ordered list of checkpoints in the checkpoint's history.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "uuid" + ], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + } + }, + "required": [ + "path" + ] + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + { + "name": "unikraft_get_instance_history", + "kind": "platform", + "path": "/v1/instances/history", + "method": "GET", + "parameters": [ + { + "name": "uuid", + "group": "query", + "style": "form", + "explode": true + }, + { + "name": "name", + "group": "query", + "style": "form", + "explode": true + } + ], + "bodyContentType": "application/json", + "title": "Get Instances Checkpoint History", + "description": "Get the checkpoint history of one or more instances.\nReturns the ordered list of checkpoints associated with each instance.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "query": { + "type": "object", + "properties": { + "uuid": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The UUID of the resource." + }, + "name": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The name of the resource." + } + }, + "required": [], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "type": "array", + "items": { + "$ref": "#/definitions/Common.NameOrUUID" + } + } + }, + "required": [ + "body" + ], + "definitions": { + "Common.NameOrUUID": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "The UUID of the resource." + }, + "name": { + "type": "string", + "description": "The name of the resource." + } + }, + "description": "An identifier for a resource — either a name or a UUID, but not both.", + "oneOf": [ + { + "required": [ + "uuid" + ], + "not": { + "required": [ + "name" + ] + } + }, + { + "required": [ + "name" + ], + "not": { + "required": [ + "uuid" + ] + } + } + ] + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + { + "name": "unikraft_get_instance_logs", + "kind": "platform", + "path": "/v1/instances/log", + "method": "GET", + "parameters": [ + { + "name": "uuid", + "group": "query", + "style": "form", + "explode": true + }, + { + "name": "name", + "group": "query", + "style": "form", + "explode": true + }, + { + "name": "offset", + "group": "query", + "style": "form", + "explode": true + }, + { + "name": "limit", + "group": "query", + "style": "form", + "explode": true + } + ], + "bodyContentType": "application/json", + "title": "Get Instances Logs", + "description": "Get instances logs.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "query": { + "type": "object", + "properties": { + "uuid": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The UUID of the resource." + }, + "name": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The name of the resource." + }, + "offset": { + "type": "array", + "items": { + "type": "integer" + }, + "description": "The byte offset of the log output to receive. A negative sign makes the\noffset relative to the end of the log." + }, + "limit": { + "type": "array", + "items": { + "type": "integer" + }, + "description": "The amount of bytes to return at most." + } + }, + "required": [], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "type": "array", + "items": { + "$ref": "#/definitions/Instances.GetInstancesLogsRequestItem" + } + } + }, + "required": [ + "body" + ], + "definitions": { + "Instances.GetInstancesLogsRequestItem": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "The UUID of the resource." + }, + "name": { + "type": "string", + "description": "The name of the resource." + }, + "offset": { + "type": "integer", + "description": "The byte offset of the log output to receive. A negative sign makes the\noffset relative to the end of the log." + }, + "limit": { + "type": "integer", + "description": "The amount of bytes to return at most." + } + }, + "description": "A single request item to get an instance's logs." + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + { + "name": "unikraft_get_instance_metrics", + "kind": "platform", + "path": "/v1/instances/metrics", + "method": "GET", + "parameters": [ + { + "name": "uuid", + "group": "query", + "style": "form", + "explode": true + }, + { + "name": "name", + "group": "query", + "style": "form", + "explode": true + } + ], + "bodyContentType": "application/json", + "title": "Get Instances Metrics", + "description": "Get instances metrics.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "query": { + "type": "object", + "properties": { + "uuid": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The UUID of the resource." + }, + "name": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The name of the resource." + } + }, + "required": [], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "type": "array", + "items": { + "$ref": "#/definitions/Common.NameOrUUID" + } + } + }, + "required": [], + "definitions": { + "Common.NameOrUUID": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "The UUID of the resource." + }, + "name": { + "type": "string", + "description": "The name of the resource." + } + }, + "description": "An identifier for a resource — either a name or a UUID, but not both.", + "oneOf": [ + { + "required": [ + "uuid" + ], + "not": { + "required": [ + "name" + ] + } + }, + { + "required": [ + "name" + ], + "not": { + "required": [ + "uuid" + ] + } + } + ] + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + { + "name": "unikraft_start_instances", + "kind": "platform", + "path": "/v1/instances/start", + "method": "PUT", + "parameters": [], + "bodyContentType": "application/json", + "title": "Start Instances", + "description": "Start instances by ID(s).", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "type": "array", + "items": { + "$ref": "#/definitions/Instances.StartInstancesRequestItem" + } + } + }, + "required": [ + "body" + ], + "definitions": { + "Instances.StartInstancesRequestItem": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "The UUID of the resource." + }, + "name": { + "type": "string", + "description": "The name of the resource." + }, + "wait_timeout_ms": { + "type": "integer", + "description": "Deprecated: Use `timeout_s` instead. Timeout in milliseconds to\nwait for the instance to reach running state. If `timeout_s` is\nnot set, this value is converted by rounding up to the next full\nsecond. No wait performed for a value of 0.", + "deprecated": true + }, + "timeout_s": { + "type": "integer", + "description": "Timeout in seconds to wait for the instance to reach running\nstate. If you start your instance, you can wait for it to\nfinish starting with a blocking API call if you specify a wait\ntimeout greater than zero. No wait performed for a value of 0." + } + }, + "description": "A single request item to start an instance." + } + } + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_stop_instances", + "kind": "platform", + "path": "/v1/instances/stop", + "method": "PUT", + "parameters": [], + "bodyContentType": "application/json", + "title": "Stop Instances", + "description": "Stop instances by ID(s).", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "type": "array", + "items": { + "$ref": "#/definitions/Instances.StopInstancesRequestItem" + } + } + }, + "required": [ + "body" + ], + "definitions": { + "Instances.StopInstancesRequestItem": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "The UUID of the resource." + }, + "name": { + "type": "string", + "description": "The name of the resource." + }, + "force": { + "type": "boolean", + "description": "Whether to immediately force stop the instance." + }, + "drain_timeout_ms": { + "type": "integer", + "description": "Timeout for draining connections in milliseconds.\nNo draining will occur if set to 0. The instance\ndoes not receive new connections in the draining\nphase. The instance is stopped when the last\nconnection has been closed or the timeout expired.\nThe maximum timeout may vary. Use -1 for the\nlargest possible value. Ignored if force is set.\n\nNote: This endpoint does not block. Use the wait\nendpoint for the instance to reach the stopped\nstate.", + "minimum": -1 + }, + "quick": { + "type": "boolean", + "description": "Whether to perform a quick shutdown. This flag is\noverridden by force." + }, + "ifstate": { + "type": "string", + "description": "Only stop the instance if it is in this state." + }, + "dump": { + "type": "boolean", + "description": "If set, forces the VMM to shutdown immediately and generate a coredump.\nCan only be used in conjunction with force." + } + }, + "description": "A single request item to stop an instance." + } + } + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_suspend_instances", + "kind": "platform", + "path": "/v1/instances/suspend", + "method": "PUT", + "parameters": [], + "bodyContentType": "application/json", + "title": "Suspend Instances", + "description": "Suspend instances by ID(s).", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "type": "array", + "items": { + "$ref": "#/definitions/Instances.SuspendInstancesRequestItem" + } + } + }, + "required": [ + "body" + ], + "definitions": { + "Instances.SuspendInstancesRequestItem": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "The UUID of the resource." + }, + "name": { + "type": "string", + "description": "The name of the resource." + }, + "drain_timeout_ms": { + "type": "integer", + "description": "Timeout for draining connections in milliseconds. No draining\nwill occur if set to 0. Use -1 for the largest possible value.", + "minimum": -1 + } + }, + "description": "A single request item to suspend an instance." + } + } + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_create_template_instances", + "kind": "platform", + "path": "/v1/instances/templates", + "method": "POST", + "parameters": [], + "bodyContentType": "application/json", + "title": "Create template instances from Instances", + "description": "Convert instances to template instances.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "type": "array", + "items": { + "$ref": "#/definitions/Instances.CreateTemplateInstancesRequestItem" + } + } + }, + "required": [ + "body" + ], + "definitions": { + "Instances.CreateTemplateInstancesRequestItem": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "The UUID of the resource." + }, + "name": { + "type": "string", + "description": "The name of the resource." + }, + "timeout_s": { + "type": "integer", + "description": "Timeout in seconds to wait for the template instances to be created.\nA value of -1 means to wait indefinitely until the instance reaches the\ndesired state. No wait performed for a value of 0." + }, + "autokill": { + "allOf": [ + { + "$ref": "#/definitions/Instances.ItemAutokill" + } + ], + "description": "Automatic delete-on-idle configuration for the new template." + } + }, + "description": "A single template instance to be created." + }, + "Instances.ItemAutokill": { + "type": "object", + "properties": { + "time_ms": { + "type": "integer", + "description": "Time in milliseconds after the template was last used for cloning before\nit is deleted. A value of 0 disables template autokill.", + "minimum": 0 + } + }, + "description": "Automatic delete-on-idle configuration for the template instance." + } + } + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_get_template_instances", + "kind": "platform", + "path": "/v1/instances/templates", + "method": "GET", + "parameters": [ + { + "name": "uuid", + "group": "query", + "style": "form", + "explode": true + }, + { + "name": "name", + "group": "query", + "style": "form", + "explode": true + }, + { + "name": "details", + "group": "query", + "style": "form", + "explode": false + }, + { + "name": "count", + "group": "query", + "style": "form", + "explode": false + }, + { + "name": "from", + "group": "query", + "style": "form", + "explode": false + }, + { + "name": "order", + "group": "query", + "style": "form", + "explode": false + }, + { + "name": "sortby", + "group": "query", + "style": "form", + "explode": false + }, + { + "name": "tags", + "group": "query", + "style": "form", + "explode": false + } + ], + "bodyContentType": "application/json", + "title": "List template instances", + "description": "Get one or more template instances.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "query": { + "type": "object", + "properties": { + "uuid": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The UUID of the resource." + }, + "name": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The name of the resource." + }, + "details": { + "type": "boolean" + }, + "count": { + "type": "integer", + "minimum": 0 + }, + "from": { + "type": "string" + }, + "order": { + "$ref": "#/definitions/Common.PaginationOrder" + }, + "sortby": { + "$ref": "#/definitions/Common.PaginationSortBy" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "type": "array", + "items": { + "$ref": "#/definitions/Common.NameOrUUID" + } + } + }, + "required": [], + "definitions": { + "Common.PaginationOrder": { + "type": "string", + "enum": [ + "asc", + "desc" + ], + "description": "The sort order used by list endpoints." + }, + "Common.PaginationSortBy": { + "type": "string", + "enum": [ + "create_time" + ], + "description": "The sort field used by list endpoints." + }, + "Common.NameOrUUID": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "The UUID of the resource." + }, + "name": { + "type": "string", + "description": "The name of the resource." + } + }, + "description": "An identifier for a resource — either a name or a UUID, but not both.", + "oneOf": [ + { + "required": [ + "uuid" + ], + "not": { + "required": [ + "name" + ] + } + }, + { + "required": [ + "name" + ], + "not": { + "required": [ + "uuid" + ] + } + } + ] + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + { + "name": "unikraft_update_template_instances", + "kind": "platform", + "path": "/v1/instances/templates", + "method": "PATCH", + "parameters": [], + "bodyContentType": "application/json", + "title": "Update template instances", + "description": "Update template instances by ID(s).", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "type": "array", + "items": { + "$ref": "#/definitions/Instances.UpdateTemplateInstancesRequestItem" + } + } + }, + "required": [ + "body" + ], + "definitions": { + "Instances.UpdateTemplateInstancesRequestItem": { + "type": "object", + "required": [ + "prop", + "op" + ], + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "The UUID of the resource." + }, + "name": { + "type": "string", + "description": "The name of the resource." + }, + "id": { + "type": "string", + "description": "A client-provided identifier for tracking this operation in the response." + }, + "prop": { + "allOf": [ + { + "$ref": "#/definitions/Instances.MutableTemplateInstanceProperty" + } + ], + "description": "The property to modify." + }, + "op": { + "allOf": [ + { + "$ref": "#/definitions/Instances.MutableTemplateInstanceOperation" + } + ], + "description": "The operation to perform." + }, + "value": { + "description": "The value for the update operation. The type depends on the property and\noperation:\n- For \"tags\": array of strings\n- For \"delete_lock\": boolean\n- For \"autokill\": object with time_ms field" + } + }, + "description": "A single template instance update request item." + }, + "Instances.MutableTemplateInstanceProperty": { + "type": "string", + "enum": [ + "tags", + "delete_lock", + "autokill" + ], + "description": "The mutable properties of a template instance that can be updated." + }, + "Instances.MutableTemplateInstanceOperation": { + "type": "string", + "enum": [ + "set", + "add", + "del" + ], + "description": "Mutable template instance operations." + } + } + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_delete_template_instances", + "kind": "platform", + "path": "/v1/instances/templates", + "method": "DELETE", + "parameters": [], + "bodyContentType": "application/json", + "title": "Delete template instances", + "description": "Delete template instances by ID(s).", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "type": "array", + "items": { + "$ref": "#/definitions/Common.NameOrUUID" + } + } + }, + "required": [ + "body" + ], + "definitions": { + "Common.NameOrUUID": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "The UUID of the resource." + }, + "name": { + "type": "string", + "description": "The name of the resource." + } + }, + "description": "An identifier for a resource — either a name or a UUID, but not both.", + "oneOf": [ + { + "required": [ + "uuid" + ], + "not": { + "required": [ + "name" + ] + } + }, + { + "required": [ + "name" + ], + "not": { + "required": [ + "uuid" + ] + } + } + ] + } + } + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_get_template_instance_by_uuid", + "kind": "platform", + "path": "/v1/instances/templates/{uuid}", + "method": "GET", + "parameters": [ + { + "name": "uuid", + "group": "path", + "style": "form", + "explode": true + }, + { + "name": "details", + "group": "query", + "style": "form", + "explode": false + } + ], + "title": "Get template instance by UUID", + "description": "Get a single template instance by UUID.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "uuid" + ], + "additionalProperties": false + }, + "query": { + "type": "object", + "properties": { + "details": { + "type": "boolean" + } + }, + "required": [], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + } + }, + "required": [ + "path" + ] + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + { + "name": "unikraft_update_template_instance_by_uuid", + "kind": "platform", + "path": "/v1/instances/templates/{uuid}", + "method": "PATCH", + "parameters": [ + { + "name": "uuid", + "group": "path", + "style": "form", + "explode": true + } + ], + "bodyContentType": "application/json", + "title": "Update template instance by UUID", + "description": "Update a template instance by UUID.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "uuid" + ], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "$ref": "#/definitions/Instances.UpdateTemplateInstanceByUUIDRequestBody" + } + }, + "required": [ + "path", + "body" + ], + "definitions": { + "Instances.UpdateTemplateInstanceByUUIDRequestBody": { + "type": "object", + "required": [ + "prop", + "op" + ], + "properties": { + "id": { + "type": "string", + "description": "A client-provided identifier for tracking this operation in the response." + }, + "prop": { + "allOf": [ + { + "$ref": "#/definitions/Instances.MutableTemplateInstanceProperty" + } + ], + "description": "The property to modify." + }, + "op": { + "allOf": [ + { + "$ref": "#/definitions/Instances.MutableTemplateInstanceOperation" + } + ], + "description": "The operation to perform." + }, + "value": { + "description": "The value for the update operation. The type depends on the property and\noperation:\n- For \"tags\": array of strings\n- For \"delete_lock\": boolean\n- For \"autokill\": object with time_ms field" + } + } + }, + "Instances.MutableTemplateInstanceProperty": { + "type": "string", + "enum": [ + "tags", + "delete_lock", + "autokill" + ], + "description": "The mutable properties of a template instance that can be updated." + }, + "Instances.MutableTemplateInstanceOperation": { + "type": "string", + "enum": [ + "set", + "add", + "del" + ], + "description": "Mutable template instance operations." + } + } + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_delete_template_instance_by_uuid", + "kind": "platform", + "path": "/v1/instances/templates/{uuid}", + "method": "DELETE", + "parameters": [ + { + "name": "uuid", + "group": "path", + "style": "form", + "explode": true + } + ], + "title": "Delete template instance by UUID", + "description": "Delete a template instance by UUID.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "uuid" + ], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + } + }, + "required": [ + "path" + ] + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_wait_instances", + "kind": "platform", + "path": "/v1/instances/wait", + "method": "GET", + "parameters": [ + { + "name": "uuid", + "group": "query", + "style": "form", + "explode": true + }, + { + "name": "name", + "group": "query", + "style": "form", + "explode": true + }, + { + "name": "state", + "group": "query", + "style": "form", + "explode": true + }, + { + "name": "timeout_ms", + "group": "query", + "style": "form", + "explode": true + }, + { + "name": "timeout_s", + "group": "query", + "style": "form", + "explode": true + } + ], + "bodyContentType": "application/json", + "title": "Wait for Instances States", + "description": "Wait for instances to reach states.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "query": { + "type": "object", + "properties": { + "uuid": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The UUID of the resource." + }, + "name": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The name of the resource." + }, + "state": { + "type": "array", + "items": { + "$ref": "#/definitions/Instances.InstanceState" + }, + "description": "The desired state to wait for. Default is `running`." + }, + "timeout_ms": { + "type": "array", + "items": { + "type": "integer" + } + }, + "timeout_s": { + "type": "array", + "items": { + "type": "integer" + } + } + }, + "required": [], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "type": "array", + "items": { + "$ref": "#/definitions/Instances.WaitInstancesRequestItem" + } + } + }, + "required": [ + "body" + ], + "definitions": { + "Instances.InstanceState": { + "type": "string", + "enum": [ + "stopped", + "starting", + "running", + "draining", + "stopping", + "template", + "standby", + "deleted", + "checkpoint" + ], + "description": "The current state of an instance." + }, + "Instances.WaitInstancesRequestItem": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "The UUID of the resource." + }, + "name": { + "type": "string", + "description": "The name of the resource." + }, + "state": { + "allOf": [ + { + "$ref": "#/definitions/Instances.InstanceState" + } + ], + "description": "The desired state to wait for. Default is `running`." + }, + "timeout_ms": { + "type": "integer", + "description": "Deprecated: Use `timeout_s` instead. Timeout in milliseconds to\nwait for the instance to reach the desired state. If `timeout_s` is\nnot set, this value is converted by rounding up to the next full\nsecond. A value of -1 means to wait indefinitely.", + "deprecated": true + }, + "timeout_s": { + "type": "integer", + "description": "Timeout in seconds to wait for the instance to reach the desired\nstate. If the timeout is reached, the request will fail with an\nerror. A value of -1 means to wait indefinitely until the instance\nreaches the desired state. No wait performed for a value of 0." + } + }, + "description": "A single request item to wait for an instance's state." + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + { + "name": "unikraft_get_instance_by_uuid", + "kind": "platform", + "path": "/v1/instances/{uuid}", + "method": "GET", + "parameters": [ + { + "name": "uuid", + "group": "path", + "style": "form", + "explode": true + }, + { + "name": "details", + "group": "query", + "style": "form", + "explode": false + } + ], + "title": "Get Instance by UUID", + "description": "Get a single instance by UUID.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "uuid" + ], + "additionalProperties": false + }, + "query": { + "type": "object", + "properties": { + "details": { + "type": "boolean" + } + }, + "required": [], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + } + }, + "required": [ + "path" + ] + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + { + "name": "unikraft_delete_instance_by_uuid", + "kind": "platform", + "path": "/v1/instances/{uuid}", + "method": "DELETE", + "parameters": [ + { + "name": "uuid", + "group": "path", + "style": "form", + "explode": true + } + ], + "bodyContentType": "application/json", + "title": "Delete Instance by UUID", + "description": "Delete instance by UUID.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "uuid" + ], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "$ref": "#/definitions/Instances.DeleteInstanceByUUIDRequestBody" + } + }, + "required": [ + "path" + ], + "definitions": { + "Instances.DeleteInstanceByUUIDRequestBody": { + "type": "object", + "properties": { + "timeout_s": { + "type": "integer", + "description": "Timeout in seconds to wait for the instance to be deleted. No wait\nperformed for a value of 0." + }, + "dont_retain": { + "type": "boolean", + "description": "Delete immediately without retention. If the instance is already\nbeing retained, this will force its deletion. Ignored if retention\nfor instances is not configured." + } + }, + "description": "Parameters for deleting the instance by UUID." + } + } + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_update_instance_by_uuid", + "kind": "platform", + "path": "/v1/instances/{uuid}", + "method": "PATCH", + "parameters": [ + { + "name": "uuid", + "group": "path", + "style": "form", + "explode": true + } + ], + "bodyContentType": "application/json", + "title": "Update Instance by UUID", + "description": "Update instance by UUID.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "uuid" + ], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "$ref": "#/definitions/Instances.UpdateInstanceByUUIDRequestBody" + } + }, + "required": [ + "path", + "body" + ], + "definitions": { + "Instances.UpdateInstanceByUUIDRequestBody": { + "type": "object", + "required": [ + "prop", + "op" + ], + "properties": { + "id": { + "type": "string", + "description": "A client-provided identifier for tracking this operation in the response." + }, + "prop": { + "allOf": [ + { + "$ref": "#/definitions/Instances.MutableInstanceProperty" + } + ], + "description": "The property to modify." + }, + "op": { + "allOf": [ + { + "$ref": "#/definitions/Instances.MutableInstanceOperation" + } + ], + "description": "The operation to perform on the property." + }, + "value": { + "description": "The value for the update operation. The type depends on the property and operation:\n- For \"image\": image reference string, or object with image url,\ncredentials, headers and pull policy\n- For \"args\": string or array of strings\n- For \"env\": object (for SET/ADD) or string/array of strings (for DEL)\n- For \"memory_mb\": integer\n- For \"vcpus\": integer\n- For \"scale_to_zero\": object with cooldown_time_ms, policy, and stateful fields\n- For \"tags\": array of strings\n- For \"delete_lock\": boolean\n- For \"schedules\": array of schedule objects (with name, when, action, and optional args fields)\nfor SET/ADD, or array of schedule names for DEL. Use action \"exec\" together with args to\nexecute a command at the scheduled time.\n- For \"autokill\": object with time_ms and num_requests fields\n- For \"hostname\": string (valid DNS label)\n- For \"roms\": array of ROM objects (with name and image fields) for SET/ADD, or array of ROM names for DEL\n- For \"plugins\": array of plugin objects (with name, image, and optional config fields)\nfor SET/ADD. The deprecated `rom` field is accepted in place of `image`.\n- For \"dependencies\": array of instance identifiers (name or UUID)\n- For \"sched_priority\": SchedPriority enum value (\"normal\", \"medium\", \"high\", \"admin\")\n- For \"annotations\": object (for SET/ADD) or string/array of strings (for DEL)" + } + } + }, + "Instances.MutableInstanceProperty": { + "type": "string", + "enum": [ + "image", + "args", + "env", + "memory_mb", + "vcpus", + "scale_to_zero", + "tags", + "delete_lock", + "schedules", + "autokill", + "hostname", + "roms", + "dependencies", + "sched_priority", + "plugins", + "annotations" + ], + "description": "The mutable properties of an instance that can be updated." + }, + "Instances.MutableInstanceOperation": { + "type": "string", + "enum": [ + "set", + "add", + "del" + ], + "description": "Mutable instance operations." + } + } + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_get_instance_history_by_uuid", + "kind": "platform", + "path": "/v1/instances/{uuid}/history", + "method": "GET", + "parameters": [ + { + "name": "uuid", + "group": "path", + "style": "form", + "explode": true + } + ], + "title": "Get Instance Checkpoint History by UUID", + "description": "Get the checkpoint history of an instance by its UUID.\nReturns the ordered list of checkpoints associated with the instance.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "uuid" + ], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + } + }, + "required": [ + "path" + ] + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + { + "name": "unikraft_get_instance_logs_by_uuid", + "kind": "platform", + "path": "/v1/instances/{uuid}/log", + "method": "GET", + "parameters": [ + { + "name": "uuid", + "group": "path", + "style": "form", + "explode": true + } + ], + "bodyContentType": "application/json", + "title": "Get Instance Logs by UUID", + "description": "Get instance logs by UUID.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "uuid" + ], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "$ref": "#/definitions/Instances.GetInstanceLogsByUUIDRequestBody" + } + }, + "required": [ + "path", + "body" + ], + "definitions": { + "Instances.GetInstanceLogsByUUIDRequestBody": { + "type": "object", + "properties": { + "offset": { + "type": "integer", + "description": "The byte offset of the log output to receive. A negative sign makes the\noffset relative to the end of the log." + }, + "limit": { + "type": "integer", + "description": "The amount of bytes to return at most." + } + } + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + { + "name": "unikraft_get_instance_metrics_by_uuid", + "kind": "platform", + "path": "/v1/instances/{uuid}/metrics", + "method": "GET", + "parameters": [ + { + "name": "uuid", + "group": "path", + "style": "form", + "explode": true + } + ], + "title": "Get Instance Metrics by UUID", + "description": "Get instance metrics by UUID.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "uuid" + ], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + } + }, + "required": [ + "path" + ] + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + { + "name": "unikraft_start_instance_by_uuid", + "kind": "platform", + "path": "/v1/instances/{uuid}/start", + "method": "PUT", + "parameters": [ + { + "name": "uuid", + "group": "path", + "style": "form", + "explode": true + } + ], + "bodyContentType": "application/json", + "title": "Start Instance by UUID", + "description": "Start instance by UUID.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "uuid" + ], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "$ref": "#/definitions/Instances.StartInstanceByUUIDRequestBody" + } + }, + "required": [ + "path", + "body" + ], + "definitions": { + "Instances.StartInstanceByUUIDRequestBody": { + "type": "object", + "properties": { + "wait_timeout_ms": { + "type": "integer", + "description": "Deprecated: Use `timeout_s` instead. Timeout in milliseconds to\nwait for the instance to reach running state. If `timeout_s` is\nnot set, this value is converted by rounding up to the next full\nsecond. No wait performed for a value of 0.", + "deprecated": true + }, + "timeout_s": { + "type": "integer", + "description": "Timeout in seconds to wait for the instance to reach running\nstate. If you start your instance, you can wait for it to\nfinish starting with a blocking API call if you specify a wait\ntimeout greater than zero. No wait performed for a value of 0." + } + }, + "description": "Parameters for starting the instance." + } + } + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_stop_instance_by_uuid", + "kind": "platform", + "path": "/v1/instances/{uuid}/stop", + "method": "PUT", + "parameters": [ + { + "name": "uuid", + "group": "path", + "style": "form", + "explode": true + } + ], + "bodyContentType": "application/json", + "title": "Stop Instance by UUID", + "description": "Stop instance by UUID.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "uuid" + ], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "$ref": "#/definitions/Instances.StopInstanceByUUIDRequestBody" + } + }, + "required": [ + "path", + "body" + ], + "definitions": { + "Instances.StopInstanceByUUIDRequestBody": { + "type": "object", + "properties": { + "force": { + "type": "boolean", + "description": "Whether to immediately force stop the instance." + }, + "drain_timeout_ms": { + "type": "integer", + "description": "Timeout for draining connections in milliseconds.\nNo draining will occur if set to 0. The instance\ndoes not receive new connections in the draining\nphase. The instance is stopped when the last\nconnection has been closed or the timeout expired.\nThe maximum timeout may vary. Use -1 for the\nlargest possible value. Ignored if force is set.\n\nNote: This endpoint does not block. Use the wait\nendpoint for the instance to reach the stopped\nstate.", + "minimum": -1 + }, + "quick": { + "type": "boolean", + "description": "Whether to perform a quick shutdown. This flag is\noverridden by force." + }, + "ifstate": { + "type": "string", + "description": "Only stop the instance if it is in this state." + }, + "dump": { + "type": "boolean", + "description": "If set, forces the VMM to shutdown immediately and generate a coredump.\nCan only be used in conjunction with force." + } + }, + "description": "Parameters for stopping the instance." + } + } + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_suspend_instance_by_uuid", + "kind": "platform", + "path": "/v1/instances/{uuid}/suspend", + "method": "PUT", + "parameters": [ + { + "name": "uuid", + "group": "path", + "style": "form", + "explode": true + } + ], + "bodyContentType": "application/json", + "title": "Suspend Instance by UUID", + "description": "Suspend instance by UUID.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "uuid" + ], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "$ref": "#/definitions/Instances.SuspendInstanceByUUIDRequestBody" + } + }, + "required": [ + "path", + "body" + ], + "definitions": { + "Instances.SuspendInstanceByUUIDRequestBody": { + "type": "object", + "properties": { + "drain_timeout_ms": { + "type": "integer", + "description": "Timeout for draining connections in milliseconds. No draining\nwill occur if set to 0. Use -1 for the largest possible value.", + "minimum": -1 + } + }, + "description": "Parameters for suspending the instance." + } + } + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_wait_instance_by_uuid", + "kind": "platform", + "path": "/v1/instances/{uuid}/wait", + "method": "GET", + "parameters": [ + { + "name": "uuid", + "group": "path", + "style": "form", + "explode": true + } + ], + "bodyContentType": "application/json", + "title": "Wait for Instance State by UUID", + "description": "Wait for instance state by UUID.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "uuid" + ], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "$ref": "#/definitions/Instances.WaitInstanceByUUIDRequestBody" + } + }, + "required": [ + "path", + "body" + ], + "definitions": { + "Instances.WaitInstanceByUUIDRequestBody": { + "type": "object", + "required": [ + "state" + ], + "properties": { + "state": { + "allOf": [ + { + "$ref": "#/definitions/Instances.InstanceState" + } + ], + "description": "The desired state to wait for. Default is `running`." + }, + "timeout_ms": { + "type": "integer", + "description": "Deprecated: Use `timeout_s` instead. Timeout in milliseconds to\nwait for the instance to reach the desired state. If `timeout_s` is\nnot set, this value is converted by rounding up to the next full\nsecond. A value of -1 means to wait indefinitely.", + "deprecated": true + }, + "timeout_s": { + "type": "integer", + "description": "Timeout in seconds to wait for the instance to reach the desired\nstate. If the timeout is reached, the request will fail with an\nerror. A value of -1 means to wait indefinitely until the instance\nreaches the desired state. No wait performed for a value of 0." + } + }, + "description": "Wait parameters." + }, + "Instances.InstanceState": { + "type": "string", + "enum": [ + "stopped", + "starting", + "running", + "draining", + "stopping", + "template", + "standby", + "deleted", + "checkpoint" + ], + "description": "The current state of an instance." + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + { + "name": "unikraft_create_service_group", + "kind": "platform", + "path": "/v1/services", + "method": "POST", + "parameters": [], + "bodyContentType": "application/json", + "title": "Create Service Group", + "description": "Create a new service group.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "$ref": "#/definitions/Services.CreateServiceGroupRequest" + } + }, + "required": [ + "body" + ], + "definitions": { + "Services.CreateServiceGroupRequest": { + "type": "object", + "required": [ + "services" + ], + "properties": { + "name": { + "type": "string", + "description": "Name of the service group. This is a human-readable name that can be used\nto identify the service group. The name must be unique within the context\nof your account. If no name is specified, a random name is generated for\nyou. The name can also be used to identify the service group in API calls." + }, + "services": { + "type": "array", + "items": { + "$ref": "#/definitions/Services.Service" + }, + "description": "Services to expose. At least one service is required." + }, + "domains": { + "type": "array", + "items": { + "$ref": "#/definitions/Services.CreateServiceGroupRequestDomain" + }, + "description": "Description of domains associated with the service group." + }, + "soft_limit": { + "type": "integer", + "description": "The soft limit is used by the Unikraft Cloud load balancer to decide when\nto wake up another standby instance.\n\nFor example, if the soft limit is set to 5 and the service consists of 2\nstandby instances, one of the instances receives up to 5 concurrent\nrequests. The 6th parallel requests wakes up the second instance. If\nthere are no more standby instances to wake up, the number of requests\nassigned to each instance will exceed the soft limit. The load balancer\nmakes sure that when the number of in-flight requests goes down again,\ninstances are put into standby as fast as possible.", + "minimum": 0 + }, + "hard_limit": { + "type": "integer", + "description": "The hard limit defines the maximum number of concurrent requests that an\ninstance assigned to the this service can handle.\n\nThe load balancer will never assign more requests to a single instance. In\ncase there are no other instances available, excess requests fail (i.e.,\nthey are blocked and not queued).", + "minimum": 0 + }, + "autokill": { + "allOf": [ + { + "$ref": "#/definitions/Services.CreateServiceGroupRequestAutokill" + } + ], + "description": "Automatic delete-on-idle configuration." + } + }, + "description": "The request message for creating a new service group." + }, + "Services.Service": { + "type": "object", + "required": [ + "port" + ], + "properties": { + "port": { + "type": "integer", + "description": "This is the public-facing port that the service will be accessible from\non the Internet.", + "minimum": 0 + }, + "destination_port": { + "type": "integer", + "description": "The port number that the instance is listening on. This is the internal\nport which Unikraft Cloud will forward traffic to.", + "minimum": 0 + }, + "protocol": { + "$ref": "#/definitions/Services.ServiceProtocol" + }, + "ip": { + "type": "string" + }, + "handlers": { + "type": "array", + "items": { + "$ref": "#/definitions/Services.ConnectionHandler" + }, + "description": "Connection handlers to use for the service. Handlers define how the\nservice will handle incoming connections and forward traffic from the\nInternet to your application. For example, a service can be configured\nto terminate TLS connections, redirect HTTP traffic, or enable HTTP mode\nfor load balancing. You configure the handlers for every published\nservice port individually." + } + }, + "description": "A service connects a public-facing port to an internal destination port on\nwhich an application instance listens on. Additional handlers can be\ndefined for each published port in order to define how the service will\nhandle incoming connections and forward traffic from the Internet to your\napplication. For example, a service can be configured to terminate TLS\nconnections, redirect HTTP traffic, or enable HTTP mode for load balancing." + }, + "Services.ServiceProtocol": { + "type": "string", + "enum": [ + "tcp", + "udp" + ], + "description": "Protocol for a service." + }, + "Services.ConnectionHandler": { + "type": "string", + "enum": [ + "tls", + "http", + "redirect" + ], + "description": "Connection handlers to use for the service.\n\nHandlers define how the service will handle incoming connections and\nforward traffic from the Internet to your application. For example, a\nservice can be configured to terminate TLS connections, redirect HTTP\ntraffic, or enable HTTP mode for load balancing. You configure the handlers\nfor every published service port individually.\n\nThere are currently 3 supported handlers:\n\n| Handler | Description |\n|------------|-------------|\n| `tls` | Terminate the TLS connection at the Unikraft Cloud gateway using our wildcard certificate issued for the kraft.cloud domain. The gateway forwards the unencrypted traffic to your application. |\n| `http` | Enable HTTP mode on the load balancer to load balance on the level of individual HTTP requests. In this mode, only HTTP connections are accepted. If this option is not set the load balancer works in TCP mode and distributes TCP connections. |\n| `redirect` | Redirect traffic from the source port to the destination port. |\n\nNote that there is a set of constraints when publishing ports:\n\n- Port 80: MUST have \"http\" and MUST not have \"tls\" set;\n- Port 443: MUST have http and tls set;\n- The `redirect` handler can only be set on port 80 (HTTP) to redirect to\nport 443 (HTTPS);\n- All other ports MUST have tls and MUST not have http set." + }, + "Services.CreateServiceGroupRequestDomain": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "description": "Publicly accessible domain name. If this name ends in a period `.` it must\nbe a valid Full Qualified Domain Name (FQDN), otherwise it will become a\nsubdomain of the target metro." + }, + "certificate": { + "allOf": [ + { + "$ref": "#/definitions/Common.NameOrUUID" + } + ], + "description": "Use an existing certificate for the domain. If this field is\nspecified, the domain must be associated with a valid certificate." + } + }, + "description": "A domain to attach when creating a service group." + }, + "Common.NameOrUUID": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "The UUID of the resource." + }, + "name": { + "type": "string", + "description": "The name of the resource." + } + }, + "description": "An identifier for a resource — either a name or a UUID, but not both.", + "oneOf": [ + { + "required": [ + "uuid" + ], + "not": { + "required": [ + "name" + ] + } + }, + { + "required": [ + "name" + ], + "not": { + "required": [ + "uuid" + ] + } + } + ] + }, + "Services.CreateServiceGroupRequestAutokill": { + "type": "object", + "properties": { + "time_ms": { + "type": "integer", + "description": "Time in milliseconds after the service group becomes empty before it is\ndeleted. A value of 0 disables autokill.", + "minimum": 0 + } + }, + "description": "Autokill configuration when creating a service group." + } + } + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_get_service_groups", + "kind": "platform", + "path": "/v1/services", + "method": "GET", + "parameters": [ + { + "name": "uuid", + "group": "query", + "style": "form", + "explode": true + }, + { + "name": "name", + "group": "query", + "style": "form", + "explode": true + }, + { + "name": "details", + "group": "query", + "style": "form", + "explode": false + }, + { + "name": "count", + "group": "query", + "style": "form", + "explode": false + }, + { + "name": "from", + "group": "query", + "style": "form", + "explode": false + }, + { + "name": "order", + "group": "query", + "style": "form", + "explode": false + }, + { + "name": "sortby", + "group": "query", + "style": "form", + "explode": false + } + ], + "bodyContentType": "application/json", + "title": "List Service Groups", + "description": "List service groups.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "query": { + "type": "object", + "properties": { + "uuid": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The UUID of the resource." + }, + "name": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The name of the resource." + }, + "details": { + "type": "boolean" + }, + "count": { + "type": "integer", + "minimum": 0 + }, + "from": { + "type": "string" + }, + "order": { + "$ref": "#/definitions/Common.PaginationOrder" + }, + "sortby": { + "$ref": "#/definitions/Common.PaginationSortBy" + } + }, + "required": [], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "type": "array", + "items": { + "$ref": "#/definitions/Common.NameOrUUID" + } + } + }, + "required": [], + "definitions": { + "Common.PaginationOrder": { + "type": "string", + "enum": [ + "asc", + "desc" + ], + "description": "The sort order used by list endpoints." + }, + "Common.PaginationSortBy": { + "type": "string", + "enum": [ + "create_time" + ], + "description": "The sort field used by list endpoints." + }, + "Common.NameOrUUID": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "The UUID of the resource." + }, + "name": { + "type": "string", + "description": "The name of the resource." + } + }, + "description": "An identifier for a resource — either a name or a UUID, but not both.", + "oneOf": [ + { + "required": [ + "uuid" + ], + "not": { + "required": [ + "name" + ] + } + }, + { + "required": [ + "name" + ], + "not": { + "required": [ + "uuid" + ] + } + } + ] + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + { + "name": "unikraft_update_service_groups", + "kind": "platform", + "path": "/v1/services", + "method": "PATCH", + "parameters": [], + "bodyContentType": "application/json", + "title": "Update Service Groups", + "description": "Update service groups.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "type": "array", + "items": { + "$ref": "#/definitions/Services.UpdateServiceGroupsRequestItem" + } + } + }, + "required": [ + "body" + ], + "definitions": { + "Services.UpdateServiceGroupsRequestItem": { + "type": "object", + "required": [ + "prop", + "op" + ], + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "The UUID of the resource." + }, + "name": { + "type": "string", + "description": "The name of the resource." + }, + "id": { + "type": "string", + "description": "A client-provided identifier for tracking this operation in the response." + }, + "prop": { + "allOf": [ + { + "$ref": "#/definitions/Services.MutableServiceGroupProperty" + } + ], + "description": "The property to modify." + }, + "op": { + "allOf": [ + { + "$ref": "#/definitions/Services.MutableServiceGroupOperation" + } + ], + "description": "The operation to perform." + }, + "value": { + "description": "The value for the update operation. The type depends on the property and operation:\n- For \"image\": string\n- For \"args\": string or array of strings\n- For \"env\": object (for SET/ADD) or string/array of strings (for DEL)\n- For \"memory_mb\": integer\n- For \"vcpus\": integer\n- For \"scale_to_zero\": object with cooldown_time_ms, policy, and stateful fields\n- For \"tags\": array of strings\n- For \"delete_lock\": boolean\n- For \"schedules\": array of schedule objects (with name, when, action, and optional args fields).\nUse action \"exec\" together with args to execute a command at the scheduled time.\n- For \"autokill\": object with time_ms and num_requests fields\n- For \"hostname\": string (valid DNS label)\n- For \"roms\": array of ROM objects (with name and image fields) for SET/ADD, or array of ROM names for DEL\n- For \"dependencies\": array of instance identifiers (name or UUID)\n- For \"sched_priority\": SchedPriority enum value (\"normal\", \"medium\", \"high\", \"admin\")" + } + }, + "description": "A single update operation to be applied to a service group." + }, + "Services.MutableServiceGroupProperty": { + "type": "string", + "enum": [ + "services", + "domains", + "soft_limit", + "hard_limit", + "autokill" + ], + "description": "Mutable service group properties." + }, + "Services.MutableServiceGroupOperation": { + "type": "string", + "enum": [ + "set", + "add", + "del" + ], + "description": "The mutable operations available on a service group's properties." + } + } + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_delete_service_groups", + "kind": "platform", + "path": "/v1/services", + "method": "DELETE", + "parameters": [], + "bodyContentType": "application/json", + "title": "Delete Service Groups", + "description": "Delete service groups by ID(s).", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "type": "array", + "items": { + "$ref": "#/definitions/Common.NameOrUUID" + } + } + }, + "required": [ + "body" + ], + "definitions": { + "Common.NameOrUUID": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "The UUID of the resource." + }, + "name": { + "type": "string", + "description": "The name of the resource." + } + }, + "description": "An identifier for a resource — either a name or a UUID, but not both.", + "oneOf": [ + { + "required": [ + "uuid" + ], + "not": { + "required": [ + "name" + ] + } + }, + { + "required": [ + "name" + ], + "not": { + "required": [ + "uuid" + ] + } + } + ] + } + } + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_create_autoscale_configurations", + "kind": "platform", + "path": "/v1/services/autoscale", + "method": "POST", + "parameters": [], + "bodyContentType": "application/json", + "title": "Create Autoscale Configurations", + "description": "Create one or more autoscale configurations for the specified service groups.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "type": "array", + "items": { + "$ref": "#/definitions/Autoscale.CreateAutoscaleConfigurationsRequestConfiguration" + } + } + }, + "required": [ + "body" + ], + "definitions": { + "Autoscale.CreateAutoscaleConfigurationsRequestConfiguration": { + "type": "object", + "required": [ + "create_args" + ], + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "The UUID of the resource." + }, + "name": { + "type": "string", + "description": "The name of the resource." + }, + "min_size": { + "type": "integer", + "description": "The minimum number of instances to keep running." + }, + "max_size": { + "type": "integer", + "description": "The maximum number of instances to keep running." + }, + "warmup_time_ms": { + "type": "integer", + "description": "The warmup time in milliseconds for new instances." + }, + "cooldown_time_ms": { + "type": "integer", + "description": "The cooldown time in milliseconds for the autoscale configuration." + }, + "create_args": { + "allOf": [ + { + "$ref": "#/definitions/Autoscale.ConfigurationInstanceCreateArgs" + } + ], + "description": "The arguments to use when creating instances." + }, + "policies": { + "type": "array", + "items": { + "$ref": "#/definitions/Autoscale.AutoscalePolicy" + }, + "description": "The policies to apply to the autoscale configuration." + } + }, + "oneOf": [ + { + "required": [ + "uuid" + ] + }, + { + "required": [ + "name" + ] + } + ] + }, + "Autoscale.ConfigurationInstanceCreateArgs": { + "type": "object", + "properties": { + "roms": { + "allOf": [ + { + "$ref": "#/definitions/Autoscale.InstanceCreateArgsInstanceCreateRequestRoms" + } + ], + "description": "The ROM to use for the autoscale configuration." + }, + "template": { + "allOf": [ + { + "$ref": "#/definitions/Common.NameOrUUID" + } + ], + "description": "The template to use for the autoscale configuration." + } + }, + "oneOf": [ + { + "required": [ + "roms" + ] + }, + { + "required": [ + "template" + ] + } + ] + }, + "Autoscale.InstanceCreateArgsInstanceCreateRequestRoms": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "description": "The name of the ROM." + }, + "image": { + "anyOf": [ + { + "type": "string" + }, + { + "$ref": "#/definitions/Instances.ImageSpec" + } + ], + "description": "The image of the ROM to use for the autoscale configuration. Mutually\nexclusive with `files`. Accepts either a plain image reference string\n(`\"nginx:latest\"`) or an object carrying additional pull configuration\n(`{\"url\": \"nginx:latest\", \"pull_policy\": \"always\"}`)." + }, + "files": { + "type": "array", + "items": { + "$ref": "#/definitions/Common.InlineFile" + }, + "description": "Inline files to use as the ROM content. When specified, the platform\ncreates an EROFS image from the provided files. Mutually exclusive with\n`image`." + } + }, + "description": "A ROM to use for an autoscale instance create configuration." + }, + "Instances.ImageSpec": { + "type": "object", + "required": [ + "url" + ], + "properties": { + "url": { + "type": "string", + "description": "The image URL" + }, + "credentials": { + "type": "string", + "description": "Optional credentials for authenticating to an OCI registry.\nOnly valid for OCI registry URLs; the platform rejects this\nfield for non-OCI schemes." + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Optional HTTP headers to send when fetching the image." + }, + "pull_policy": { + "allOf": [ + { + "$ref": "#/definitions/Instances.PullPolicy" + } + ], + "description": "Controls when the image is pulled relative to what is already cached on\nthe node." + } + }, + "description": "ImageSpec describes an image to use for an instance." + }, + "Instances.PullPolicy": { + "type": "string", + "enum": [ + "always", + "if_not_present", + "never" + ], + "description": "PullPolicy defines when an image should be pulled." + }, + "Common.InlineFile": { + "type": "object", + "required": [ + "path", + "data" + ], + "properties": { + "path": { + "type": "string", + "description": "The file path within the image." + }, + "encoding": { + "allOf": [ + { + "$ref": "#/definitions/Common.InlineDataEncoding" + } + ], + "description": "The encoding of the data field. Defaults to \"text\"." + }, + "data": { + "type": "string", + "description": "The file data, encoded according to the encoding field." + } + }, + "description": "An inline file entry represents a single file within an image." + }, + "Common.InlineDataEncoding": { + "type": "string", + "enum": [ + "text", + "base64" + ], + "description": "Encoding type for inline file data." + }, + "Common.NameOrUUID": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "The UUID of the resource." + }, + "name": { + "type": "string", + "description": "The name of the resource." + } + }, + "description": "An identifier for a resource — either a name or a UUID, but not both.", + "oneOf": [ + { + "required": [ + "uuid" + ], + "not": { + "required": [ + "name" + ] + } + }, + { + "required": [ + "name" + ], + "not": { + "required": [ + "uuid" + ] + } + } + ] + }, + "Autoscale.AutoscalePolicy": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "description": "The name of the policy." + }, + "enabled": { + "type": "boolean", + "description": "Whether the policy is enabled." + }, + "metric": { + "allOf": [ + { + "$ref": "#/definitions/Autoscale.StepPolicyMetric" + } + ], + "description": "Metric to use for the step policy (only for step policies)." + }, + "adjustment_type": { + "allOf": [ + { + "$ref": "#/definitions/Autoscale.AdjustmentType" + } + ], + "description": "The type of adjustment to be made in the step policy (only for step policies)." + }, + "steps": { + "type": "array", + "items": { + "$ref": "#/definitions/Autoscale.AutoscalePolicyStep" + }, + "description": "The steps for the step policy. Each step defines an adjustment value and\noptional bounds." + } + }, + "description": "AutoscalePolicy defines the autoscale policy for a service. Right now it\ncontains fields from both the `ondemand` and `step` policies. They are\nmarked both as optional, so only one of them should be set at a time. This\nis a current limitation of the API design." + }, + "Autoscale.StepPolicyMetric": { + "type": "string", + "enum": [ + "cpu" + ], + "description": "The step policy is a type of autoscaling policy that scales the number of\ninstances in a service by a fixed number of instances at each step. It uses\na metric to determine when to scale up or down." + }, + "Autoscale.AdjustmentType": { + "type": "string", + "enum": [ + "change", + "exact", + "percentage" + ], + "description": "AdjustmentType defines the type of adjustment to be made in an autoscaling\nstep policy." + }, + "Autoscale.AutoscalePolicyStep": { + "type": "object", + "required": [ + "adjustment" + ], + "properties": { + "adjustment": { + "type": "integer", + "description": "The adjustment value for the step." + }, + "lower_bound": { + "type": "integer", + "description": "Lower bound for the step." + }, + "upper_bound": { + "type": "integer", + "description": "Upper bound for the step." + } + }, + "description": "A single step in a step autoscaling policy." + } + } + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_get_autoscale_configurations", + "kind": "platform", + "path": "/v1/services/autoscale", + "method": "GET", + "parameters": [ + { + "name": "uuid", + "group": "query", + "style": "form", + "explode": true + }, + { + "name": "name", + "group": "query", + "style": "form", + "explode": true + } + ], + "bodyContentType": "application/json", + "title": "List Autoscale Configurations", + "description": "List autoscale configurations for the specified service groups.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "query": { + "type": "object", + "properties": { + "uuid": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The UUID of the resource." + }, + "name": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The name of the resource." + } + }, + "required": [], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "type": "array", + "items": { + "$ref": "#/definitions/Common.NameOrUUID" + } + } + }, + "required": [ + "body" + ], + "definitions": { + "Common.NameOrUUID": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "The UUID of the resource." + }, + "name": { + "type": "string", + "description": "The name of the resource." + } + }, + "description": "An identifier for a resource — either a name or a UUID, but not both.", + "oneOf": [ + { + "required": [ + "uuid" + ], + "not": { + "required": [ + "name" + ] + } + }, + { + "required": [ + "name" + ], + "not": { + "required": [ + "uuid" + ] + } + } + ] + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + { + "name": "unikraft_delete_autoscale_configurations", + "kind": "platform", + "path": "/v1/services/autoscale", + "method": "DELETE", + "parameters": [], + "bodyContentType": "application/json", + "title": "Delete Autoscale Configurations", + "description": "Delete autoscale configurations for the specified service groups.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "type": "array", + "items": { + "$ref": "#/definitions/Common.NameOrUUID" + } + } + }, + "required": [ + "body" + ], + "definitions": { + "Common.NameOrUUID": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "The UUID of the resource." + }, + "name": { + "type": "string", + "description": "The name of the resource." + } + }, + "description": "An identifier for a resource — either a name or a UUID, but not both.", + "oneOf": [ + { + "required": [ + "uuid" + ], + "not": { + "required": [ + "name" + ] + } + }, + { + "required": [ + "name" + ], + "not": { + "required": [ + "uuid" + ] + } + } + ] + } + } + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_get_service_group_by_uuid", + "kind": "platform", + "path": "/v1/services/{uuid}", + "method": "GET", + "parameters": [ + { + "name": "uuid", + "group": "path", + "style": "form", + "explode": true + }, + { + "name": "details", + "group": "query", + "style": "form", + "explode": false + } + ], + "title": "Get Service Group by UUID", + "description": "Get a service group by UUID.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "uuid" + ], + "additionalProperties": false + }, + "query": { + "type": "object", + "properties": { + "details": { + "type": "boolean" + } + }, + "required": [], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + } + }, + "required": [ + "path" + ] + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + { + "name": "unikraft_update_service_group_by_uuid", + "kind": "platform", + "path": "/v1/services/{uuid}", + "method": "PATCH", + "parameters": [ + { + "name": "uuid", + "group": "path", + "style": "form", + "explode": true + } + ], + "bodyContentType": "application/json", + "title": "Update Service Group by UUID", + "description": "Update a service group by UUID.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "uuid" + ], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "$ref": "#/definitions/Services.UpdateServiceGroupByUUIDRequestBody" + } + }, + "required": [ + "path", + "body" + ], + "definitions": { + "Services.UpdateServiceGroupByUUIDRequestBody": { + "type": "object", + "required": [ + "prop", + "op" + ], + "properties": { + "id": { + "type": "string", + "description": "A client-provided identifier for tracking this operation in the response." + }, + "prop": { + "allOf": [ + { + "$ref": "#/definitions/Services.MutableServiceGroupProperty" + } + ], + "description": "The property to modify." + }, + "op": { + "allOf": [ + { + "$ref": "#/definitions/Services.MutableServiceGroupOperation" + } + ], + "description": "The operation to perform." + }, + "value": { + "description": "The value for the update operation. The type depends on the property and operation:\n- For \"image\": string\n- For \"args\": string or array of strings\n- For \"env\": object (for SET/ADD) or string/array of strings (for DEL)\n- For \"memory_mb\": integer\n- For \"vcpus\": integer\n- For \"scale_to_zero\": object with cooldown_time_ms, policy, and stateful fields\n- For \"tags\": array of strings\n- For \"delete_lock\": boolean\n- For \"schedules\": array of schedule objects (with name, when, action, and optional args fields).\nUse action \"exec\" together with args to execute a command at the scheduled time.\n- For \"autokill\": object with time_ms and num_requests fields\n- For \"hostname\": string (valid DNS label)\n- For \"roms\": array of ROM objects (with name and image fields) for SET/ADD, or array of ROM names for DEL\n- For \"dependencies\": array of instance identifiers (name or UUID)\n- For \"sched_priority\": SchedPriority enum value (\"normal\", \"medium\", \"high\", \"admin\")" + } + } + }, + "Services.MutableServiceGroupProperty": { + "type": "string", + "enum": [ + "services", + "domains", + "soft_limit", + "hard_limit", + "autokill" + ], + "description": "Mutable service group properties." + }, + "Services.MutableServiceGroupOperation": { + "type": "string", + "enum": [ + "set", + "add", + "del" + ], + "description": "The mutable operations available on a service group's properties." + } + } + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_delete_service_group_by_uuid", + "kind": "platform", + "path": "/v1/services/{uuid}", + "method": "DELETE", + "parameters": [ + { + "name": "uuid", + "group": "path", + "style": "form", + "explode": true + } + ], + "title": "Delete Service Group by UUID", + "description": "Delete a service group by UUID.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "uuid" + ], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + } + }, + "required": [ + "path" + ] + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_create_autoscale_configuration_by_service_group_uuid", + "kind": "platform", + "path": "/v1/services/{uuid}/autoscale", + "method": "POST", + "parameters": [ + { + "name": "uuid", + "group": "path", + "style": "form", + "explode": true + } + ], + "bodyContentType": "application/json", + "title": "Create Autoscale Configuration by Service Group UUID", + "description": "Create an autoscale configuration for a service group by UUID.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "uuid" + ], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "$ref": "#/definitions/Autoscale.CreateAutoscaleConfigurationByServiceGroupUUIDRequest" + } + }, + "required": [ + "path", + "body" + ], + "definitions": { + "Autoscale.CreateAutoscaleConfigurationByServiceGroupUUIDRequest": { + "type": "object", + "required": [ + "uuid", + "create_args" + ], + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "The UUID of the service to create a configuration for." + }, + "min_size": { + "type": "integer", + "description": "The minimum number of instances to keep running." + }, + "max_size": { + "type": "integer", + "description": "The maximum number of instances to keep running." + }, + "warmup_time_ms": { + "type": "integer", + "description": "The warmup time in milliseconds for new instances." + }, + "cooldown_time_ms": { + "type": "integer", + "description": "The cooldown time in milliseconds for the autoscale configuration." + }, + "create_args": { + "allOf": [ + { + "$ref": "#/definitions/Autoscale.CreateAutoscaleConfigurationByServiceGroupUUIDRequestInstanceCreateArgs" + } + ], + "description": "The arguments to use when creating instances." + }, + "policies": { + "type": "array", + "items": { + "$ref": "#/definitions/Autoscale.AutoscalePolicy" + }, + "description": "The policies to apply to the autoscale configuration." + } + }, + "description": "The request message to create an autoscale configuration for a service group\nbased on its UUID." + }, + "Autoscale.CreateAutoscaleConfigurationByServiceGroupUUIDRequestInstanceCreateArgs": { + "type": "object", + "properties": { + "roms": { + "allOf": [ + { + "$ref": "#/definitions/Autoscale.InstanceCreateArgsInstanceCreateRequestRoms" + } + ], + "description": "The ROM to use for the autoscale configuration." + }, + "template": { + "allOf": [ + { + "$ref": "#/definitions/Common.NameOrUUID" + } + ], + "description": "The template to use for the autoscale configuration." + } + }, + "oneOf": [ + { + "required": [ + "roms" + ] + }, + { + "required": [ + "template" + ] + } + ] + }, + "Autoscale.InstanceCreateArgsInstanceCreateRequestRoms": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "description": "The name of the ROM." + }, + "image": { + "anyOf": [ + { + "type": "string" + }, + { + "$ref": "#/definitions/Instances.ImageSpec" + } + ], + "description": "The image of the ROM to use for the autoscale configuration. Mutually\nexclusive with `files`. Accepts either a plain image reference string\n(`\"nginx:latest\"`) or an object carrying additional pull configuration\n(`{\"url\": \"nginx:latest\", \"pull_policy\": \"always\"}`)." + }, + "files": { + "type": "array", + "items": { + "$ref": "#/definitions/Common.InlineFile" + }, + "description": "Inline files to use as the ROM content. When specified, the platform\ncreates an EROFS image from the provided files. Mutually exclusive with\n`image`." + } + }, + "description": "A ROM to use for an autoscale instance create configuration." + }, + "Instances.ImageSpec": { + "type": "object", + "required": [ + "url" + ], + "properties": { + "url": { + "type": "string", + "description": "The image URL" + }, + "credentials": { + "type": "string", + "description": "Optional credentials for authenticating to an OCI registry.\nOnly valid for OCI registry URLs; the platform rejects this\nfield for non-OCI schemes." + }, + "headers": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "Optional HTTP headers to send when fetching the image." + }, + "pull_policy": { + "allOf": [ + { + "$ref": "#/definitions/Instances.PullPolicy" + } + ], + "description": "Controls when the image is pulled relative to what is already cached on\nthe node." + } + }, + "description": "ImageSpec describes an image to use for an instance." + }, + "Instances.PullPolicy": { + "type": "string", + "enum": [ + "always", + "if_not_present", + "never" + ], + "description": "PullPolicy defines when an image should be pulled." + }, + "Common.InlineFile": { + "type": "object", + "required": [ + "path", + "data" + ], + "properties": { + "path": { + "type": "string", + "description": "The file path within the image." + }, + "encoding": { + "allOf": [ + { + "$ref": "#/definitions/Common.InlineDataEncoding" + } + ], + "description": "The encoding of the data field. Defaults to \"text\"." + }, + "data": { + "type": "string", + "description": "The file data, encoded according to the encoding field." + } + }, + "description": "An inline file entry represents a single file within an image." + }, + "Common.InlineDataEncoding": { + "type": "string", + "enum": [ + "text", + "base64" + ], + "description": "Encoding type for inline file data." + }, + "Common.NameOrUUID": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "The UUID of the resource." + }, + "name": { + "type": "string", + "description": "The name of the resource." + } + }, + "description": "An identifier for a resource — either a name or a UUID, but not both.", + "oneOf": [ + { + "required": [ + "uuid" + ], + "not": { + "required": [ + "name" + ] + } + }, + { + "required": [ + "name" + ], + "not": { + "required": [ + "uuid" + ] + } + } + ] + }, + "Autoscale.AutoscalePolicy": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "description": "The name of the policy." + }, + "enabled": { + "type": "boolean", + "description": "Whether the policy is enabled." + }, + "metric": { + "allOf": [ + { + "$ref": "#/definitions/Autoscale.StepPolicyMetric" + } + ], + "description": "Metric to use for the step policy (only for step policies)." + }, + "adjustment_type": { + "allOf": [ + { + "$ref": "#/definitions/Autoscale.AdjustmentType" + } + ], + "description": "The type of adjustment to be made in the step policy (only for step policies)." + }, + "steps": { + "type": "array", + "items": { + "$ref": "#/definitions/Autoscale.AutoscalePolicyStep" + }, + "description": "The steps for the step policy. Each step defines an adjustment value and\noptional bounds." + } + }, + "description": "AutoscalePolicy defines the autoscale policy for a service. Right now it\ncontains fields from both the `ondemand` and `step` policies. They are\nmarked both as optional, so only one of them should be set at a time. This\nis a current limitation of the API design." + }, + "Autoscale.StepPolicyMetric": { + "type": "string", + "enum": [ + "cpu" + ], + "description": "The step policy is a type of autoscaling policy that scales the number of\ninstances in a service by a fixed number of instances at each step. It uses\na metric to determine when to scale up or down." + }, + "Autoscale.AdjustmentType": { + "type": "string", + "enum": [ + "change", + "exact", + "percentage" + ], + "description": "AdjustmentType defines the type of adjustment to be made in an autoscaling\nstep policy." + }, + "Autoscale.AutoscalePolicyStep": { + "type": "object", + "required": [ + "adjustment" + ], + "properties": { + "adjustment": { + "type": "integer", + "description": "The adjustment value for the step." + }, + "lower_bound": { + "type": "integer", + "description": "Lower bound for the step." + }, + "upper_bound": { + "type": "integer", + "description": "Upper bound for the step." + } + }, + "description": "A single step in a step autoscaling policy." + } + } + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_get_autoscale_configurations_by_service_group_uuid", + "kind": "platform", + "path": "/v1/services/{uuid}/autoscale", + "method": "GET", + "parameters": [ + { + "name": "uuid", + "group": "path", + "style": "form", + "explode": true + } + ], + "title": "Get Autoscale Configurations by Service Group UUID", + "description": "Get autoscale configurations for a service group by UUID.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "uuid" + ], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + } + }, + "required": [ + "path" + ] + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + { + "name": "unikraft_delete_autoscale_configurations_by_service_group_uuid", + "kind": "platform", + "path": "/v1/services/{uuid}/autoscale", + "method": "DELETE", + "parameters": [ + { + "name": "uuid", + "group": "path", + "style": "form", + "explode": true + } + ], + "title": "Delete Autoscale Configurations by Service Group UUID", + "description": "Delete the autoscale configuration for a service group by UUID.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "uuid" + ], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + } + }, + "required": [ + "path" + ] + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_create_autoscale_configuration_policy", + "kind": "platform", + "path": "/v1/services/{uuid}/autoscale/policies", + "method": "POST", + "parameters": [ + { + "name": "uuid", + "group": "path", + "style": "form", + "explode": true + } + ], + "bodyContentType": "application/json", + "title": "Create Autoscale Configuration Policy", + "description": "Add a new autoscale policy to a service group by UUID.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "uuid" + ], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "$ref": "#/definitions/Autoscale.CreateAutoscaleConfigurationPolicyRequest" + } + }, + "required": [ + "path", + "body" + ], + "definitions": { + "Autoscale.CreateAutoscaleConfigurationPolicyRequest": { + "type": "object", + "required": [ + "name", + "type" + ], + "properties": { + "name": { + "type": "string", + "description": "The name of the policy." + }, + "type": { + "allOf": [ + { + "$ref": "#/definitions/Autoscale.AutoscalePolicy" + } + ], + "description": "The policy type to add to the autoscale configuration." + } + }, + "description": "The request message to create an autoscale configuration policy for a\nservice." + }, + "Autoscale.AutoscalePolicy": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "description": "The name of the policy." + }, + "enabled": { + "type": "boolean", + "description": "Whether the policy is enabled." + }, + "metric": { + "allOf": [ + { + "$ref": "#/definitions/Autoscale.StepPolicyMetric" + } + ], + "description": "Metric to use for the step policy (only for step policies)." + }, + "adjustment_type": { + "allOf": [ + { + "$ref": "#/definitions/Autoscale.AdjustmentType" + } + ], + "description": "The type of adjustment to be made in the step policy (only for step policies)." + }, + "steps": { + "type": "array", + "items": { + "$ref": "#/definitions/Autoscale.AutoscalePolicyStep" + }, + "description": "The steps for the step policy. Each step defines an adjustment value and\noptional bounds." + } + }, + "description": "AutoscalePolicy defines the autoscale policy for a service. Right now it\ncontains fields from both the `ondemand` and `step` policies. They are\nmarked both as optional, so only one of them should be set at a time. This\nis a current limitation of the API design." + }, + "Autoscale.StepPolicyMetric": { + "type": "string", + "enum": [ + "cpu" + ], + "description": "The step policy is a type of autoscaling policy that scales the number of\ninstances in a service by a fixed number of instances at each step. It uses\na metric to determine when to scale up or down." + }, + "Autoscale.AdjustmentType": { + "type": "string", + "enum": [ + "change", + "exact", + "percentage" + ], + "description": "AdjustmentType defines the type of adjustment to be made in an autoscaling\nstep policy." + }, + "Autoscale.AutoscalePolicyStep": { + "type": "object", + "required": [ + "adjustment" + ], + "properties": { + "adjustment": { + "type": "integer", + "description": "The adjustment value for the step." + }, + "lower_bound": { + "type": "integer", + "description": "Lower bound for the step." + }, + "upper_bound": { + "type": "integer", + "description": "Upper bound for the step." + } + }, + "description": "A single step in a step autoscaling policy." + } + } + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_get_autoscale_configuration_policies", + "kind": "platform", + "path": "/v1/services/{uuid}/autoscale/policies", + "method": "GET", + "parameters": [ + { + "name": "uuid", + "group": "path", + "style": "form", + "explode": true + } + ], + "bodyContentType": "application/json", + "title": "List Autoscale Configuration Policies", + "description": "List autoscale policies for a service group by UUID.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "uuid" + ], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "$ref": "#/definitions/Autoscale.GetAutoscaleConfigurationPolicyRequest" + } + }, + "required": [ + "path" + ], + "definitions": { + "Autoscale.GetAutoscaleConfigurationPolicyRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "description": "The Name of the policy to get." + } + }, + "description": "The request message to get an autoscale configuration policy by name." + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + { + "name": "unikraft_delete_autoscale_configuration_policies", + "kind": "platform", + "path": "/v1/services/{uuid}/autoscale/policies", + "method": "DELETE", + "parameters": [ + { + "name": "uuid", + "group": "path", + "style": "form", + "explode": true + } + ], + "bodyContentType": "application/json", + "title": "Delete Autoscale Configuration Policies", + "description": "Delete autoscale policies for a service group by UUID.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "uuid" + ], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "$ref": "#/definitions/Autoscale.DeletePolicyRequest" + } + }, + "required": [ + "path", + "body" + ], + "definitions": { + "Autoscale.DeletePolicyRequest": { + "type": "object", + "required": [ + "name" + ], + "properties": { + "name": { + "type": "string", + "description": "The name of the policy to delete." + } + }, + "description": "The request message to delete an autoscale configuration policy by name." + } + } + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_get_autoscale_configuration_policy_by_name", + "kind": "platform", + "path": "/v1/services/{uuid}/autoscale/policies/{name}", + "method": "GET", + "parameters": [ + { + "name": "uuid", + "group": "path", + "style": "form", + "explode": true + }, + { + "name": "name", + "group": "path", + "style": "form", + "explode": true + } + ], + "title": "Get Autoscale Configuration Policy by Name", + "description": "Get an autoscale policy by name for a service group by UUID.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + } + }, + "required": [ + "uuid", + "name" + ], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + } + }, + "required": [ + "path" + ] + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + { + "name": "unikraft_delete_autoscale_configuration_policy_by_name", + "kind": "platform", + "path": "/v1/services/{uuid}/autoscale/policies/{name}", + "method": "DELETE", + "parameters": [ + { + "name": "uuid", + "group": "path", + "style": "form", + "explode": true + }, + { + "name": "name", + "group": "path", + "style": "form", + "explode": true + } + ], + "title": "Delete Autoscale Configuration Policy by Name", + "description": "Delete an autoscale policy by name for a service group by UUID.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string" + } + }, + "required": [ + "uuid", + "name" + ], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + } + }, + "required": [ + "path" + ] + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_get_user", + "kind": "platform", + "path": "/v1/users/quotas", + "method": "GET", + "parameters": [], + "title": "Get Current User Quotas", + "description": "List quota usage and limits of your user account.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + } + }, + "required": [] + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + { + "name": "unikraft_get_user_by_uuid", + "kind": "platform", + "path": "/v1/users/{uuid}/quotas", + "method": "GET", + "parameters": [ + { + "name": "uuid", + "group": "path", + "style": "form", + "explode": true + } + ], + "title": "Get User Quotas by UUID", + "description": "List quota usage and limits of a user account by UUID.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "uuid" + ], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + } + }, + "required": [ + "path" + ] + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + { + "name": "unikraft_create_volume", + "kind": "platform", + "path": "/v1/volumes", + "method": "POST", + "parameters": [], + "bodyContentType": "application/json", + "title": "Create Volume", + "description": "Create a new volume.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "$ref": "#/definitions/Volumes.CreateVolumeRequest" + } + }, + "required": [ + "body" + ], + "definitions": { + "Volumes.CreateVolumeRequest": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "size_mb": { + "type": "integer", + "minimum": 0 + }, + "host_path": { + "type": "string" + }, + "template": { + "$ref": "#/definitions/Common.NameOrUUID" + }, + "quota_policy": { + "$ref": "#/definitions/Volumes.VolumeQuotaPolicy" + }, + "filesystem": { + "type": "string" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + }, + "uid": { + "type": "integer", + "minimum": 0 + }, + "gid": { + "type": "integer", + "minimum": 0 + }, + "access_mode": { + "$ref": "#/definitions/Volumes.VolumeAccessMode" + }, + "args": { + "type": "object", + "additionalProperties": { + "type": "string" + } + } + }, + "description": "The request message for creating a volume.", + "oneOf": [ + { + "required": [ + "size_mb" + ] + }, + { + "required": [ + "host_path" + ] + }, + { + "required": [ + "template" + ] + } + ] + }, + "Common.NameOrUUID": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "The UUID of the resource." + }, + "name": { + "type": "string", + "description": "The name of the resource." + } + }, + "description": "An identifier for a resource — either a name or a UUID, but not both.", + "oneOf": [ + { + "required": [ + "uuid" + ], + "not": { + "required": [ + "name" + ] + } + }, + { + "required": [ + "name" + ], + "not": { + "required": [ + "uuid" + ] + } + } + ] + }, + "Volumes.VolumeQuotaPolicy": { + "type": "string", + "enum": [ + "static", + "dynamic" + ], + "description": "Quota policy for a volume." + }, + "Volumes.VolumeAccessMode": { + "type": "string", + "enum": [ + "rwo", + "rox", + "rwx" + ], + "description": "VolumeAccessMode defines the access mode of a volume, which controls sharing\nbehavior and caching strategy." + } + } + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_get_volumes", + "kind": "platform", + "path": "/v1/volumes", + "method": "GET", + "parameters": [ + { + "name": "uuid", + "group": "query", + "style": "form", + "explode": true + }, + { + "name": "name", + "group": "query", + "style": "form", + "explode": true + }, + { + "name": "details", + "group": "query", + "style": "form", + "explode": false + }, + { + "name": "count", + "group": "query", + "style": "form", + "explode": false + }, + { + "name": "from", + "group": "query", + "style": "form", + "explode": false + }, + { + "name": "order", + "group": "query", + "style": "form", + "explode": false + }, + { + "name": "sortby", + "group": "query", + "style": "form", + "explode": false + }, + { + "name": "tags", + "group": "query", + "style": "form", + "explode": false + } + ], + "bodyContentType": "application/json", + "title": "List Volumes", + "description": "List volumes.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "query": { + "type": "object", + "properties": { + "uuid": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The UUID of the resource." + }, + "name": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The name of the resource." + }, + "details": { + "type": "boolean" + }, + "count": { + "type": "integer", + "minimum": 0 + }, + "from": { + "type": "string" + }, + "order": { + "$ref": "#/definitions/Common.PaginationOrder" + }, + "sortby": { + "$ref": "#/definitions/Common.PaginationSortBy" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "type": "array", + "items": { + "$ref": "#/definitions/Common.NameOrUUID" + } + } + }, + "required": [], + "definitions": { + "Common.PaginationOrder": { + "type": "string", + "enum": [ + "asc", + "desc" + ], + "description": "The sort order used by list endpoints." + }, + "Common.PaginationSortBy": { + "type": "string", + "enum": [ + "create_time" + ], + "description": "The sort field used by list endpoints." + }, + "Common.NameOrUUID": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "The UUID of the resource." + }, + "name": { + "type": "string", + "description": "The name of the resource." + } + }, + "description": "An identifier for a resource — either a name or a UUID, but not both.", + "oneOf": [ + { + "required": [ + "uuid" + ], + "not": { + "required": [ + "name" + ] + } + }, + { + "required": [ + "name" + ], + "not": { + "required": [ + "uuid" + ] + } + } + ] + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + { + "name": "unikraft_update_volumes", + "kind": "platform", + "path": "/v1/volumes", + "method": "PATCH", + "parameters": [], + "bodyContentType": "application/json", + "title": "Update Volumes", + "description": "Update volumes.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "type": "array", + "items": { + "$ref": "#/definitions/Volumes.UpdateVolumesRequestItem" + } + } + }, + "required": [ + "body" + ], + "definitions": { + "Volumes.UpdateVolumesRequestItem": { + "type": "object", + "required": [ + "prop", + "op" + ], + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "The UUID of the resource." + }, + "name": { + "type": "string", + "description": "The name of the resource." + }, + "id": { + "type": "string", + "description": "A client-provided identifier for tracking this operation in the response." + }, + "prop": { + "allOf": [ + { + "$ref": "#/definitions/Volumes.MutableVolumeProperty" + } + ], + "description": "The property to modify." + }, + "op": { + "allOf": [ + { + "$ref": "#/definitions/Volumes.MutableVolumeOperation" + } + ], + "description": "The operation to perform." + }, + "value": { + "description": "The value for the update operation. The type depends on the property and operation:\n- For \"image\": string\n- For \"args\": string or array of strings\n- For \"env\": object (for SET/ADD) or string/array of strings (for DEL)\n- For \"memory_mb\": integer\n- For \"vcpus\": integer\n- For \"scale_to_zero\": object with cooldown_time_ms, policy, and stateful fields\n- For \"tags\": array of strings\n- For \"delete_lock\": boolean\n- For \"schedules\": array of schedule objects (with name, when, action, and optional args fields).\nUse action \"exec\" together with args to execute a command at the scheduled time.\n- For \"autokill\": object with time_ms and num_requests fields\n- For \"hostname\": string (valid DNS label)\n- For \"roms\": array of ROM objects (with name and image fields) for SET/ADD, or array of ROM names for DEL\n- For \"dependencies\": array of instance identifiers (name or UUID)\n- For \"sched_priority\": SchedPriority enum value (\"normal\", \"medium\", \"high\", \"admin\")" + } + }, + "description": "A single update operation to be applied to a volume." + }, + "Volumes.MutableVolumeProperty": { + "type": "string", + "enum": [ + "size_mb", + "tags", + "quota_policy", + "delete_lock" + ], + "description": "Mutable volume properties." + }, + "Volumes.MutableVolumeOperation": { + "type": "string", + "enum": [ + "set", + "add", + "del" + ], + "description": "Mutable volume operations." + } + } + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_delete_volumes", + "kind": "platform", + "path": "/v1/volumes", + "method": "DELETE", + "parameters": [], + "bodyContentType": "application/json", + "title": "Delete Volumes", + "description": "Delete volumes by ID(s).", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "type": "array", + "items": { + "$ref": "#/definitions/Common.NameOrUUID" + } + } + }, + "required": [ + "body" + ], + "definitions": { + "Common.NameOrUUID": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "The UUID of the resource." + }, + "name": { + "type": "string", + "description": "The name of the resource." + } + }, + "description": "An identifier for a resource — either a name or a UUID, but not both.", + "oneOf": [ + { + "required": [ + "uuid" + ], + "not": { + "required": [ + "name" + ] + } + }, + { + "required": [ + "name" + ], + "not": { + "required": [ + "uuid" + ] + } + } + ] + } + } + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_attach_volumes", + "kind": "platform", + "path": "/v1/volumes/attach", + "method": "PUT", + "parameters": [], + "bodyContentType": "application/json", + "title": "Attach Volumes", + "description": "Attach volumes to instances.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "type": "array", + "items": { + "$ref": "#/definitions/Volumes.AttachVolumesRequestItem" + } + } + }, + "required": [ + "body" + ], + "definitions": { + "Volumes.AttachVolumesRequestItem": { + "type": "object", + "required": [ + "attach_to", + "at" + ], + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "The UUID of the resource." + }, + "name": { + "type": "string", + "description": "The name of the resource." + }, + "attach_to": { + "allOf": [ + { + "$ref": "#/definitions/Common.NameOrUUID" + } + ], + "description": "UUID or name of the instance to attach the volume to." + }, + "at": { + "type": "string", + "description": "Path of the mountpoint.\n\nThe path must be absolute, not contain `.` and `..` components, and not\ncontain colons (`:`). The path must point to an empty directory. If the\ndirectory does not exist, it is created." + }, + "readonly": { + "type": "boolean", + "description": "Whether the volume should be mounted read-only." + } + }, + "description": "A single request item to attach a volume to an instance." + }, + "Common.NameOrUUID": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "The UUID of the resource." + }, + "name": { + "type": "string", + "description": "The name of the resource." + } + }, + "description": "An identifier for a resource — either a name or a UUID, but not both.", + "oneOf": [ + { + "required": [ + "uuid" + ], + "not": { + "required": [ + "name" + ] + } + }, + { + "required": [ + "name" + ], + "not": { + "required": [ + "uuid" + ] + } + } + ] + } + } + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_clone_volumes", + "kind": "platform", + "path": "/v1/volumes/clone", + "method": "POST", + "parameters": [], + "bodyContentType": "application/json", + "title": "Clone Volumes", + "description": "Clone volumes.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "type": "array", + "items": { + "$ref": "#/definitions/Volumes.CloneVolumesRequestItem" + } + } + }, + "required": [ + "body" + ], + "definitions": { + "Volumes.CloneVolumesRequestItem": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "The UUID of the resource." + }, + "name": { + "type": "string", + "description": "The name of the resource." + }, + "vol_name": { + "type": "string", + "description": "The name of the new cloned volume. If not provided, a random name\nof the form `vol-X` is generated for you, where `X` is a 5 character\nlong random alphanumeric suffix." + }, + "quota_policy": { + "allOf": [ + { + "$ref": "#/definitions/Volumes.VolumeQuotaPolicy" + } + ], + "description": "The quota policy for the new cloned volume. If not provided, the quota\npolicy of the source volume is used." + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "A list of tags to assign to the new cloned volume." + }, + "access_mode": { + "$ref": "#/definitions/Volumes.VolumeAccessMode" + } + }, + "description": "A single request item to clone a volume." + }, + "Volumes.VolumeQuotaPolicy": { + "type": "string", + "enum": [ + "static", + "dynamic" + ], + "description": "Quota policy for a volume." + }, + "Volumes.VolumeAccessMode": { + "type": "string", + "enum": [ + "rwo", + "rox", + "rwx" + ], + "description": "VolumeAccessMode defines the access mode of a volume, which controls sharing\nbehavior and caching strategy." + } + } + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_detach_volumes", + "kind": "platform", + "path": "/v1/volumes/detach", + "method": "PUT", + "parameters": [], + "bodyContentType": "application/json", + "title": "Detach Volumes", + "description": "Detach volumes from instances.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "type": "array", + "items": { + "$ref": "#/definitions/Volumes.DetachVolumesRequestItem" + } + } + }, + "required": [ + "body" + ], + "definitions": { + "Volumes.DetachVolumesRequestItem": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "The UUID of the resource." + }, + "name": { + "type": "string", + "description": "The name of the resource." + }, + "from": { + "allOf": [ + { + "$ref": "#/definitions/Common.NameOrUUID" + } + ], + "description": "UUID or name of the instance to detach the volume from. If not specified,\nthe volume is detached from all instances." + } + }, + "description": "A single request item to detach a volume from an instance." + }, + "Common.NameOrUUID": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "The UUID of the resource." + }, + "name": { + "type": "string", + "description": "The name of the resource." + } + }, + "description": "An identifier for a resource — either a name or a UUID, but not both.", + "oneOf": [ + { + "required": [ + "uuid" + ], + "not": { + "required": [ + "name" + ] + } + }, + { + "required": [ + "name" + ], + "not": { + "required": [ + "uuid" + ] + } + } + ] + } + } + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_create_template_volume", + "kind": "platform", + "path": "/v1/volumes/templates", + "method": "POST", + "parameters": [], + "bodyContentType": "application/json", + "title": "Create Template Volume", + "description": "Create a template volume.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "type": "array", + "items": { + "$ref": "#/definitions/Common.NameOrUUID" + } + } + }, + "required": [ + "body" + ], + "definitions": { + "Common.NameOrUUID": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "The UUID of the resource." + }, + "name": { + "type": "string", + "description": "The name of the resource." + } + }, + "description": "An identifier for a resource — either a name or a UUID, but not both.", + "oneOf": [ + { + "required": [ + "uuid" + ], + "not": { + "required": [ + "name" + ] + } + }, + { + "required": [ + "name" + ], + "not": { + "required": [ + "uuid" + ] + } + } + ] + } + } + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_get_template_volumes", + "kind": "platform", + "path": "/v1/volumes/templates", + "method": "GET", + "parameters": [ + { + "name": "uuid", + "group": "query", + "style": "form", + "explode": true + }, + { + "name": "name", + "group": "query", + "style": "form", + "explode": true + }, + { + "name": "details", + "group": "query", + "style": "form", + "explode": false + }, + { + "name": "count", + "group": "query", + "style": "form", + "explode": false + }, + { + "name": "from", + "group": "query", + "style": "form", + "explode": false + }, + { + "name": "order", + "group": "query", + "style": "form", + "explode": false + }, + { + "name": "sortby", + "group": "query", + "style": "form", + "explode": false + }, + { + "name": "tags", + "group": "query", + "style": "form", + "explode": false + } + ], + "bodyContentType": "application/json", + "title": "List Template Volumes", + "description": "List template volumes.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "query": { + "type": "object", + "properties": { + "uuid": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The UUID of the resource." + }, + "name": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The name of the resource." + }, + "details": { + "type": "boolean" + }, + "count": { + "type": "integer", + "minimum": 0 + }, + "from": { + "type": "string" + }, + "order": { + "$ref": "#/definitions/Common.PaginationOrder" + }, + "sortby": { + "$ref": "#/definitions/Common.PaginationSortBy" + }, + "tags": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "required": [], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "type": "array", + "items": { + "$ref": "#/definitions/Common.NameOrUUID" + } + } + }, + "required": [], + "definitions": { + "Common.PaginationOrder": { + "type": "string", + "enum": [ + "asc", + "desc" + ], + "description": "The sort order used by list endpoints." + }, + "Common.PaginationSortBy": { + "type": "string", + "enum": [ + "create_time" + ], + "description": "The sort field used by list endpoints." + }, + "Common.NameOrUUID": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "The UUID of the resource." + }, + "name": { + "type": "string", + "description": "The name of the resource." + } + }, + "description": "An identifier for a resource — either a name or a UUID, but not both.", + "oneOf": [ + { + "required": [ + "uuid" + ], + "not": { + "required": [ + "name" + ] + } + }, + { + "required": [ + "name" + ], + "not": { + "required": [ + "uuid" + ] + } + } + ] + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + { + "name": "unikraft_update_template_volumes", + "kind": "platform", + "path": "/v1/volumes/templates", + "method": "PATCH", + "parameters": [], + "bodyContentType": "application/json", + "title": "Update Template Volumes", + "description": "Update template volumes.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "type": "array", + "items": { + "$ref": "#/definitions/Volumes.UpdateTemplateVolumesRequestItem" + } + } + }, + "required": [ + "body" + ], + "definitions": { + "Volumes.UpdateTemplateVolumesRequestItem": { + "type": "object", + "required": [ + "prop", + "op" + ], + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "The UUID of the resource." + }, + "name": { + "type": "string", + "description": "The name of the resource." + }, + "id": { + "type": "string", + "description": "A client-provided identifier for tracking this operation in the response." + }, + "prop": { + "allOf": [ + { + "$ref": "#/definitions/Volumes.MutableTemplateVolumeProperty" + } + ], + "description": "The property to modify." + }, + "op": { + "allOf": [ + { + "$ref": "#/definitions/Volumes.MutableTemplateVolumeOperation" + } + ], + "description": "The operation to perform." + }, + "value": { + "description": "The value for the update operation. The type depends on the property and operation:\n- For \"image\": string\n- For \"args\": string or array of strings\n- For \"env\": object (for SET/ADD) or string/array of strings (for DEL)\n- For \"memory_mb\": integer\n- For \"vcpus\": integer\n- For \"scale_to_zero\": object with cooldown_time_ms, policy, and stateful fields\n- For \"tags\": array of strings\n- For \"delete_lock\": boolean\n- For \"schedules\": array of schedule objects (with name, when, action, and optional args fields).\nUse action \"exec\" together with args to execute a command at the scheduled time.\n- For \"autokill\": object with time_ms and num_requests fields\n- For \"hostname\": string (valid DNS label)\n- For \"roms\": array of ROM objects (with name and image fields) for SET/ADD, or array of ROM names for DEL\n- For \"dependencies\": array of instance identifiers (name or UUID)\n- For \"sched_priority\": SchedPriority enum value (\"normal\", \"medium\", \"high\", \"admin\")" + } + }, + "description": "A single update operation to be applied to a template volume." + }, + "Volumes.MutableTemplateVolumeProperty": { + "type": "string", + "enum": [ + "tags", + "delete_lock" + ], + "description": "Mutable template volume properties." + }, + "Volumes.MutableTemplateVolumeOperation": { + "type": "string", + "enum": [ + "set", + "add", + "del" + ], + "description": "Mutable template volume operations." + } + } + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_delete_template_volumes", + "kind": "platform", + "path": "/v1/volumes/templates", + "method": "DELETE", + "parameters": [], + "bodyContentType": "application/json", + "title": "Delete Template Volumes", + "description": "Delete template volumes by ID(s).", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "type": "array", + "items": { + "$ref": "#/definitions/Common.NameOrUUID" + } + } + }, + "required": [ + "body" + ], + "definitions": { + "Common.NameOrUUID": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "The UUID of the resource." + }, + "name": { + "type": "string", + "description": "The name of the resource." + } + }, + "description": "An identifier for a resource — either a name or a UUID, but not both.", + "oneOf": [ + { + "required": [ + "uuid" + ], + "not": { + "required": [ + "name" + ] + } + }, + { + "required": [ + "name" + ], + "not": { + "required": [ + "uuid" + ] + } + } + ] + } + } + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_get_template_volume_by_uuid", + "kind": "platform", + "path": "/v1/volumes/templates/{uuid}", + "method": "GET", + "parameters": [ + { + "name": "uuid", + "group": "path", + "style": "form", + "explode": true + }, + { + "name": "details", + "group": "query", + "style": "form", + "explode": false + } + ], + "title": "Get Template Volume by UUID", + "description": "Get a template volume by UUID.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "uuid" + ], + "additionalProperties": false + }, + "query": { + "type": "object", + "properties": { + "details": { + "type": "boolean" + } + }, + "required": [], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + } + }, + "required": [ + "path" + ] + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + { + "name": "unikraft_update_template_volume_by_uuid", + "kind": "platform", + "path": "/v1/volumes/templates/{uuid}", + "method": "PATCH", + "parameters": [ + { + "name": "uuid", + "group": "path", + "style": "form", + "explode": true + } + ], + "bodyContentType": "application/json", + "title": "Update Template Volume by UUID", + "description": "Update a template volume by UUID.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "uuid" + ], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "$ref": "#/definitions/Volumes.UpdateTemplateVolumeByUUIDRequestBody" + } + }, + "required": [ + "path", + "body" + ], + "definitions": { + "Volumes.UpdateTemplateVolumeByUUIDRequestBody": { + "type": "object", + "required": [ + "prop", + "op" + ], + "properties": { + "id": { + "type": "string", + "description": "A client-provided identifier for tracking this operation in the response." + }, + "prop": { + "allOf": [ + { + "$ref": "#/definitions/Volumes.MutableTemplateVolumeProperty" + } + ], + "description": "The property to modify." + }, + "op": { + "allOf": [ + { + "$ref": "#/definitions/Volumes.MutableTemplateVolumeOperation" + } + ], + "description": "The operation to perform." + }, + "value": { + "description": "The value for the update operation. The type depends on the property and operation:\n- For \"image\": string\n- For \"args\": string or array of strings\n- For \"env\": object (for SET/ADD) or string/array of strings (for DEL)\n- For \"memory_mb\": integer\n- For \"vcpus\": integer\n- For \"scale_to_zero\": object with cooldown_time_ms, policy, and stateful fields\n- For \"tags\": array of strings\n- For \"delete_lock\": boolean\n- For \"schedules\": array of schedule objects (with name, when, action, and optional args fields).\nUse action \"exec\" together with args to execute a command at the scheduled time.\n- For \"autokill\": object with time_ms and num_requests fields\n- For \"hostname\": string (valid DNS label)\n- For \"roms\": array of ROM objects (with name and image fields) for SET/ADD, or array of ROM names for DEL\n- For \"dependencies\": array of instance identifiers (name or UUID)\n- For \"sched_priority\": SchedPriority enum value (\"normal\", \"medium\", \"high\", \"admin\")" + } + } + }, + "Volumes.MutableTemplateVolumeProperty": { + "type": "string", + "enum": [ + "tags", + "delete_lock" + ], + "description": "Mutable template volume properties." + }, + "Volumes.MutableTemplateVolumeOperation": { + "type": "string", + "enum": [ + "set", + "add", + "del" + ], + "description": "Mutable template volume operations." + } + } + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_delete_template_volume_by_uuid", + "kind": "platform", + "path": "/v1/volumes/templates/{uuid}", + "method": "DELETE", + "parameters": [ + { + "name": "uuid", + "group": "path", + "style": "form", + "explode": true + } + ], + "title": "Delete Template Volume by UUID", + "description": "Delete a template volume by UUID.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "uuid" + ], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + } + }, + "required": [ + "path" + ] + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_get_volume_by_uuid", + "kind": "platform", + "path": "/v1/volumes/{uuid}", + "method": "GET", + "parameters": [ + { + "name": "uuid", + "group": "path", + "style": "form", + "explode": true + }, + { + "name": "details", + "group": "query", + "style": "form", + "explode": false + } + ], + "title": "Get Volume by UUID", + "description": "Get a volume by UUID.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "uuid" + ], + "additionalProperties": false + }, + "query": { + "type": "object", + "properties": { + "details": { + "type": "boolean" + } + }, + "required": [], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + } + }, + "required": [ + "path" + ] + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + { + "name": "unikraft_update_volume_by_uuid", + "kind": "platform", + "path": "/v1/volumes/{uuid}", + "method": "PATCH", + "parameters": [ + { + "name": "uuid", + "group": "path", + "style": "form", + "explode": true + } + ], + "bodyContentType": "application/json", + "title": "Update Volume by UUID", + "description": "Update a volume by UUID.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "uuid" + ], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "$ref": "#/definitions/Volumes.UpdateVolumeByUUIDRequestBody" + } + }, + "required": [ + "path", + "body" + ], + "definitions": { + "Volumes.UpdateVolumeByUUIDRequestBody": { + "type": "object", + "required": [ + "prop", + "op" + ], + "properties": { + "id": { + "type": "string", + "description": "A client-provided identifier for tracking this operation in the response." + }, + "prop": { + "allOf": [ + { + "$ref": "#/definitions/Volumes.MutableVolumeProperty" + } + ], + "description": "The property to modify." + }, + "op": { + "allOf": [ + { + "$ref": "#/definitions/Volumes.MutableVolumeOperation" + } + ], + "description": "The operation to perform." + }, + "value": { + "description": "The value for the update operation. The type depends on the property and operation:\n- For \"image\": string\n- For \"args\": string or array of strings\n- For \"env\": object (for SET/ADD) or string/array of strings (for DEL)\n- For \"memory_mb\": integer\n- For \"vcpus\": integer\n- For \"scale_to_zero\": object with cooldown_time_ms, policy, and stateful fields\n- For \"tags\": array of strings\n- For \"delete_lock\": boolean\n- For \"schedules\": array of schedule objects (with name, when, action, and optional args fields).\nUse action \"exec\" together with args to execute a command at the scheduled time.\n- For \"autokill\": object with time_ms and num_requests fields\n- For \"hostname\": string (valid DNS label)\n- For \"roms\": array of ROM objects (with name and image fields) for SET/ADD, or array of ROM names for DEL\n- For \"dependencies\": array of instance identifiers (name or UUID)\n- For \"sched_priority\": SchedPriority enum value (\"normal\", \"medium\", \"high\", \"admin\")" + } + } + }, + "Volumes.MutableVolumeProperty": { + "type": "string", + "enum": [ + "size_mb", + "tags", + "quota_policy", + "delete_lock" + ], + "description": "Mutable volume properties." + }, + "Volumes.MutableVolumeOperation": { + "type": "string", + "enum": [ + "set", + "add", + "del" + ], + "description": "Mutable volume operations." + } + } + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_delete_volume_by_uuid", + "kind": "platform", + "path": "/v1/volumes/{uuid}", + "method": "DELETE", + "parameters": [ + { + "name": "uuid", + "group": "path", + "style": "form", + "explode": true + } + ], + "title": "Delete Volume by UUID", + "description": "Delete a volume by UUID.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "uuid" + ], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + } + }, + "required": [ + "path" + ] + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_attach_volume_by_uuid", + "kind": "platform", + "path": "/v1/volumes/{uuid}/attach", + "method": "PUT", + "parameters": [ + { + "name": "uuid", + "group": "path", + "style": "form", + "explode": true + } + ], + "bodyContentType": "application/json", + "title": "Attach Volume by UUID", + "description": "Attach a volume by UUID.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "uuid" + ], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "$ref": "#/definitions/Volumes.AttachVolumeByUUIDRequestBody" + } + }, + "required": [ + "path", + "body" + ], + "definitions": { + "Volumes.AttachVolumeByUUIDRequestBody": { + "type": "object", + "required": [ + "attach_to", + "at" + ], + "properties": { + "attach_to": { + "allOf": [ + { + "$ref": "#/definitions/Common.NameOrUUID" + } + ], + "description": "UUID or name of the instance to attach the volume to." + }, + "at": { + "type": "string", + "description": "Path of the mountpoint.\n\nThe path must be absolute, not contain `.` and `..` components, and not\ncontain colons (`:`). The path must point to an empty directory. If the\ndirectory does not exist, it is created." + }, + "readonly": { + "type": "boolean", + "description": "Whether the volume should be mounted read-only." + } + } + }, + "Common.NameOrUUID": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "The UUID of the resource." + }, + "name": { + "type": "string", + "description": "The name of the resource." + } + }, + "description": "An identifier for a resource — either a name or a UUID, but not both.", + "oneOf": [ + { + "required": [ + "uuid" + ], + "not": { + "required": [ + "name" + ] + } + }, + { + "required": [ + "name" + ], + "not": { + "required": [ + "uuid" + ] + } + } + ] + } + } + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_clone_volume_by_uuid", + "kind": "platform", + "path": "/v1/volumes/{uuid}/clone", + "method": "POST", + "parameters": [ + { + "name": "uuid", + "group": "path", + "style": "form", + "explode": true + } + ], + "bodyContentType": "application/json", + "title": "Clone Volume by UUID", + "description": "Clone a volume by UUID.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "uuid" + ], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "$ref": "#/definitions/Volumes.CloneVolumeByUUIDRequestBody" + } + }, + "required": [ + "path", + "body" + ], + "definitions": { + "Volumes.CloneVolumeByUUIDRequestBody": { + "type": "object", + "properties": { + "vol_name": { + "type": "string", + "description": "The name of the new cloned volume. If not provided, a random name\nof the form `vol-X` is generated for you, where `X` is a 5 character\nlong random alphanumeric suffix." + }, + "quota_policy": { + "allOf": [ + { + "$ref": "#/definitions/Volumes.VolumeQuotaPolicy" + } + ], + "description": "The quota policy for the new cloned volume. If not provided, the quota\npolicy of the source volume is used." + }, + "tags": { + "type": "array", + "items": { + "type": "string" + }, + "description": "A list of tags to assign to the new cloned volume." + }, + "access_mode": { + "$ref": "#/definitions/Volumes.VolumeAccessMode" + } + } + }, + "Volumes.VolumeQuotaPolicy": { + "type": "string", + "enum": [ + "static", + "dynamic" + ], + "description": "Quota policy for a volume." + }, + "Volumes.VolumeAccessMode": { + "type": "string", + "enum": [ + "rwo", + "rox", + "rwx" + ], + "description": "VolumeAccessMode defines the access mode of a volume, which controls sharing\nbehavior and caching strategy." + } + } + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_detach_volume_by_uuid", + "kind": "platform", + "path": "/v1/volumes/{uuid}/detach", + "method": "PUT", + "parameters": [ + { + "name": "uuid", + "group": "path", + "style": "form", + "explode": true + } + ], + "bodyContentType": "application/json", + "title": "Detach Volume by UUID", + "description": "Detach a volume by UUID.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "uuid" + ], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "body": { + "$ref": "#/definitions/Volumes.DetachVolumeByUUIDRequestBody" + } + }, + "required": [ + "path", + "body" + ], + "definitions": { + "Volumes.DetachVolumeByUUIDRequestBody": { + "type": "object", + "properties": { + "from": { + "allOf": [ + { + "$ref": "#/definitions/Common.NameOrUUID" + } + ], + "description": "UUID or name of the instance to detach the volume from. If not specified,\nthe volume is detached from all instances." + } + } + }, + "Common.NameOrUUID": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid", + "description": "The UUID of the resource." + }, + "name": { + "type": "string", + "description": "The name of the resource." + } + }, + "description": "An identifier for a resource — either a name or a UUID, but not both.", + "oneOf": [ + { + "required": [ + "uuid" + ], + "not": { + "required": [ + "name" + ] + } + }, + { + "required": [ + "name" + ], + "not": { + "required": [ + "uuid" + ] + } + } + ] + } + } + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_sandbox_list_commands", + "kind": "sandbox", + "path": "/commands", + "method": "GET", + "parameters": [], + "title": "List Commands", + "description": "List the UUIDs of all commands, in the order they were started.\n\nThis operation is also the plugin's readiness probe: it takes no\nparameters, changes nothing, and answers on an empty sandbox, so a client\ncan call it to establish that the plugin is serving before it sends any\nreal work. Hence `x-unikraft-plugin-readiness`, which the SDK generators\nread to render their readiness helpers against this operation.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "instance": { + "type": "string", + "minLength": 1, + "maxLength": 200, + "description": "Name or UUID of the instance running the sandbox plugin." + } + }, + "required": [ + "instance" + ] + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + { + "name": "unikraft_sandbox_run_command", + "kind": "sandbox", + "path": "/commands", + "method": "POST", + "parameters": [], + "bodyContentType": "application/json", + "title": "Start Command", + "description": "Start a command in the sandbox. `500` is returned when the command could\nnot be spawned.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "instance": { + "type": "string", + "minLength": 1, + "maxLength": 200, + "description": "Name or UUID of the instance running the sandbox plugin." + }, + "body": { + "$ref": "#/definitions/RunCommandRequest" + } + }, + "required": [ + "instance", + "body" + ], + "definitions": { + "RunCommandRequest": { + "type": "object", + "required": [ + "cmd" + ], + "properties": { + "cmd": { + "anyOf": [ + { + "$ref": "#/definitions/CommandLineShell" + }, + { + "$ref": "#/definitions/CommandLineArgs" + } + ], + "description": "The command to run: a command line interpreted by the shell, or an argument\nvector executed directly. A bare program name in a vector is looked up in\nthe `PATH` of `env`, falling back to the sandbox's own `PATH`. `400` is\nreturned if the vector is empty or the program cannot be resolved." + }, + "cwd": { + "type": "string", + "description": "(OPTIONAL) Current working directory" + }, + "env": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "description": "(OPTIONAL) Environment variables" + } + }, + "description": "The request to start a command in the sandbox." + }, + "CommandLineShell": { + "type": "string", + "description": "The command line, interpreted by the shell." + }, + "CommandLineArgs": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The command as an argument vector, executed directly without a shell." + } + } + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_sandbox_get_command_by_uuid", + "kind": "sandbox", + "path": "/commands/{uuid}", + "method": "GET", + "parameters": [ + { + "name": "uuid", + "group": "path", + "style": "form", + "explode": true + } + ], + "title": "Get Command by UUID", + "description": "Inspect a command by UUID. `404` is returned, with an empty body, when no\nsuch command exists.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "uuid" + ], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "instance": { + "type": "string", + "minLength": 1, + "maxLength": 200, + "description": "Name or UUID of the instance running the sandbox plugin." + } + }, + "required": [ + "path", + "instance" + ] + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + { + "name": "unikraft_sandbox_delete_command_by_uuid", + "kind": "sandbox", + "path": "/commands/{uuid}", + "method": "DELETE", + "parameters": [ + { + "name": "uuid", + "group": "path", + "style": "form", + "explode": true + } + ], + "title": "Delete Command by UUID", + "description": "Forget a command by UUID. `404` is returned, with an empty body, when no\nsuch command exists, and `409` when the command has not finished\nexecuting: commands which are still running cannot be forgotten.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "uuid" + ], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "instance": { + "type": "string", + "minLength": 1, + "maxLength": 200, + "description": "Name or UUID of the instance running the sandbox plugin." + } + }, + "required": [ + "path", + "instance" + ] + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_sandbox_delete_command_logs_by_uuid", + "kind": "sandbox", + "path": "/commands/{uuid}/logs", + "method": "DELETE", + "parameters": [ + { + "name": "uuid", + "group": "path", + "style": "form", + "explode": true + } + ], + "title": "Delete Command Logs", + "description": "Forget the output logs of an existing command, freeing up memory.\nThe stdin/out/err pipes of running commands will be closed. Idempotent.\n`404` is returned, with an empty body, when no such command exists.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "uuid" + ], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "instance": { + "type": "string", + "minLength": 1, + "maxLength": 200, + "description": "Name or UUID of the instance running the sandbox plugin." + } + }, + "required": [ + "path", + "instance" + ] + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_sandbox_get_command_logs", + "kind": "sandbox", + "path": "/commands/{uuid}/logs", + "method": "GET", + "parameters": [ + { + "name": "uuid", + "group": "path", + "style": "form", + "explode": true + } + ], + "bodyContentType": "application/json", + "title": "Get Command Logs", + "description": "Fetch the collected output of a command. Both streams are base64-encoded;\nuse the raw log endpoint for binary output. `404` is returned, with an\nempty body, when no such command exists. `410` is returned, with an empty body,\nif the command's logs have been deleted.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "uuid" + ], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "instance": { + "type": "string", + "minLength": 1, + "maxLength": 200, + "description": "Name or UUID of the instance running the sandbox plugin." + }, + "body": { + "$ref": "#/definitions/CommandLogsRequest" + } + }, + "required": [ + "path", + "instance" + ], + "definitions": { + "CommandLogsRequest": { + "type": "object", + "required": [ + "stdout", + "stderr" + ], + "properties": { + "stdout": { + "allOf": [ + { + "$ref": "#/definitions/CommandLogsRange" + } + ], + "description": "The part of the standard output stream to return." + }, + "stderr": { + "allOf": [ + { + "$ref": "#/definitions/CommandLogsRange" + } + ], + "description": "The part of the standard error stream to return." + } + }, + "description": "The (optional) request body limiting which parts of the output streams are\nreturned. When omitted, both streams are returned in their entirety." + }, + "CommandLogsRange": { + "type": "object", + "required": [ + "offset" + ], + "properties": { + "offset": { + "type": "integer", + "description": "The first byte of the stream to return.", + "default": 0, + "minimum": 0 + }, + "limit": { + "type": "integer", + "description": "The maximum number of bytes to return. When omitted, everything from\n`offset` to the end of the stream is returned.", + "minimum": 0 + } + }, + "description": "The byte range of a single output stream to return." + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + { + "name": "unikraft_sandbox_get_raw_command_log", + "kind": "sandbox", + "path": "/commands/{uuid}/logs/raw/{stream}", + "method": "GET", + "parameters": [ + { + "name": "uuid", + "group": "path", + "style": "form", + "explode": true + }, + { + "name": "stream", + "group": "path", + "style": "form", + "explode": true + }, + { + "name": "Range", + "group": "headers", + "style": "form", + "explode": true + } + ], + "title": "Get Raw Command Log", + "description": "Fetch an output stream of a command as raw bytes. A single contiguous\n`Range` header is honoured, as described in RFC 9110.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + }, + "stream": { + "$ref": "#/definitions/CommandLogStream" + } + }, + "required": [ + "uuid", + "stream" + ], + "additionalProperties": false + }, + "headers": { + "type": "object", + "properties": { + "Range": { + "type": "string", + "description": "A single contiguous byte range, e.g. `bytes=0-1023`." + } + }, + "required": [], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "instance": { + "type": "string", + "minLength": 1, + "maxLength": 200, + "description": "Name or UUID of the instance running the sandbox plugin." + } + }, + "required": [ + "path", + "instance" + ], + "definitions": { + "CommandLogStream": { + "type": "string", + "enum": [ + "stdout", + "stderr" + ], + "description": "The output stream to read raw bytes from." + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + { + "name": "unikraft_sandbox_signal_command", + "kind": "sandbox", + "path": "/commands/{uuid}/signal", + "method": "POST", + "parameters": [ + { + "name": "uuid", + "group": "path", + "style": "form", + "explode": true + } + ], + "bodyContentType": "application/json", + "title": "Signal Command", + "description": "Send a signal to a command. `404` is returned, with an empty body, when no\nsuch command exists, and `400` when the signal is not a known signal name.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "uuid" + ], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "instance": { + "type": "string", + "minLength": 1, + "maxLength": 200, + "description": "Name or UUID of the instance running the sandbox plugin." + }, + "body": { + "$ref": "#/definitions/CommandSignalRequest" + } + }, + "required": [ + "path", + "instance", + "body" + ], + "definitions": { + "CommandSignalRequest": { + "type": "object", + "required": [ + "signal" + ], + "properties": { + "signal": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + } + ], + "description": "The signal to send, either as a number or as a name, with or without the\n`SIG` prefix, e.g. `15`, `TERM` or `SIGTERM`." + } + }, + "description": "The request to send a signal to a command." + } + } + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_sandbox_write_command_stdin", + "kind": "sandbox", + "path": "/commands/{uuid}/stdin", + "method": "POST", + "parameters": [ + { + "name": "uuid", + "group": "path", + "style": "form", + "explode": true + } + ], + "bodyContentType": "application/json", + "title": "Write Command Stdin", + "description": "Feed data into the standard input of a command. `404` is returned, with an\nempty body, when no such command exists, `410` if the command's logs have been\ndeleted, and `400` when `data` is not valid base64.\nFeeding data into a command whose standard input has been closed is an error.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "uuid" + ], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "instance": { + "type": "string", + "minLength": 1, + "maxLength": 200, + "description": "Name or UUID of the instance running the sandbox plugin." + }, + "body": { + "$ref": "#/definitions/CommandStdinRequest" + } + }, + "required": [ + "path", + "instance", + "body" + ], + "definitions": { + "CommandStdinRequest": { + "type": "object", + "required": [ + "data" + ], + "properties": { + "data": { + "type": "string", + "description": "The bytes to feed into standard input, base64-encoded." + }, + "eof": { + "type": "boolean", + "description": "Whether to close standard input after injecting `data`." + } + }, + "description": "The request to feed data into a command's standard input." + } + } + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": true, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_sandbox_wait_for_command", + "kind": "sandbox", + "path": "/commands/{uuid}/wait", + "method": "POST", + "parameters": [ + { + "name": "uuid", + "group": "path", + "style": "form", + "explode": true + } + ], + "title": "Wait for Command", + "description": "Wait for a command to finish, without a timeout. `404` is returned, with an\nempty body, when no such command exists.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "uuid" + ], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "instance": { + "type": "string", + "minLength": 1, + "maxLength": 200, + "description": "Name or UUID of the instance running the sandbox plugin." + } + }, + "required": [ + "path", + "instance" + ] + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + { + "name": "unikraft_sandbox_wait_for_command_with_timeout", + "kind": "sandbox", + "path": "/commands/{uuid}/wait_timeout", + "method": "POST", + "parameters": [ + { + "name": "uuid", + "group": "path", + "style": "form", + "explode": true + } + ], + "bodyContentType": "application/json", + "title": "Wait for Command with Timeout", + "description": "Wait for a command to finish, up to a timeout. Commands which are still\nrunning when the timeout elapses return `408`. `404` is returned, with an\nempty body, when no such command exists, and `400` when the timeout is not\na representable duration.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "object", + "properties": { + "uuid": { + "type": "string", + "format": "uuid" + } + }, + "required": [ + "uuid" + ], + "additionalProperties": false + }, + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "instance": { + "type": "string", + "minLength": 1, + "maxLength": 200, + "description": "Name or UUID of the instance running the sandbox plugin." + }, + "body": { + "$ref": "#/definitions/CommandWaitTimeoutRequest" + } + }, + "required": [ + "path", + "instance", + "body" + ], + "definitions": { + "CommandWaitTimeoutRequest": { + "type": "object", + "required": [ + "timeout_s" + ], + "properties": { + "timeout_s": { + "type": "number", + "description": "The maximum time to wait, in seconds. Decimals are accepted." + } + }, + "description": "The request to wait for a command to finish, up to a timeout." + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + { + "name": "unikraft_sandbox_create_directory", + "kind": "sandbox", + "path": "/fs/mkdir", + "method": "POST", + "parameters": [], + "bodyContentType": "application/json", + "title": "Create Directory", + "description": "Create a directory in the sandbox filesystem. `500` is returned when the\ndirectory could not be created.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "instance": { + "type": "string", + "minLength": 1, + "maxLength": 200, + "description": "Name or UUID of the instance running the sandbox plugin." + }, + "body": { + "$ref": "#/definitions/MkdirRequest" + } + }, + "required": [ + "instance", + "body" + ], + "definitions": { + "MkdirRequest": { + "type": "object", + "required": [ + "path", + "parents" + ], + "properties": { + "path": { + "type": "string", + "description": "The absolute path of the directory to create." + }, + "parents": { + "type": "boolean", + "description": "Whether to create missing parent directories." + } + }, + "description": "The request to create a directory in the sandbox filesystem." + } + } + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_sandbox_read_file", + "kind": "sandbox", + "path": "/fs/read", + "method": "POST", + "parameters": [], + "bodyContentType": "application/json", + "title": "Read File", + "description": "Read the contents of a file, base64-encoded. `500` is returned when the\nfile could not be read.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "instance": { + "type": "string", + "minLength": 1, + "maxLength": 200, + "description": "Name or UUID of the instance running the sandbox plugin." + }, + "body": { + "$ref": "#/definitions/ReadFileRequest" + } + }, + "required": [ + "instance", + "body" + ], + "definitions": { + "ReadFileRequest": { + "type": "object", + "required": [ + "path" + ], + "properties": { + "path": { + "type": "string", + "description": "The absolute path of the file to read." + } + }, + "description": "The request to read a file from the sandbox filesystem." + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + { + "name": "unikraft_sandbox_read_raw_file", + "kind": "sandbox", + "path": "/fs/read_raw", + "method": "POST", + "parameters": [], + "bodyContentType": "application/json", + "title": "Read Raw File", + "description": "Download the contents of a file verbatim.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "instance": { + "type": "string", + "minLength": 1, + "maxLength": 200, + "description": "Name or UUID of the instance running the sandbox plugin." + }, + "body": { + "$ref": "#/definitions/ReadFileRequest" + } + }, + "required": [ + "instance", + "body" + ], + "definitions": { + "ReadFileRequest": { + "type": "object", + "required": [ + "path" + ], + "properties": { + "path": { + "type": "string", + "description": "The absolute path of the file to read." + } + }, + "description": "The request to read a file from the sandbox filesystem." + } + } + }, + "annotations": { + "readOnlyHint": true, + "destructiveHint": false, + "idempotentHint": true, + "openWorldHint": true + } + }, + { + "name": "unikraft_sandbox_upload_file", + "kind": "sandbox", + "path": "/fs/upload", + "method": "POST", + "parameters": [], + "bodyContentType": "application/json", + "title": "Upload File", + "description": "Upload a file to the sandbox at a given path. A new file will be created at\n- if `path` ends with '/' or exists and is a directory: path/filename\n- else: path\n`400` is returned when `encoding` is not a known encoding or `data` is not\nvalid base64, and `500` when the file could not be created.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "instance": { + "type": "string", + "minLength": 1, + "maxLength": 200, + "description": "Name or UUID of the instance running the sandbox plugin." + }, + "body": { + "$ref": "#/definitions/UploadFileRequest" + } + }, + "required": [ + "instance", + "body" + ], + "definitions": { + "UploadFileRequest": { + "type": "object", + "required": [ + "path", + "filename", + "parents", + "encoding", + "data" + ], + "properties": { + "path": { + "type": "string", + "description": "Path where to upload the file." + }, + "filename": { + "type": "string", + "description": "Name of uploaded file; will be used if `path` is a directory." + }, + "parents": { + "type": "boolean", + "description": "Whether to create parent directories of `path`." + }, + "encoding": { + "allOf": [ + { + "$ref": "#/definitions/FileEncoding" + } + ], + "description": "The encoding of `data`." + }, + "data": { + "type": "string", + "description": "The data to write, in the given encoding" + } + }, + "description": "The request to upload a file to the sandbox filesystem." + }, + "FileEncoding": { + "type": "string", + "enum": [ + "utf-8", + "base64" + ], + "description": "The encoding of the data written to a file." + } + } + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": true + } + }, + { + "name": "unikraft_sandbox_write_file", + "kind": "sandbox", + "path": "/fs/write", + "method": "POST", + "parameters": [], + "bodyContentType": "application/json", + "title": "Write File", + "description": "Write data to a file, creating it if it does not exist. `400` is returned\nwhen `encoding` is not a known encoding or `data` is not valid base64, and\n`500` when the file could not be written.", + "inputSchema": { + "type": "object", + "additionalProperties": false, + "properties": { + "metro": { + "type": "string", + "enum": [ + "dal", + "fra", + "sfo", + "sin", + "was" + ], + "description": "Unikraft Cloud metro. Defaults to the connection's default metro." + }, + "instance": { + "type": "string", + "minLength": 1, + "maxLength": 200, + "description": "Name or UUID of the instance running the sandbox plugin." + }, + "body": { + "$ref": "#/definitions/WriteFileRequest" + } + }, + "required": [ + "instance", + "body" + ], + "definitions": { + "WriteFileRequest": { + "type": "object", + "required": [ + "path", + "append", + "encoding", + "data" + ], + "properties": { + "path": { + "type": "string", + "description": "The absolute path of the file to write. It is created if missing." + }, + "append": { + "type": "boolean", + "description": "Whether to append to the file instead of truncating it." + }, + "encoding": { + "allOf": [ + { + "$ref": "#/definitions/FileEncoding" + } + ], + "description": "The encoding of `data`." + }, + "data": { + "type": "string", + "description": "The data to write, in the given encoding." + } + }, + "description": "The request to write a file in the sandbox filesystem." + }, + "FileEncoding": { + "type": "string", + "enum": [ + "utf-8", + "base64" + ], + "description": "The encoding of the data written to a file." + } + } + }, + "annotations": { + "readOnlyHint": false, + "destructiveHint": false, + "idempotentHint": false, + "openWorldHint": true + } + } + ], + "streaming": [ + { + "name": "unikraft_subscribe_audit_events", + "kind": "platform", + "path": "/v1/audit", + "method": "GET", + "transport": "server-sent-events" + } + ] +} diff --git a/server/src/services/daytona.ts b/server/src/services/daytona.ts index 81529e5b867..76a78b63c0c 100644 --- a/server/src/services/daytona.ts +++ b/server/src/services/daytona.ts @@ -1,62 +1,27 @@ -import Ajv, { type ValidateFunction } from "ajv"; -import addFormats from "ajv-formats"; import { z } from "zod"; -import { HttpError } from "../errors.js"; -import { redactSensitiveText } from "../redaction.js"; -import { descriptorHash, stableHash } from "./tool-catalog-hash.js"; +import { NATIVE_REST_MAX_BYTES as MAX_BYTES, nativeCatalog, nativeRestFailure as failure, nativeRestPathValue, nativeRestRecord as record, + nativeRestSender, type NativeCatalogEntry, type NativeRestRequest } from "./native-rest-client.js"; import catalog from "./daytona/operations.generated.json" with { type: "json" }; export const DAYTONA_API_URL = "https://app.daytona.io/api"; // Preserve the saved method identity when upgrading the earlier small catalog. export const DAYTONA_METHOD = "workspace-diagnostics"; -const MAX_BYTES = 8 * 1024 * 1024; -const record = (value: unknown): Record => - value && typeof value === "object" && !Array.isArray(value) ? value as Record : {}; -const failure = (status: number, code: string, message: string) => new HttpError(status, message, { code }); -export type DaytonaRequest = (url: string, init: RequestInit) => Promise; +export type DaytonaRequest = NativeRestRequest; type Operation = typeof catalog.tools[number]; -const operations = new Map(catalog.tools.map(operation => [operation.name, operation])); -const validators = new Map(); -// Match the interop used by the existing plugin configuration validator. -const AjvCtor = (Ajv as any).default ?? Ajv; -const ajv = new AjvCtor({ strict: false, allErrors: true, validateFormats: true }); -((addFormats as any).default ?? addFormats)(ajv); +const contract = nativeCatalog(catalog.tools); -export const DAYTONA_TOOLS = catalog.tools.map(({ name, title, description, inputSchema, annotations }) => ({ - name, title, description, inputSchema: inputSchema as Record, annotations, -})); - -export function isDaytonaToolName(name: string): boolean { - return operations.has(name); -} - -const currentContracts = new Map(DAYTONA_TOOLS.map(tool => [tool.name, { - versionHash: descriptorHash(tool, daytonaRisk(tool.name)), schemaHash: stableHash(tool.inputSchema), riskLevel: daytonaRisk(tool.name), -}])); - -export function isCurrentDaytonaCatalogEntry(entry: { toolName: string; versionHash: string; schemaHash: string | null; riskLevel: string }): boolean { - const current = currentContracts.get(entry.toolName); - return Boolean(current && current.versionHash === entry.versionHash && current.schemaHash === entry.schemaHash && current.riskLevel === entry.riskLevel); -} +export const DAYTONA_TOOLS = contract.tools; +export const isDaytonaToolName = contract.isToolName; +export const daytonaRisk = contract.risk; +export const isCurrentDaytonaCatalogEntry = (entry: NativeCatalogEntry) => contract.isCurrentEntry(entry); export function isDaytonaConnection(connection: { transport: string; config?: Record }) { return connection.transport === "rest_api" && connection.config?.sourceTemplateKey === "daytona" && connection.config.connectionMethodKey === DAYTONA_METHOD && connection.config.url === DAYTONA_API_URL; } -export function daytonaRisk(name: string): "read" | "write" | "destructive" { - const operation = operations.get(name); - if (!operation || operation.annotations.destructiveHint) return "destructive"; - return operation.annotations.readOnlyHint ? "read" : "write"; -} - function pathValue(value: unknown) { - const text = String(value); - if (!text || text.length > 8192 || /[\u0000-\u001f\\]/.test(text) - || text.split("/").some(segment => segment === "." || segment === "..")) { - throw failure(422, "daytona_arguments_invalid", "Invalid Daytona path argument."); - } - return encodeURIComponent(text); + return nativeRestPathValue(value, () => failure(422, "daytona_arguments_invalid", "Invalid Daytona path argument.")); } function decodeFile(value: unknown) { @@ -77,78 +42,11 @@ export function daytonaClient(headers: Record, request: DaytonaR if (!authorization || !/^Bearer [^\r\n]+$/.test(authorization)) { throw failure(422, "daytona_credential_missing", "Reconnect Daytona to restore its API key."); } - const secret = authorization.slice(7); - const expired = () => failure(504, "daytona_deadline_exceeded", "The Daytona action deadline expired. An operation already sent may still be running; inspect it before retrying."); - // Bound the caller even if a transport or body reader is slow to acknowledge - // cancellation. A late prerequisite result never advances to the next call. - async function withinDeadline(work: () => Promise): Promise { - if (signal.aborted || Date.now() >= deadlineAt) throw expired(); - let abort!: () => void; - try { - return await Promise.race([ - Promise.resolve().then(() => { if (signal.aborted || Date.now() >= deadlineAt) throw expired(); return work(); }), - new Promise((_resolve, reject) => { - abort = () => reject(expired()); - signal.addEventListener("abort", abort, { once: true }); - }), - ]); - } finally { signal.removeEventListener("abort", abort); } - } - function sanitize(value: unknown): unknown { - if (typeof value === "string") return redactSensitiveText(value.split(secret).join("[REDACTED]")); - if (Array.isArray(value)) return value.map(sanitize); - if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, sanitize(item)])); - return value; - } - - async function send(url: string, init: RequestInit, organizationId?: string): Promise { - const projected = new Headers(init.headers); - projected.set("Authorization", authorization!); - projected.set("X-Daytona-Source", "paperclip-connector"); - if (organizationId) projected.set("X-Daytona-Organization-ID", organizationId); - let response: Response; - try { - response = await withinDeadline(() => request(url, { ...init, redirect: "error", headers: Object.fromEntries(projected), signal })); - } catch { - if (signal.aborted || Date.now() >= deadlineAt) throw expired(); - throw failure(502, "daytona_outcome_unknown", init.method === "GET" - ? "Daytona could not complete the request." - : "Daytona did not return a result. The operation may have started; inspect its state before retrying."); - } - if (!response.ok) { - void response.body?.cancel().catch(() => {}); - const status = [400, 401, 403, 404, 409, 422, 429].includes(response.status) ? response.status : 502; - throw failure(status, "daytona_request_failed", status === 401 || status === 403 - ? "Daytona denied access. Check this key's organization and permissions. Some account operations require provider user authentication." - : status === 404 ? "Daytona could not find this resource." : "Daytona could not complete this operation."); - } - if (response.status === 204 || !response.body) return null; - const reader = response.body.getReader(); - const chunks: Uint8Array[] = []; - let length = 0; - try { - for (;;) { - const part = await withinDeadline(() => reader.read()); - if (part.done) break; - length += part.value.byteLength; - if (length > MAX_BYTES) throw failure(502, "daytona_output_limit", "Daytona's response exceeded the limit. Request a smaller page or file range."); - chunks.push(part.value); - } - } catch (error) { - if (error instanceof HttpError) throw error; - throw failure(502, "daytona_outcome_unknown", "Daytona's response was interrupted. Inspect the operation before retrying."); - } finally { void reader.cancel().catch(() => {}); } - const bytes = Buffer.concat(chunks); - if (!bytes.length) return null; - const contentType = response.headers.get("Content-Type") ?? "application/octet-stream"; - if (contentType.includes("json")) { - try { return sanitize(JSON.parse(bytes.toString("utf8"))); } - catch { throw failure(502, "daytona_invalid_response", "Daytona returned an invalid response."); } - } - if (contentType.startsWith("text/")) return sanitize(bytes.toString("utf8")); - if (bytes.includes(Buffer.from(secret))) throw failure(403, "daytona_credential_export_denied", "The response contains connection credential material."); - return { contentType, contentBase64: bytes.toString("base64"), byteLength: bytes.length }; - } + const { send: sendRequest, withinDeadline } = nativeRestSender({ provider: "Daytona", code: "daytona", authorization, request, signal, deadlineAt, + deniedMessage: "Daytona denied access. Check this key's organization and permissions. Some account operations require provider user authentication.", + fixedHeaders: { "X-Daytona-Source": "paperclip-connector" } }); + const send = (url: string, init: RequestInit, organizationId?: string) => + sendRequest(url, init, organizationId ? { "X-Daytona-Organization-ID": organizationId } : {}); async function identity() { const value = record(await send(`${DAYTONA_API_URL}/api-keys/current`, { method: "GET" })); @@ -248,11 +146,9 @@ export async function executeDaytonaTool(input: { if (!input.binding.agentId || input.connection.companyId !== input.binding.companyId) { throw failure(404, "daytona_connection_not_found", "Connection not found."); } - const operation = operations.get(input.toolName); + const operation = contract.operation(input.toolName); if (!operation) throw failure(404, "daytona_tool_not_found", "Daytona action not found."); - let validate = validators.get(operation.name); - if (!validate) { validate = ajv.compile(operation.inputSchema); validators.set(operation.name, validate!); } - if (!validate!(input.parameters)) throw failure(422, "daytona_arguments_invalid", "Invalid Daytona action arguments."); + if (!contract.validate(operation.name, input.parameters)) throw failure(422, "daytona_arguments_invalid", "Invalid Daytona action arguments."); const config = record(input.connection.config.methodConfig); return daytonaClient(input.headers, input.request, config.organizationId, input.signal, input.deadlineAt).execute(operation, record(input.parameters)); } diff --git a/server/src/services/native-provider-catalogs.ts b/server/src/services/native-provider-catalogs.ts new file mode 100644 index 00000000000..a37681c4168 --- /dev/null +++ b/server/src/services/native-provider-catalogs.ts @@ -0,0 +1,50 @@ +import { DAYTONA_TOOLS, daytonaClient, daytonaRisk, executeDaytonaTool, isCurrentDaytonaCatalogEntry, isDaytonaConnection, isDaytonaToolName } from "./daytona.js"; +import type { NativeCatalogEntry, NativeRestRequest } from "./native-rest-client.js"; +import { UNIKRAFT_DEFAULT_METRO, UNIKRAFT_TOOLS, executeUnikraftTool, isCurrentUnikraftCatalogEntry, isUnikraftConnection, + isUnikraftToolName, unikraftClient, unikraftRisk } from "./connectors/unikraft/client.js"; + +type ConnectionShape = { transport: string; config?: Record }; +const methodConfig = (connection: ConnectionShape) => { + const value = connection.config?.methodConfig; + return value && typeof value === "object" && !Array.isArray(value) ? value as Record : {}; +}; + +/** A provider whose action catalog is generated from its published HTTP API + * and executed by a Paperclip bridge rather than a remote MCP server. Shared + * connection code consumes this contract instead of naming providers. */ +export type NativeProviderCatalog = { + slug: string; + name: string; + tools: typeof DAYTONA_TOOLS; + isConnection(connection: ConnectionShape): boolean; + isToolName(name: string): boolean; + risk(name: string): "read" | "write" | "destructive"; + isCurrentEntry(entry: NativeCatalogEntry): boolean; + probe(connection: ConnectionShape, headers: Record, request: NativeRestRequest): Promise; + execute(input: Parameters[0]): Promise; +}; + +export const NATIVE_PROVIDER_CATALOGS: readonly NativeProviderCatalog[] = [ + { + slug: "daytona", name: "Daytona", tools: DAYTONA_TOOLS, + isConnection: isDaytonaConnection, isToolName: isDaytonaToolName, risk: daytonaRisk, isCurrentEntry: isCurrentDaytonaCatalogEntry, + probe: (connection, headers, request) => daytonaClient(headers, request, methodConfig(connection).organizationId).probe(), + execute: executeDaytonaTool, + }, + { + slug: "unikraft", name: "Unikraft", tools: UNIKRAFT_TOOLS, + isConnection: isUnikraftConnection, isToolName: isUnikraftToolName, risk: unikraftRisk, isCurrentEntry: isCurrentUnikraftCatalogEntry, + probe: (connection, headers, request) => unikraftClient(headers, request, methodConfig(connection).metro ?? UNIKRAFT_DEFAULT_METRO).probe(), + execute: executeUnikraftTool, + }, +]; + +export const NATIVE_PROVIDER_SLUGS: readonly string[] = NATIVE_PROVIDER_CATALOGS.map(provider => provider.slug); + +export function nativeProviderCatalogFor(connection: ConnectionShape): NativeProviderCatalog | null { + return NATIVE_PROVIDER_CATALOGS.find(provider => provider.isConnection(connection)) ?? null; +} + +export function nativeProviderCatalogForSlug(slug: unknown): NativeProviderCatalog | null { + return NATIVE_PROVIDER_CATALOGS.find(provider => provider.slug === slug) ?? null; +} diff --git a/server/src/services/native-rest-client.ts b/server/src/services/native-rest-client.ts new file mode 100644 index 00000000000..2b78e0baa77 --- /dev/null +++ b/server/src/services/native-rest-client.ts @@ -0,0 +1,168 @@ +import Ajv, { type ValidateFunction } from "ajv"; +import addFormats from "ajv-formats"; +import { HttpError } from "../errors.js"; +import { redactSensitiveText } from "../redaction.js"; +import { descriptorHash, stableHash } from "./tool-catalog-hash.js"; + +/** Shared transport for native provider catalogs (Daytona, Unikraft). Each + * provider owns its endpoints and target checks; this owns the bounded, + * credential-redacting request/response handling they must all apply. */ +export const NATIVE_REST_MAX_BYTES = 8 * 1024 * 1024; +export type NativeRestRequest = (url: string, init: RequestInit) => Promise; + +export const nativeRestFailure = (status: number, code: string, message: string) => new HttpError(status, message, { code }); + +export function nativeRestRecord(value: unknown): Record { + return value && typeof value === "object" && !Array.isArray(value) ? value as Record : {}; +} + +export function nativeRestPathValue(value: unknown, invalid: () => Error) { + const text = String(value); + if (!text || text.length > 8192 || /[\u0000-\u001f\\]/.test(text) + || text.split("/").some(segment => segment === "." || segment === "..")) { + throw invalid(); + } + return encodeURIComponent(text); +} + +export function nativeRestSender(options: { + /** Provider display name used in caller-facing messages. */ + provider: string; + /** Error code prefix, such as `daytona`. */ + code: string; + authorization: string; + request: NativeRestRequest; + signal: AbortSignal; + deadlineAt: number; + deniedMessage: string; + /** Provider-owned headers set after caller headers so they cannot be replaced. */ + fixedHeaders?: Record; +}) { + const { provider, code, authorization, request, signal, deadlineAt } = options; + const secret = authorization.replace(/^Bearer /, ""); + const expired = () => nativeRestFailure(504, `${code}_deadline_exceeded`, `The ${provider} action deadline expired. An operation already sent may still be running; inspect it before retrying.`); + // Bound the caller even if a transport or body reader is slow to acknowledge + // cancellation. A late prerequisite result never advances to the next call. + async function withinDeadline(work: () => Promise): Promise { + if (signal.aborted || Date.now() >= deadlineAt) throw expired(); + let abort!: () => void; + try { + return await Promise.race([ + Promise.resolve().then(() => { if (signal.aborted || Date.now() >= deadlineAt) throw expired(); return work(); }), + new Promise((_resolve, reject) => { + abort = () => reject(expired()); + signal.addEventListener("abort", abort, { once: true }); + }), + ]); + } finally { signal.removeEventListener("abort", abort); } + } + function sanitize(value: unknown): unknown { + if (typeof value === "string") return redactSensitiveText(value.split(secret).join("[REDACTED]")); + if (Array.isArray(value)) return value.map(sanitize); + if (value && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, sanitize(item)])); + return value; + } + + /** `expectedStatuses` maps a provider-documented non-2xx outcome (for + * example, a wait that timed out) to the result returned in its place. */ + async function send(url: string, init: RequestInit, extraHeaders: Record = {}, + expectedStatuses: Record = {}): Promise { + const projected = new Headers(init.headers); + projected.set("Authorization", authorization); + for (const [name, value] of Object.entries({ ...options.fixedHeaders, ...extraHeaders })) projected.set(name, value); + let response: Response; + try { + response = await withinDeadline(() => request(url, { ...init, redirect: "error", headers: Object.fromEntries(projected), signal })); + } catch { + if (signal.aborted || Date.now() >= deadlineAt) throw expired(); + throw nativeRestFailure(502, `${code}_outcome_unknown`, init.method === "GET" + ? `${provider} could not complete the request.` + : `${provider} did not return a result. The operation may have started; inspect its state before retrying.`); + } + if (Object.hasOwn(expectedStatuses, response.status)) { + void response.body?.cancel().catch(() => {}); + return expectedStatuses[response.status]; + } + if (!response.ok) { + void response.body?.cancel().catch(() => {}); + const status = [400, 401, 403, 404, 409, 422, 429].includes(response.status) ? response.status : 502; + throw nativeRestFailure(status, `${code}_request_failed`, status === 401 || status === 403 + ? options.deniedMessage + : status === 404 ? `${provider} could not find this resource.` : `${provider} could not complete this operation.`); + } + if (response.status === 204 || !response.body) return null; + const reader = response.body.getReader(); + const chunks: Uint8Array[] = []; + let length = 0; + try { + for (;;) { + const part = await withinDeadline(() => reader.read()); + if (part.done) break; + length += part.value.byteLength; + if (length > NATIVE_REST_MAX_BYTES) throw nativeRestFailure(502, `${code}_output_limit`, `${provider}'s response exceeded the limit. Request a smaller page or file range.`); + chunks.push(part.value); + } + } catch (error) { + if (error instanceof HttpError) throw error; + throw nativeRestFailure(502, `${code}_outcome_unknown`, `${provider}'s response was interrupted. Inspect the operation before retrying.`); + } finally { void reader.cancel().catch(() => {}); } + const bytes = Buffer.concat(chunks); + if (!bytes.length) return null; + const contentType = response.headers.get("Content-Type") ?? "application/octet-stream"; + if (contentType.includes("json")) { + try { return sanitize(JSON.parse(bytes.toString("utf8"))); } + catch { throw nativeRestFailure(502, `${code}_invalid_response`, `${provider} returned an invalid response.`); } + } + if (contentType.startsWith("text/")) return sanitize(bytes.toString("utf8")); + if (bytes.includes(Buffer.from(secret))) throw nativeRestFailure(403, `${code}_credential_export_denied`, "The response contains connection credential material."); + return { contentType, contentBase64: bytes.toString("base64"), byteLength: bytes.length }; + } + + return { send, withinDeadline }; +} + +type NativeOperation = { + name: string; title: string; description: string; inputSchema: unknown; + annotations: { readOnlyHint: boolean; destructiveHint: boolean; idempotentHint?: boolean; openWorldHint?: boolean }; +}; +export type NativeCatalogEntry = { toolName: string; versionHash: string; schemaHash: string | null; riskLevel: string }; + +// Match the interop used by the existing plugin configuration validator. +const AjvCtor = (Ajv as any).default ?? Ajv; +const ajv = new AjvCtor({ strict: false, allErrors: true, validateFormats: true }); +((addFormats as any).default ?? addFormats)(ajv); + +/** The deployed contract of a generated provider catalog: agent-visible tools, + * reviewed risk, saved-entry staleness and argument validation. */ +export function nativeCatalog(operationsList: T[]) { + const operations = new Map(operationsList.map(operation => [operation.name, operation])); + const validators = new Map(); + const risk = (name: string): "read" | "write" | "destructive" => { + const operation = operations.get(name); + if (!operation || operation.annotations.destructiveHint) return "destructive"; + return operation.annotations.readOnlyHint ? "read" : "write"; + }; + const tools = operationsList.map(({ name, title, description, inputSchema, annotations }) => ({ + name, title, description, inputSchema: inputSchema as Record, annotations, + })); + const contracts = new Map(tools.map(tool => [tool.name, { + versionHash: descriptorHash(tool, risk(tool.name)), schemaHash: stableHash(tool.inputSchema), riskLevel: risk(tool.name), + }])); + return { + tools, + operation: (name: string) => operations.get(name), + isToolName: (name: string) => operations.has(name), + risk, + isCurrentEntry(entry: NativeCatalogEntry): boolean { + const current = contracts.get(entry.toolName); + return Boolean(current && current.versionHash === entry.versionHash && current.schemaHash === entry.schemaHash && current.riskLevel === entry.riskLevel); + }, + validate(name: string, parameters: unknown): boolean { + const operation = operations.get(name); + if (!operation) return false; + let validate = validators.get(name); + if (!validate) { validate = ajv.compile(operation.inputSchema as object) as ValidateFunction; validators.set(name, validate); } + return validate(parameters) as boolean; + }, + }; +} diff --git a/server/src/services/tool-access.ts b/server/src/services/tool-access.ts index 7bee173fc66..03b7255ebbd 100644 --- a/server/src/services/tool-access.ts +++ b/server/src/services/tool-access.ts @@ -72,7 +72,7 @@ export { loadGitHubGrantMetadata, loadGitHubTokenRepositories } from "./tool-acc import { stableHash } from "./tool-catalog-hash.js"; -import { isDaytonaConnection } from "./daytona.js"; +import { NATIVE_PROVIDER_SLUGS, nativeProviderCatalogFor } from "./native-provider-catalogs.js"; import { connectorToolPolicy } from "./connectors/tool-policy.js"; import { notifyChatEndpointWork } from "./chat-work-notifications.js"; @@ -2412,7 +2412,7 @@ export function toolAccessService( applicationKey: `app-gallery:${galleryEntry?.slug ?? "link"}:${randomUUID()}`, name: applicationName, description: safeApplicationDescription, - type: transport === "rest_api" && ["browser-use-cloud", "daytona"].includes(galleryEntry?.slug ?? "") ? "rest_api" : transport === "mcp_remote" ? "mcp_http" : "mcp_stdio", + type: transport === "rest_api" && ["browser-use-cloud", ...NATIVE_PROVIDER_SLUGS].includes(galleryEntry?.slug ?? "") ? "rest_api" : transport === "mcp_remote" ? "mcp_http" : "mcp_stdio", status: "draft", metadata: galleryEntry ? { @@ -4488,7 +4488,7 @@ export function toolAccessService( companyId, applicationKey: normalizeKey(input.applicationName ?? input.name), name: input.applicationName ?? input.name, - type: isBrowserUseConnection({ transport, config }) || isDaytonaConnection({ transport, config }) ? "rest_api" : transport === "mcp_remote" ? "mcp_http" : "mcp_stdio", + type: isBrowserUseConnection({ transport, config }) || nativeProviderCatalogFor({ transport, config }) ? "rest_api" : transport === "mcp_remote" ? "mcp_http" : "mcp_stdio", status: "active", metadata: {}, }) diff --git a/server/src/services/tool-access/catalog/descriptors.ts b/server/src/services/tool-access/catalog/descriptors.ts index 7644a700019..f30c518ea6c 100644 --- a/server/src/services/tool-access/catalog/descriptors.ts +++ b/server/src/services/tool-access/catalog/descriptors.ts @@ -1,4 +1,4 @@ -import { isDaytonaConnection } from "../../daytona.js"; +import { nativeProviderCatalogFor } from "../../native-provider-catalogs.js"; import { googleWorkspaceToolTarget } from "@paperclipai/shared"; import { BROWSER_USE_TOOLS } from "@paperclipai/shared"; import { isMemoryConnectorId } from "@paperclipai/shared"; @@ -291,5 +291,5 @@ export function isGoogleWorkspaceToolAllowed( export function shouldQuarantineNewEntries( connection: typeof toolConnections.$inferSelect, ): boolean { - return isDaytonaConnection(connection) || asRecord(connection.config).quarantineNewEntries === true; + return Boolean(nativeProviderCatalogFor(connection)) || asRecord(connection.config).quarantineNewEntries === true; } diff --git a/server/src/services/tool-access/catalog/discovery.ts b/server/src/services/tool-access/catalog/discovery.ts index deb7343a5ad..8dfd90c0c19 100644 --- a/server/src/services/tool-access/catalog/discovery.ts +++ b/server/src/services/tool-access/catalog/discovery.ts @@ -5,7 +5,7 @@ import type { ActorInfo } from "../types.js"; import { createOAuthDiscovery } from "../oauth/discovery.js"; import { oauthConfig } from "../oauth/metadata.js"; import { projectedConnectionHeaders } from "../connection-methods.js"; -import { DAYTONA_TOOLS, daytonaClient, isDaytonaConnection } from "../../daytona.js"; +import { nativeProviderCatalogFor } from "../../native-provider-catalogs.js"; import { GOOGLE_WORKSPACE_SERVICES, googleWorkspaceServiceGranted, googleWorkspaceToolName, googleWorkspaceGrantedScopes, isGoogleWorkspaceToolGranted, type GoogleWorkspaceService } from "@paperclipai/shared"; import { isInsufficientConnectionScope, INSUFFICIENT_CONNECTION_SCOPE_MESSAGE } from "../../connection-permission-errors.js"; import { mcpDiscoveryHttpFailure, withMcpConnectionFailure } from "../../mcp-connection-failure.js"; @@ -426,10 +426,11 @@ export function createToolCatalogDiscovery(db: Db, { now, audit, secrets, creden assertSupportedConnection(connection); await assertConnectorConnectionSetupEnabled(connection); if (connection.connectionPurpose === "ai") throw unprocessable("AI connections provide runtime authentication, not tool actions"); - if (isDaytonaConnection(connection)) { + const nativeProvider = nativeProviderCatalogFor(connection); + if (nativeProvider) { const headers = credentialHeaders ?? await resolveCredentialHeaders(connection, actor); - await daytonaClient(headers, (url, init) => requestRemoteHttpEndpoint(new URL(url), init), asRecord(connection.config.methodConfig).organizationId).probe(); - return DAYTONA_TOOLS; + await nativeProvider.probe(connection, headers, (url, init) => requestRemoteHttpEndpoint(new URL(url), init)); + return nativeProvider.tools; } if (isBrowserUseConnection(connection)) { const headers = credentialHeaders ?? await resolveCredentialHeaders(connection, actor); diff --git a/server/src/services/tool-access/catalog/health.ts b/server/src/services/tool-access/catalog/health.ts index e05be18a92b..e67f21ea6ad 100644 --- a/server/src/services/tool-access/catalog/health.ts +++ b/server/src/services/tool-access/catalog/health.ts @@ -3,7 +3,7 @@ import { connectionSetupUrl, connectionReconnectUrl } from "../identifiers.js"; import { createGitHubGrantContinuity } from "../github-grant-continuity.js"; import { createToolRuntimeSlots } from "../runtime/slots.js"; import type { ActorInfo } from "../types.js"; -import { isDaytonaConnection } from "../../daytona.js"; +import { nativeProviderCatalogFor } from "../../native-provider-catalogs.js"; import { retainMcpConnectionFailure } from "../../mcp-connection-failure.js"; import { isBrowserUseConnection } from "../../browser-use-client.js"; import { and, asc, eq, isNull, lte, ne, or, sql } from "drizzle-orm"; @@ -121,7 +121,7 @@ export function createConnectionHealth(db: Db, { now, audit, getConnectionRow, d }); for (const grant of grantsToCheck) await refreshManagedGitHubGrantAccess(connection, grant, actor); - } else if (isBrowserUseConnection(connection) || isDaytonaConnection(connection)) { + } else if (isBrowserUseConnection(connection) || nativeProviderCatalogFor(connection)) { await discoverTools(connection, undefined, actor); } else if (isAgentMailConnection(connection)) { await validateAgentMailConnection(connection); @@ -157,8 +157,8 @@ export function createConnectionHealth(db: Db, { now, audit, getConnectionRow, d : config.sourceTemplateKey === "github" && oauth.connectorProfile === "github.code" ? "GitHub account, installation, and repository access are available." - : isDaytonaConnection(connection) - ? "Daytona API access is connected." + : nativeProviderCatalogFor(connection) + ? `${nativeProviderCatalogFor(connection)!.name} API access is connected.` : isBrowserUseConnection(connection) ? "Browser Use API key is connected." : isAgentMailConnection(connection) diff --git a/server/src/services/tool-access/catalog/publication.ts b/server/src/services/tool-access/catalog/publication.ts index f60d00f213e..f8236f5c58c 100644 --- a/server/src/services/tool-access/catalog/publication.ts +++ b/server/src/services/tool-access/catalog/publication.ts @@ -4,7 +4,7 @@ import { createToolRuntimeSlots } from "../runtime/slots.js"; import type { ActorInfo, ToolAccessMutationDb } from "../types.js"; import { connectionMethodForConnection } from "../connection-methods.js"; import { stableHash, descriptorHash } from "../../tool-catalog-hash.js"; -import { isDaytonaConnection } from "../../daytona.js"; +import { nativeProviderCatalogFor } from "../../native-provider-catalogs.js"; import { googleWorkspaceToolTarget, googleWorkspaceGrantedScopes, isGoogleWorkspaceToolGranted } from "@paperclipai/shared"; import { retainMcpConnectionFailure } from "../../mcp-connection-failure.js"; import { isRemoteMcpConnectorMethod } from "@paperclipai/shared"; @@ -115,9 +115,9 @@ export function createToolCatalogPublication(db: Db, { now, audit, getConnection const existingByName = new Map( existingRows.map((entry) => [entry.toolName, entry]), ); - const nativeDaytona = isDaytonaConnection(connection); + const nativeProvider = Boolean(nativeProviderCatalogFor(connection)); if (connection.config.sourceTemplateKey === "google-workspace" - || nativeDaytona || isRemoteMcpConnectorMethod(connection.config.sourceTemplateKey, connection.config.connectionMethodKey)) { + || nativeProvider || isRemoteMcpConnectorMethod(connection.config.sourceTemplateKey, connection.config.connectionMethodKey)) { const discoveredNames = new Set(descriptors.map((descriptor) => descriptor.name)); // The catalog is shared, but discovery uses only the caller's grant. A // narrower personal consent must not remove actions another active grant @@ -135,13 +135,13 @@ export function createToolCatalogPublication(db: Db, { now, audit, getConnection } // Preserve only consent-filtered actions, not failed-service catalogs or // tools a successful, sufficiently authorized discovery actually removed. - const removedStatus = nativeDaytona ? "removed" : "disabled"; + const removedStatus = nativeProvider ? "removed" : "disabled"; const removedIds = existingRows.filter((entry) => !discoveredNames.has(entry.toolName) && entry.status !== removedStatus && !(otherWorkspaceGrants.some((scopes) => isGoogleWorkspaceToolGranted(entry.toolName, scopes)) && !isGoogleWorkspaceToolGranted(entry.toolName, selectedWorkspaceScopes) && !unavailableServices.includes(googleWorkspaceToolTarget(entry.toolName)?.service ?? ""))).map((entry) => entry.id); if (removedIds.length) await publish(tx => tx.update(toolCatalogEntries) - .set({ status: removedStatus, quarantineReason: nativeDaytona ? "provider_tool_removed" : "mcp_tool_removed", updatedAt: refreshedAt }) + .set({ status: removedStatus, quarantineReason: nativeProvider ? "provider_tool_removed" : "mcp_tool_removed", updatedAt: refreshedAt }) .where(and(eq(toolCatalogEntries.companyId, connection.companyId), eq(toolCatalogEntries.connectionId, connection.id), inArray(toolCatalogEntries.id, removedIds)))); } // Retired native actions are absent from discovery, but old catalog rows @@ -200,7 +200,7 @@ export function createToolCatalogPublication(db: Db, { now, audit, getConnection quarantineOnRefresh && (!existing || changed) && existing?.status !== "disabled" && - (!safeDefault || riskLevel !== "read" || nativeDaytona); + (!safeDefault || riskLevel !== "read" || nativeProvider); const providerPermanentlyBlocked = Boolean( googleProfile && !isGoogleWorkspaceToolAllowed(googleProfile, descriptor), diff --git a/server/src/services/tool-access/catalog/reader.ts b/server/src/services/tool-access/catalog/reader.ts index b7c1d300125..938f5042d87 100644 --- a/server/src/services/tool-access/catalog/reader.ts +++ b/server/src/services/tool-access/catalog/reader.ts @@ -1,7 +1,7 @@ import { singleFlight } from "../single-flight.js"; import type { ActorInfo } from "../types.js"; import { descriptorHash } from "../../tool-catalog-hash.js"; -import { DAYTONA_TOOLS, daytonaRisk, isDaytonaConnection, isDaytonaToolName } from "../../daytona.js"; +import { nativeProviderCatalogFor } from "../../native-provider-catalogs.js"; import { desc, eq } from "drizzle-orm"; import type { Db } from "@paperclipai/db"; import { toolCatalogEntries, toolConnections } from "@paperclipai/db"; @@ -27,18 +27,18 @@ export function createToolCatalogReader(db: Db, options: { catalogCacheTtlMs?: n .where(eq(toolCatalogEntries.connectionId, connection.id)) .orderBy(desc(toolCatalogEntries.updatedAt)); // Native provider catalogs change with the deployed code. Upgrade saved - // Daytona connections too; their first small catalog must not survive - // merely because REST discovery has no remote MCP cache timer. - const nativeDaytona = isDaytonaConnection(connection); + // connections too; an earlier catalog must not survive merely because + // REST discovery has no remote MCP cache timer. + const nativeProvider = nativeProviderCatalogFor(connection); const rowsByName = new Map(rows.map((row) => [row.toolName, row])); - const daytonaCatalogChanged = nativeDaytona && connection.status === "active" && ( - rows.some((row) => row.status !== "removed" && !isDaytonaToolName(row.toolName)) - || DAYTONA_TOOLS.some((tool) => { + const nativeCatalogChanged = Boolean(nativeProvider) && connection.status === "active" && ( + rows.some((row) => row.status !== "removed" && !nativeProvider!.isToolName(row.toolName)) + || nativeProvider!.tools.some((tool) => { const existing = rowsByName.get(tool.name); - return !existing || existing.versionHash !== descriptorHash(tool, daytonaRisk(tool.name)); + return !existing || existing.versionHash !== descriptorHash(tool, nativeProvider!.risk(tool.name)); }) ); - const cacheExpired = daytonaCatalogChanged || ( + const cacheExpired = nativeCatalogChanged || ( connection.transport === "mcp_remote" && connection.status !== "archived" && (rows.length === 0 || @@ -48,7 +48,7 @@ export function createToolCatalogReader(db: Db, options: { catalogCacheTtlMs?: n if (cacheExpired) { try { await singleFlight(catalogRefreshFlights, connection.id, () => - refreshCatalog(connection.id, nativeDaytona && actor ? actor : { + refreshCatalog(connection.id, nativeProvider && actor ? actor : { actorType: "system", actorId: "tool_catalog_cache", }), @@ -68,7 +68,7 @@ export function createToolCatalogReader(db: Db, options: { catalogCacheTtlMs?: n // Even failed discovery must not keep retired native tools visible. // Their database rows remain available to historical invocation records. return rows - .filter((row) => !nativeDaytona || (row.status !== "removed" && isDaytonaToolName(row.toolName))) + .filter((row) => !nativeProvider || (row.status !== "removed" && nativeProvider.isToolName(row.toolName))) .map((row) => toCatalogEntryForConnection(row, connection)); } }; diff --git a/server/src/services/tool-access/token-broker/service.ts b/server/src/services/tool-access/token-broker/service.ts index f6757e383bc..2d914ae2030 100644 --- a/server/src/services/tool-access/token-broker/service.ts +++ b/server/src/services/tool-access/token-broker/service.ts @@ -1,4 +1,4 @@ -import { isDaytonaConnection } from "../../daytona.js"; +import { nativeProviderCatalogFor } from "../../native-provider-catalogs.js"; import { isBrowserUseConnection } from "../../browser-use-client.js"; import { and, eq, sql } from "drizzle-orm"; import type { Db } from "@paperclipai/db"; @@ -58,7 +58,8 @@ export function createConnectionTokenBroker(db: Db, { now, secrets, policySvc, a input.connectionId, input.companyId, ); - if (isDaytonaConnection(connection)) throw forbidden("Daytona credentials stay in the governed tool gateway and cannot be exported."); + const nativeProvider = nativeProviderCatalogFor(connection); + if (nativeProvider) throw forbidden(`${nativeProvider.name} credentials stay in the governed tool gateway and cannot be exported.`); if (isBrowserUseConnection(connection)) throw forbidden("Browser Use credentials stay in the governed tool gateway and cannot be exported."); if (connection.connectionPurpose === "ai") throw unprocessable("AI credentials are available only through the runtime resolver"); const application = await getConnectionApplication(connection); diff --git a/server/src/services/tool-gateway.ts b/server/src/services/tool-gateway.ts index 3e97a9fd7cc..7bfde3b1b85 100644 --- a/server/src/services/tool-gateway.ts +++ b/server/src/services/tool-gateway.ts @@ -5,7 +5,7 @@ import type { BudgetServiceHooks } from "./budgets.js"; import type { PromptInjectionDetectorMetadata } from "@paperclipai/shared"; import { notifyDeliveryWork, DELIVERY_QUEUES } from "./delivery-work-notifications.js"; import { measureToolDiscovery, measureRunPhase } from "./execution-preparation-timing.js"; -import { executeDaytonaTool, isDaytonaConnection, isCurrentDaytonaCatalogEntry } from "./daytona.js"; +import { nativeProviderCatalogFor } from "./native-provider-catalogs.js"; import { composeConnectionInstructions } from "./connection-instructions.js"; import { googleWorkspaceToolTarget, googleWorkspaceGrantedScopes, isGoogleWorkspaceToolGranted } from "@paperclipai/shared"; import { isInsufficientConnectionScope, INSUFFICIENT_CONNECTION_SCOPE_MESSAGE } from "./connection-permission-errors.js"; @@ -1445,9 +1445,9 @@ export function createToolGatewayService( ({ catalogEntry, connection, application }) => (!featureSettings || isConnectorFeatureEnabled(getAppDefinitionForApplicationKey(application.applicationKey), featureSettings)) && !isRetiredComposioConnection(connection) && - !(isDaytonaConnection(connection) && !isCurrentDaytonaCatalogEntry(catalogEntry)) && + !(nativeProviderCatalogFor(connection) && !nativeProviderCatalogFor(connection)!.isCurrentEntry(catalogEntry)) && !(isRailwayEndpoint(connection.config.url) && (isRailwayToolBlocked(catalogEntry.toolName) || (normalizeRailwayToolName(catalogEntry.toolName).startsWith(RAILWAY_TOOL_PREFIX) && connection.config.railwayApiStatus !== "available"))) && - (((isBrowserUseConnection(connection) || isDaytonaConnection(connection)) && application.type === "rest_api") || + (((isBrowserUseConnection(connection) || nativeProviderCatalogFor(connection)) && application.type === "rest_api") || (connection.transport === "mcp_remote" && application.type === "mcp_http") || (connection.transport === "local_stdio" && @@ -1473,7 +1473,7 @@ export function createToolGatewayService( ({ catalogEntry, connection, application }, index) => { if ( connection.transport !== "mcp_remote" && - connection.transport !== "local_stdio" && !isBrowserUseConnection(connection) && !isDaytonaConnection(connection) + connection.transport !== "local_stdio" && !isBrowserUseConnection(connection) && !nativeProviderCatalogFor(connection) ) { throw new Error( `Non-MCP connection ${connection.id} cannot be exposed through the MCP gateway`, @@ -4903,7 +4903,7 @@ export function createToolGatewayService( ), ) .limit(1); - if (!connection || (connection.transport !== "mcp_remote" && !isBrowserUseConnection(connection) && !isDaytonaConnection(connection))) { + if (!connection || (connection.transport !== "mcp_remote" && !isBrowserUseConnection(connection) && !nativeProviderCatalogFor(connection))) { throw new ToolGatewayHttpError( 404, `Tool "${tool.name}" not found`, @@ -4926,8 +4926,9 @@ export function createToolGatewayService( if (isRailwayEndpoint(connection.config.url) && isRailwayToolBlocked(entry.toolName)) { throw new ToolGatewayHttpError(403, "This Railway action cannot bind its effects to an approved target. Use redeploy, restart, or rollback for an existing deployment.", "railway_action_blocked"); } - if (isDaytonaConnection(connection) && !isCurrentDaytonaCatalogEntry(entry)) { - throw new ToolGatewayHttpError(409, "Daytona's action catalog changed. Review the updated actions in Permissions.", "daytona_catalog_changed"); + const nativeProvider = nativeProviderCatalogFor(connection); + if (nativeProvider && !nativeProvider.isCurrentEntry(entry)) { + throw new ToolGatewayHttpError(409, `${nativeProvider.name}'s action catalog changed. Review the updated actions in Permissions.`, `${nativeProvider.slug}_catalog_changed`); } return { entry, connection }; } @@ -6069,11 +6070,12 @@ export function createToolGatewayService( ms = railwayCommandBudgetMs(parameters); } const grant = await resolveConnectionGrant(session, connection); - if (isDaytonaConnection(connection)) { + const nativeProvider = nativeProviderCatalogFor(connection); + if (nativeProvider) { const budgetMs = Math.min(ms, 30_000); const deadlineAt = Date.now() + budgetMs; const signal = AbortSignal.timeout(budgetMs); - const data = await executeDaytonaTool({ binding: session, connection, + const data = await nativeProvider.execute({ binding: session, connection, headers: await resolveCredentialHeaders(session, connection, grant), request: (url, init) => options.remoteHttpRequest ? options.remoteHttpRequest(url, init) : guardedRemoteHttpFetch(url, init, remoteHttpFetchOptions()), diff --git a/tests/e2e/unikraft-connector.spec.ts b/tests/e2e/unikraft-connector.spec.ts new file mode 100644 index 00000000000..eba676cf29e --- /dev/null +++ b/tests/e2e/unikraft-connector.spec.ts @@ -0,0 +1,26 @@ +import { expect, test } from "@playwright/test"; + +test("Unikraft uses the ordinary access and credential wizard", async ({ page, request }) => { + const response = await request.post("/api/companies", { data: { name: `Unikraft setup ${Date.now()}` } }); + expect(response.ok()).toBe(true); + const company = await response.json(); + await page.goto(`/${company.issuePrefix}/apps`); + await expect(page.getByRole("heading", { name: "Connectors", exact: true })).toBeVisible(); + await page.getByRole("searchbox", { name: "Search connectors" }).fill("Unikraft"); + const card = page.getByRole("list", { name: "Connector list" }).getByRole("listitem").filter({ hasText: "Unikraft" }); + await expect(card).toHaveCount(1); + // App marks are decorative (empty alt), so they have no accessible img role. + await expect(card.locator('img[src="/brands/apps/unikraft.svg"]')).toBeVisible(); + await card.getByRole("button", { name: "Add key Unikraft", exact: true }).click(); + await expect(page.getByRole("heading", { name: "Connect Unikraft", exact: true })).toBeVisible(); + await expect(page.getByLabel("Your Unikraft key", { exact: true })).toBeVisible(); + await expect(page.getByLabel("Your Unikraft key", { exact: true })).toHaveAttribute("type", "password"); + await expect(page.getByText("Default metro", { exact: true })).not.toBeVisible(); + await expect(page.getByText("Connects for everyone in your organization, available to all agents.", { exact: true })).toBeVisible(); + await page.getByRole("button", { name: "Change", exact: true }).click(); + await expect(page.getByText("Which humans can use this credential?", { exact: true })).toBeVisible(); + await expect(page.getByText("Default metro", { exact: true })).toBeVisible(); + await expect(page.locator("select").filter({ has: page.locator('option[value="fra"]') })).toHaveValue("fra"); + // No token is submitted and no real provider connection is created. Gateway + // lifecycle and provider reads are exercised in the database-backed suite. +}); diff --git a/ui/public/brands/apps/manifest.json b/ui/public/brands/apps/manifest.json index e77518449c9..110ab305261 100644 --- a/ui/public/brands/apps/manifest.json +++ b/ui/public/brands/apps/manifest.json @@ -702,6 +702,12 @@ "catalogVisible": true, "localAsset": "/brands/apps/paperclip-notebook.svg", "darkAsset": "/brands/apps/paperclip-notebook-dark.svg" + }, + { + "slug": "unikraft", + "provider": "Unikraft", + "catalogVisible": true, + "localAsset": "/brands/apps/unikraft.svg" } ] } diff --git a/ui/public/brands/apps/unikraft.svg b/ui/public/brands/apps/unikraft.svg new file mode 100644 index 00000000000..834d2ff3a13 --- /dev/null +++ b/ui/public/brands/apps/unikraft.svg @@ -0,0 +1 @@ + \ No newline at end of file