@luxorai/mcp 0.0.0-stage → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +83 -0
- package/LICENSE +21 -0
- package/README.md +221 -2
- package/SECURITY.md +50 -0
- package/dist/client.d.ts +192 -0
- package/dist/client.js +621 -0
- package/dist/env.d.ts +6 -0
- package/dist/env.js +25 -0
- package/dist/http.d.ts +17 -0
- package/dist/http.js +297 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +61 -0
- package/dist/oauth.d.ts +39 -0
- package/dist/oauth.js +166 -0
- package/dist/server.d.ts +50 -0
- package/dist/server.js +238 -0
- package/dist/tools/artifacts.d.ts +16 -0
- package/dist/tools/artifacts.js +19 -0
- package/dist/tools/auth_status.d.ts +35 -0
- package/dist/tools/auth_status.js +108 -0
- package/dist/tools/common.d.ts +70 -0
- package/dist/tools/common.js +138 -0
- package/dist/tools/egress.d.ts +38 -0
- package/dist/tools/egress.js +153 -0
- package/dist/tools/exec.d.ts +4 -0
- package/dist/tools/exec.js +120 -0
- package/dist/tools/files.d.ts +4 -0
- package/dist/tools/files.js +55 -0
- package/dist/tools/files_ops.d.ts +12 -0
- package/dist/tools/files_ops.js +62 -0
- package/dist/tools/git.d.ts +3 -0
- package/dist/tools/git.js +92 -0
- package/dist/tools/identity.d.ts +4 -0
- package/dist/tools/identity.js +5 -0
- package/dist/tools/ports.d.ts +11 -0
- package/dist/tools/ports.js +27 -0
- package/dist/tools/previews.d.ts +18 -0
- package/dist/tools/previews.js +117 -0
- package/dist/tools/run.d.ts +4 -0
- package/dist/tools/run.js +11 -0
- package/dist/tools/sandboxes.d.ts +4 -0
- package/dist/tools/sandboxes.js +388 -0
- package/dist/tools/sessions_admin.d.ts +9 -0
- package/dist/tools/sessions_admin.js +102 -0
- package/dist/tools/snapshots.d.ts +11 -0
- package/dist/tools/snapshots.js +98 -0
- package/dist/tools/ssh.d.ts +15 -0
- package/dist/tools/ssh.js +43 -0
- package/dist/tools/templates.d.ts +15 -0
- package/dist/tools/templates.js +162 -0
- package/dist/tools/titles.d.ts +8 -0
- package/dist/tools/titles.js +102 -0
- package/dist/tools/volumes.d.ts +16 -0
- package/dist/tools/volumes.js +102 -0
- package/dist/tools/workspace.d.ts +12 -0
- package/dist/tools/workspace.js +68 -0
- package/package.json +60 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@tenkicloud/mcp`. This project follows semantic versioning.
|
|
4
|
+
|
|
5
|
+
## [0.4.0] - 2026-11-10
|
|
6
|
+
|
|
7
|
+
- Read `LUXORAI_*` environment variables before their `TENKI_*` equivalents (credentials, endpoint, and every `TENKI_MCP_*` option). `TENKI_*` keeps working.
|
|
8
|
+
- Accept `lx_` API keys alongside `tk_`.
|
|
9
|
+
- `TENKI_MCP_OAUTH_RESOURCE` accepts a comma-separated list; protected-resource metadata returns the entry matching the request host.
|
|
10
|
+
- Add a `luxor-mcp` bin alias and a `luxor` Claude Code plugin. Prepare a gated dual publish as `@luxorai/mcp` (`io.github.LuxorLabs/luxor-mcp`, `ghcr.io/luxorlabs/luxor-mcp`).
|
|
11
|
+
- Remove `tenki_get_workspace_settings`, `tenki_update_workspace_settings`, `tenki_get_snapshot_retention_settings` and `tenki_update_snapshot_retention_settings`: their API methods were removed upstream and every call returned 404. Plan limits are visible through `tenki_get_workspace_usage`.
|
|
12
|
+
- Add `tenki_get_sandbox_metrics` (CPU/memory averages over a window) and `tenki_get_workspace_preview_domains` / `tenki_update_workspace_preview_domains` (wildcard preview domains).
|
|
13
|
+
- Fix `tenki_attach_volume` `read_only`, which the API silently ignored (wrong wire field name); the mount was always read-write.
|
|
14
|
+
- Fix `tenki_update_sandbox`: `max_duration_seconds` always failed (the API requires an explicit sticky value — sticky=false is now sent for you after checking the sandbox is not sticky; a sticky sandbox is refused without an explicit `sticky`); `tags: []` now clears tags; new `sticky` and `clear_tags` arguments; `idle_timeout_minutes` removed because the API cannot change it after creation.
|
|
15
|
+
- Fix `tenki_list_ssh_gateways`: `workspace_id` did nothing; the tool now takes `region` and/or `session_id`.
|
|
16
|
+
- `tenki_get_download_url` no longer requires `session_id` (the API ignores it).
|
|
17
|
+
- `tenki_list_preview_urls` filters by `session_id` on the server instead of on the fetched page.
|
|
18
|
+
- `tenki_create_sandbox`: explicit `allow_inbound: false` / `allow_outbound: false` are sent; new `sticky`, `volumes`, `ssh_authorized_keys`, `metadata`, `egress_allow_domains` / `egress_allow_cidrs`, `template_spec_id`, `setup_env`, `setup_secrets`, `secret_overrides`; the server-side ready wait is used; API `warnings` are returned; `memory_mb` must be even and `disk_size_gb` 5-100.
|
|
19
|
+
- New optional arguments: `async` on `tenki_pause_sandbox` and `tenki_create_snapshot`; `tags` / `clear_tags` on `tenki_update_snapshot` and `tenki_update_volume`, plus `clear_expires_at` on snapshots; `force` on `tenki_detach_volume`; `mode` on `tenki_make_dir`; `expires_at` on `tenki_expose_port` and `tenki_bind_preview_url`; `slug` lookup on `tenki_get_preview_url`; `requested_ttl_seconds` / `source_addresses` on `tenki_issue_ssh_cert`; `tags` / `sticky` filters on the sandbox lists.
|
|
20
|
+
- Tool descriptions now flag the API-deprecated methods behind `tenki_expose_port`, `tenki_unexpose_port`, `tenki_list_exposed_ports`, `tenki_list_workspace_sandboxes` and `tenki_list_workspace_snapshots`, and point to the replacements.
|
|
21
|
+
- `tenki_create_sandbox`: `cpu_cores` up to 128 and `memory_mb` up to 524288 (the API's new ceilings; your plan may cap lower); new `secret_files` (write a workspace secret or inline text to a guest path at boot), `secret_policies` (attach saved secret access policies that inject workspace secrets into matching outbound HTTPS requests) and `tailnet` (join the sandbox to a Tailscale tailnet with an auth key, where the workspace allows it). Template `cpu_cores` / `memory_mb` bounds raised to match.
|
|
22
|
+
- Server-side `search`, `states`, `sort_by` / `sort_desc` and `include_facets` on `tenki_list_workspace_sandboxes` (plus `order`), `tenki_list_volumes`, `tenki_list_templates` and `tenki_list_preview_urls`; preview-URL rows now include the bound sandbox's state and name.
|
|
23
|
+
- `tenki_issue_ssh_cert` documents the new `pinHostKey` flag (verify the gateway host certificate against `caPub`).
|
|
24
|
+
- Contradictory or empty updates are rejected before any call: `tags`+`clear_tags`, `expires_at`+`clear_expires_at`, an egress allowlist with `allow_outbound: false`, and update calls with nothing to change (sandbox, snapshot, volume, template).
|
|
25
|
+
- Add `tenki_list_template_builds` (every build of a template with per-image storage and whether it can be deleted) and `tenki_delete_template_build` (free a build's image and any published image version carrying it; refused while the image is still referenced, e.g. published, in use by a non-terminated sandbox, building or tagged).
|
|
26
|
+
- Add `tenki_create_egress_policy`, `tenki_get_egress_policy`, `tenki_list_egress_policies`, `tenki_update_egress_policy` and `tenki_delete_egress_policy` for named workspace egress policies.
|
|
27
|
+
- `tenki_create_sandbox`: new `egress_policies` (pin saved egress policies by name), `egress_intercept_https` (check each HTTPS request's host) and `egress_http_rules` (allow only matching HTTP methods and paths per host).
|
|
28
|
+
- Add `tenki_fork_sandbox` (start copies of a running sandbox from a fresh snapshot of its memory and disk) and `tenki_list_ci_runner_sizes` (the runs-on sizes Tenki CI can run for the workspace).
|
|
29
|
+
- `tenki_create_sandbox`: new `egress_allowed_ports` (extra TCP ports alongside HTTPS interception, also `allowed_ports` on the egress policy tools), `disable_tailnet` (skip the workspace's default Tailscale connection) and `tailnet.connection_id` / `connection_revision` (join with a Tailscale connection saved in the dashboard instead of an auth key).
|
|
30
|
+
- Descriptions now match the API: HTTPS interception limits public egress to ports 80/443; a volume still syncing back cannot be detached, even with `force`; expired snapshots cannot be booted from or updated, and pause snapshots cannot be booted from or have their expiry changed; another workspace's image with runtime secrets needs each one selected in `secret_overrides`; `tenki_pause_sandbox` returns once the checkpoint is saved on the host, and a tailnet-attached sandbox cannot be paused yet.
|
|
31
|
+
- A fork that fails partway names the snapshot and sandboxes it already created, and is never retried automatically. Create, fork and other non-repeatable calls refused by a workspace limit now fail at once instead of retrying.
|
|
32
|
+
- `tenki_resize_volume` and `tenki_detach_volume` descriptions now match the API: only a never-attached volume can be resized, and a forced detach loses unflushed writes.
|
|
33
|
+
- Update `@modelcontextprotocol/sdk` to 1.32.1 (fixes GHSA-6qxp-vccf-f47h: the OAuth client could send credentials to an authorization server chosen by the MCP server) and refresh transitive pins flagged by `npm audit`.
|
|
34
|
+
- `tenki_resume_sandbox` matches the API's new resume behavior (sandbox-engine v2.35.0): it waits until the sandbox is RUNNING with the 600s slow-operation budget instead of 30s, takes `async` to return once the resume is accepted, and a wait that times out says the sandbox may still be resuming. Calling it again joins a resume in progress.
|
|
35
|
+
- **Breaking:** `tenki_git` is replaced by `tenki_git_clone`, `tenki_git_checkout`, `tenki_git_diff` and `tenki_git_log`, so reads and writes are separate tools. Each takes typed arguments instead of an `operation` + `args` map, and checkout/diff/log take `directory` to target a repository cloned into a subdirectory (previously they always ran in `/home/tenki` and failed with "not a git repository"). Values git would parse as options (a leading `-`, e.g. `--output=<file>`) are refused, and output over 64 KiB is truncated with `totalBytes`.
|
|
36
|
+
- Every tool now has a human-readable `title` (MCP `Tool.title` and `annotations.title`).
|
|
37
|
+
- `destructiveHint` now marks every tool that can overwrite, modify, interrupt or delete existing state, not only deletes: `tenki_exec`, `tenki_write_file`, `tenki_move_path`, `tenki_git_checkout`, every `update` tool, `tenki_pause_sandbox`, `tenki_open_preview`, `tenki_bind_preview_url`, `tenki_resize_volume`, `tenki_cancel_template_build`, `tenki_build_template` (a build with `image_name` changes what `workspace/name` boots) and `tenki_get_upload_url`. Clients that prompt before destructive tools will now prompt for these. Only tools that only add, never change or remove what exists: create (sandboxes, forks, clones, snapshots, volumes, templates, egress policies, preview URLs, port exposures, directories, SSH certificates), resume, extend, attach a volume, keep-alive are non-destructive.
|
|
38
|
+
- `tenki_read_file` returns at most `max_bytes` (default 65536, up to 1 MiB) from `offset`, with `totalBytes`, and `truncated` + `nextOffset` when more remains. Cuts never split a UTF-8 character. Previously it returned the whole file.
|
|
39
|
+
- `tenki_open_preview`'s description now says what it returns (a viewer-token-gated, non-public URL; 7681 is the web terminal) and that reopening a port replaces its earlier URL, instead of steering callers away from it.
|
|
40
|
+
- Every tool parameter now has a description.
|
|
41
|
+
|
|
42
|
+
## [0.2.0] — 2026-08-21
|
|
43
|
+
|
|
44
|
+
- Remove standalone image management tools. Template images remain available through template builds and sandbox creation.
|
|
45
|
+
- Use `image` when creating a sandbox from a template image; removed image-reference arguments are now rejected.
|
|
46
|
+
- Cap `tenki_exec` / `tenki_run_code` inline output at 64 KB per stream (override with `max_output_bytes` on `tenki_exec`). Larger output returns a head+tail preview with `stdoutTruncated`/`stderrTruncated` set, and the full capture file is kept in the sandbox at the reported `stdoutPath`/`stderrPath`.
|
|
47
|
+
- Remove the project-scoped surface — the `tenki_list_project_sandboxes`/`_templates`/`_volumes`/`_snapshots` tools (their API methods no longer exist) and the `project_id` arguments (ignored by the API). Use the workspace-scoped equivalents.
|
|
48
|
+
- Document the template-image path end to end: `image_name` builds require a typed template (`builder_spec`), and `tenki_create_sandbox`'s `image` takes the ready build's `imageDigestRef`.
|
|
49
|
+
|
|
50
|
+
## [0.1.0] — 2026-08-03 — Initial release
|
|
51
|
+
|
|
52
|
+
Model Context Protocol server for Tenki Cloud — disposable microVM sandboxes for AI agents. Published as **`@tenkicloud/mcp`**, matching the `@tenkicloud/sandbox` SDK. Install with `npx -y @tenkicloud/mcp`; the command it provides is `tenki-mcp`.
|
|
53
|
+
|
|
54
|
+
### Tools
|
|
55
|
+
|
|
56
|
+
- **85 tools covering the full Tenki unary API**, enforced by a CI parity audit (`scripts/parity-audit.mjs` fails the build if any SandboxService / DataPlane / SSHGateway method lacks a tool): sandbox lifecycle, code execution (`tenki_exec`, `tenki_run_code`), files, git, ports and preview URLs, snapshots, volumes, templates, SSH, artifacts, and workspace administration.
|
|
57
|
+
- **`tenki_git`** validates `operation` as an enum of what the API actually supports (`clone`, `checkout`, `diff`, `log`) with per-operation arg keys documented; other git commands go through `tenki_exec`.
|
|
58
|
+
- **`tenki_exec` returns structured output**: it declares an `outputSchema` and returns `structuredContent` (`stdout`/`stderr`/`exitCode`/`ok`) alongside a human-readable rendering in which control characters, bidi overrides, and zero-width marks are escaped to visible `\xNN`/`\uNNNN` — sandbox output is untrusted.
|
|
59
|
+
- **`tenki_auth_status`** reports whether a usable credential is configured, which kind (`api_key`, `oauth_session_token`, or `session_cookie`), which env var it came from, the endpoint targeted, and whether a live identity probe succeeded — without ever returning the token. The server boots without a credential with this as the only registered tool, so a misconfigured client can ask what is wrong instead of seeing an opaque "server failed to start".
|
|
60
|
+
- Shared, described input schemas across modules: session ids (trimmed, non-empty), ports (1–65535), preview slugs (matching the server's own validation), and file paths (blank rejected client-side, otherwise sent verbatim — no silent trimming of legal POSIX filenames).
|
|
61
|
+
|
|
62
|
+
### Transports
|
|
63
|
+
|
|
64
|
+
- **stdio** (default) and **Streamable HTTP** (`TENKI_MCP_TRANSPORT=http PORT=3000`), built from a shared `createServer()` factory.
|
|
65
|
+
- The HTTP transport is hardened: loopback-only by default (`TENKI_MCP_HTTP_HOST` to expose), bearer auth via `TENKI_MCP_HTTP_TOKEN` (constant-time checked; required to bind a non-loopback host), DNS-rebinding protection via a Host-header allowlist, a 1 MiB request-body cap enforced on both the header and the streamed byte count, session count cap with idle reaping, and SIGTERM/SIGINT graceful shutdown.
|
|
66
|
+
|
|
67
|
+
### Security posture
|
|
68
|
+
|
|
69
|
+
- **Tool annotations on every tool** (`readOnlyHint`, `destructiveHint`, `openWorldHint`) so clients can surface or gate dangerous tools.
|
|
70
|
+
- **Least-privilege env controls:** `TENKI_MCP_READONLY=1` registers only read tools (plus `tenki_auth_status`); `TENKI_MCP_DISABLED_TOOLS=a,b` drops named tools; `TENKI_MCP_AUDIT=1` logs each tool call's name and arg keys — never values, content, or tokens — to stderr. Applied centrally via a registration guard covering both SDK registration APIs.
|
|
71
|
+
- **SECURITY.md** documents the threat model, trust boundaries (the key and endpoint are capabilities; sandbox output is untrusted), and a CSA MCP Server Top-10 mapping.
|
|
72
|
+
- Importing the package cannot execute the server or exit the host process: the package is bin-only.
|
|
73
|
+
|
|
74
|
+
### Network layer
|
|
75
|
+
|
|
76
|
+
- **Every request carries a timeout** with one shared deadline per call (retries and backoff draw from it): 30s for unary calls (`TENKI_MCP_TIMEOUT_MS`), 600s for RPCs that block on storage work — snapshot creation and pause (`TENKI_MCP_SLOW_TIMEOUT_MS`) — and command execution follows the command's own timeout plus a 30s margin.
|
|
77
|
+
- **Retry policy shaped by double-apply risk:** methods that must never run twice (creates, builds, publishes, resumes, extends, exec) never retry transient failures; reads and idempotent teardown retry `unavailable`, gateway-shaped 502/503/504, and transport-level failures, with rate limits retried for every method under jittered backoff honoring `Retry-After`. Read-shaped data-plane methods retry under the same rules.
|
|
78
|
+
- **Session-credential cache** with expiry-aware refresh, single-flight minting, and invalidate-and-retry-once on a stale certificate (`permission_denied` deliberately does not re-mint).
|
|
79
|
+
|
|
80
|
+
### Client
|
|
81
|
+
|
|
82
|
+
- Dependency-free ConnectRPC client for the control and data planes; runtime dependencies are only `@modelcontextprotocol/sdk` and `zod`. Node ≥ 22.
|
|
83
|
+
- Offline regression suites (security, transport input handling, exec output, network behavior — local stubs, zero external network) run in CI via `npm test`; live end-to-end suites in `test/run.mjs`.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tenki Cloud
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,222 @@
|
|
|
1
|
-
#
|
|
1
|
+
# tenki-mcp — `@tenkicloud/mcp`
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
**A [Model Context Protocol](https://modelcontextprotocol.io) server for [Tenki Cloud](https://tenki.cloud).** Give any agent — Claude, Codex, Cursor — a disposable microVM it can create, run code in, read and write files, run git, and expose to the web. Sandboxes boot in ~2 seconds and are billed per second.
|
|
4
|
+
|
|
5
|
+
Part of making Tenki the execution layer coding agents reach for: the agent writes code and Tenki runs it in isolation. (Tenki's **Code Reviewer** and **Runners** — AI PR review and managed CI — are separate products; this server currently exposes the Sandbox.)
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
"Run this Python in a fresh sandbox and tell me what it prints."
|
|
9
|
+
│
|
|
10
|
+
▼ tenki_run_code
|
|
11
|
+
boots a microVM → runs it → returns stdout → tears it down
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Quickstart
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
export TENKI_API_KEY=tk_your_key_here
|
|
18
|
+
npx -y @tenkicloud/mcp # speaks MCP over stdio
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Nothing to clone or build. The package installs one command, `tenki-mcp`.
|
|
22
|
+
|
|
23
|
+
### Use it in Claude Code
|
|
24
|
+
|
|
25
|
+
Connect to Tenki's hosted MCP service and sign in with your Tenki account:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
claude mcp add --transport http tenki https://mcp.tenki.cloud/mcp
|
|
29
|
+
claude mcp login tenki
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Claude Code's `mcp add` command stores the server configuration; `mcp login` performs the one-time browser authorization and workspace selection.
|
|
33
|
+
|
|
34
|
+
Alternatively, run the MCP server locally with an API key:
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
claude mcp add tenki --env TENKI_API_KEY=tk_your_key_here -- npx -y @tenkicloud/mcp
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Or install it as a Claude Code plugin
|
|
41
|
+
|
|
42
|
+
The repo doubles as a plugin marketplace. This prompts for your API key on install and stores it in your OS keychain — no env var to manage:
|
|
43
|
+
|
|
44
|
+
```
|
|
45
|
+
/plugin marketplace add LuxorLabs/tenki-mcp
|
|
46
|
+
/plugin install tenki@tenki
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Use it in Claude Desktop
|
|
50
|
+
|
|
51
|
+
Add to `claude_desktop_config.json`:
|
|
52
|
+
|
|
53
|
+
```json
|
|
54
|
+
{
|
|
55
|
+
"mcpServers": {
|
|
56
|
+
"tenki": {
|
|
57
|
+
"command": "npx",
|
|
58
|
+
"args": ["-y", "@tenkicloud/mcp"],
|
|
59
|
+
"env": { "TENKI_API_KEY": "tk_your_key_here" }
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Use it in Cursor
|
|
66
|
+
|
|
67
|
+
Add the same block to `.cursor/mcp.json` (project) or `~/.cursor/mcp.json` (global):
|
|
68
|
+
|
|
69
|
+
```json
|
|
70
|
+
{
|
|
71
|
+
"mcpServers": {
|
|
72
|
+
"tenki": {
|
|
73
|
+
"command": "npx",
|
|
74
|
+
"args": ["-y", "@tenkicloud/mcp"],
|
|
75
|
+
"env": { "TENKI_API_KEY": "tk_your_key_here" }
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Use it in Codex
|
|
82
|
+
|
|
83
|
+
The OpenAI Codex CLI reads MCP servers from `~/.codex/config.toml`. Add:
|
|
84
|
+
|
|
85
|
+
```toml
|
|
86
|
+
[mcp_servers.tenki]
|
|
87
|
+
command = "npx"
|
|
88
|
+
args = ["-y", "@tenkicloud/mcp"]
|
|
89
|
+
env = { TENKI_API_KEY = "tk_your_key_here" }
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Confirm it's working
|
|
93
|
+
|
|
94
|
+
After adding the server, **start a fresh session** so the client loads it, then ask your agent one of:
|
|
95
|
+
|
|
96
|
+
- *"Check tenki auth status."* — confirms your credential was picked up.
|
|
97
|
+
- *"Run `print(2+2)` in a fresh Tenki sandbox."* — a full boot → run → teardown round-trip.
|
|
98
|
+
|
|
99
|
+
If the `tenki_*` tools don't show up, restart the client so it re-reads its config.
|
|
100
|
+
|
|
101
|
+
No key yet? Start the server without one and ask the agent to check `tenki_auth_status` — it reports what to set and where. See [Auth](#auth).
|
|
102
|
+
|
|
103
|
+
### Run it from a clone instead
|
|
104
|
+
|
|
105
|
+
For development, or to run an unreleased change:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
git clone https://github.com/LuxorLabs/tenki-mcp.git && cd tenki-mcp
|
|
109
|
+
npm install && npm run build
|
|
110
|
+
TENKI_API_KEY=tk_your_key_here node dist/index.js
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Substitute `node /absolute/path/to/tenki-mcp/dist/index.js` for the `npx` command in any of the configs above. See [CONTRIBUTING.md](CONTRIBUTING.md) for the full development loop.
|
|
114
|
+
|
|
115
|
+
### Environment variables
|
|
116
|
+
|
|
117
|
+
| Variable | Default | Purpose |
|
|
118
|
+
|---|---|---|
|
|
119
|
+
| `TENKI_API_KEY` | — | API key (`tk_…`). One of this or `TENKI_AUTH_TOKEN` is required. |
|
|
120
|
+
| `TENKI_AUTH_TOKEN` | — | Session token (`ory_st_…` or cookie value). **Takes precedence over `TENKI_API_KEY`** when both are set. |
|
|
121
|
+
| `TENKI_API_ENDPOINT` | `https://api.tenki.cloud` | Control-plane base URL (`TENKI_API_URL` is an alias). |
|
|
122
|
+
| `TENKI_MCP_READONLY` | off | `1` registers only read tools (no create/run/delete/spend). |
|
|
123
|
+
| `TENKI_MCP_DISABLED_TOOLS` | — | Comma-separated tool names to skip registering. |
|
|
124
|
+
| `TENKI_MCP_AUDIT` | off | `1` logs each tool call name + arg keys to stderr. |
|
|
125
|
+
| `TENKI_MCP_TRANSPORT` | `stdio` | `http` serves Streamable HTTP instead (see below). |
|
|
126
|
+
| `PORT` | `3000` | HTTP transport port. |
|
|
127
|
+
| `TENKI_MCP_HTTP_HOST` | `127.0.0.1` | HTTP bind host; non-loopback requires `TENKI_MCP_HTTP_TOKEN`. |
|
|
128
|
+
| `TENKI_MCP_HTTP_TOKEN` | — | Bearer token for the HTTP endpoint; optional on loopback, required on a non-loopback host. |
|
|
129
|
+
| `TENKI_MCP_PUBLIC_URL` | — | Public base URL for an OAuth-protected hosted server. |
|
|
130
|
+
| `TENKI_MCP_OAUTH_ISSUER` | — | OAuth authorization-server issuer. Enables delegated OAuth HTTP mode. |
|
|
131
|
+
| `TENKI_MCP_OAUTH_RESOURCE` | `<public URL>/mcp` | RFC 8707 resource identifier accepted in access-token audiences. A comma-separated list serves one resource per domain; metadata returns the one matching the request host, else the first. |
|
|
132
|
+
| `TENKI_MCP_OAUTH_SCOPE` | `mcp` | Required delegated scope. |
|
|
133
|
+
| `TENKI_MCP_IDENTITY_URL` | — | Internal Tenki Identity service endpoint used to exchange OAuth access tokens. |
|
|
134
|
+
| `TENKI_MCP_IDENTITY_SERVICE_TOKEN` | — | Service credential for the private Identity token-exchange RPC. |
|
|
135
|
+
|
|
136
|
+
Every `TENKI_*` variable above also has a `LUXORAI_*` equivalent (for example `LUXORAI_API_KEY`, `LUXORAI_MCP_TRANSPORT`). All `LUXORAI_*` credentials are read before any `TENKI_*` one, and `lx_…` API keys are accepted alongside `tk_…`.
|
|
137
|
+
|
|
138
|
+
## Tools
|
|
139
|
+
|
|
140
|
+
**82 tools** — all 76 tool-worthy public unary API methods (enforced by a CI [parity audit](scripts/parity-audit.mjs), which also fails if a tool calls a method the API no longer has; `GitOperation` is exposed as one tool per operation), two workflow helpers, and `tenki_auth_status`. A few admin surfaces (published images, saved Tailscale connections) and implementation-only control-plane methods are intentionally excluded. Grouped by domain:
|
|
141
|
+
|
|
142
|
+
| Domain | Tools |
|
|
143
|
+
|---|---|
|
|
144
|
+
| **Auth** | `tenki_auth_status` (which credential is configured, and does it work — the only tool available when none is) |
|
|
145
|
+
| **Identity** | `tenki_whoami` |
|
|
146
|
+
| **Run** | `tenki_run_code` (one-shot: boot → run shell/python/js → tear down) |
|
|
147
|
+
| **Sandboxes** | `tenki_create_sandbox` · `tenki_get_sandbox` · `tenki_get_sandbox_metrics` · `tenki_list_sandboxes` · `tenki_terminate_sandbox` · `tenki_pause_sandbox` · `tenki_resume_sandbox` · `tenki_fork_sandbox` (copies of a running sandbox) |
|
|
148
|
+
| **Session admin** | `tenki_extend_sandbox` · `tenki_update_sandbox` · `tenki_terminate_sandboxes` (bulk) · `tenki_report_sandbox_activity` · `tenki_list_workspace_sandboxes` |
|
|
149
|
+
| **Exec** | `tenki_exec` (stdout/stderr/exit inline) |
|
|
150
|
+
| **Files** | `tenki_read_file` · `tenki_write_file` · `tenki_list_files` · `tenki_stat_path` · `tenki_make_dir` · `tenki_remove_path` · `tenki_move_path` |
|
|
151
|
+
| **Git** | `tenki_git_clone` · `tenki_git_checkout` · `tenki_git_diff` · `tenki_git_log` (the four operations the API supports; pass `directory` to target a cloned repo; run other git commands via `tenki_exec`) |
|
|
152
|
+
| **Ports & previews** | expose · list-exposed · unexpose · create-preview-url · open-preview (viewer-token-gated, not public; port 7681 is the web terminal) · list/get/delete-preview-url · touch-preview · bind/unbind-preview-url · resolve-preview-token |
|
|
153
|
+
| **Artifacts** (binary transfer) | `tenki_get_upload_url` · `tenki_get_download_url` (signed URLs for binary PUT/GET) |
|
|
154
|
+
| **SSH** | `tenki_update_ssh_keys` · `tenki_issue_ssh_cert` · `tenki_list_ssh_gateways` |
|
|
155
|
+
| **Snapshots** | create · get · list · list-session · list-workspace · list-dangling · update · delete · get-download-url |
|
|
156
|
+
| **Volumes** | create · get · list · update · delete · resize · attach · detach |
|
|
157
|
+
| **Egress policies** | create · get · list · update · delete (named workspace allowlists that `tenki_create_sandbox` pins via `egress_policies`) |
|
|
158
|
+
| **Templates** | create · get · list · update · delete · build · cancel-build · get-build · list-active-builds · list-builds · delete-build |
|
|
159
|
+
| **Workspace** | `tenki_get_workspace_usage` · `tenki_list_ci_runner_sizes` · `tenki_get_workspace_preview_domains` · `tenki_update_workspace_preview_domains` |
|
|
160
|
+
|
|
161
|
+
Full per-release breakdown in [CHANGELOG.md](CHANGELOG.md).
|
|
162
|
+
|
|
163
|
+
**Deprecated upstream, still served:** `tenki_expose_port` / `tenki_unexpose_port` / `tenki_list_exposed_ports` (the API prefers the preview-URL tools), `tenki_list_workspace_sandboxes` / `tenki_list_workspace_snapshots` (prefer the credential-scoped lists), and the `ExecuteCommand` RPC behind `tenki_exec` / `tenki_run_code` / `tenki_move_path` (successor is the data-plane `Run` stream, which needs an HTTP/2 transport this server does not have yet). The parity audit prints these on every run.
|
|
164
|
+
|
|
165
|
+
## Auth
|
|
166
|
+
|
|
167
|
+
Set one of `TENKI_API_KEY` or `TENKI_AUTH_TOKEN` — when both are set, `TENKI_AUTH_TOKEN` wins. The header is chosen by token prefix: `tk_…` → `Authorization: Bearer`, `ory_st_…` → `X-Session-Token`, otherwise a session cookie. Override the endpoint with `TENKI_API_ENDPOINT` (default `https://api.tenki.cloud`).
|
|
168
|
+
|
|
169
|
+
**Without a credential the server still starts**, registering only `tenki_auth_status` — so instead of an MCP client reporting an opaque "server failed to start", the agent can call that tool and get told what to set. Ask it "check tenki auth status" any time other tools return auth errors: it reports the credential kind (API key vs session token), the endpoint, and whether a live identity probe succeeded — never the token itself. It reports status only; get a credential with `tenki login` or from the dashboard.
|
|
170
|
+
|
|
171
|
+
## Host it over HTTP (v2.0-beta)
|
|
172
|
+
|
|
173
|
+
Besides stdio, the server speaks **Streamable HTTP** so it can be hosted for remote MCP clients:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
TENKI_MCP_TRANSPORT=http PORT=3000 TENKI_API_KEY=… npx -y @tenkicloud/mcp
|
|
177
|
+
# → tenki-mcp running on http://127.0.0.1:3000/mcp (Streamable HTTP) [loopback only, no auth]
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
For a single-user deployment, HTTP mode can hold one shared `TENKI_API_KEY` and therefore exposes a powerful capability. By default it:
|
|
181
|
+
|
|
182
|
+
- **binds to loopback (`127.0.0.1`) only** — set `TENKI_MCP_HTTP_HOST=0.0.0.0` to expose it, but then
|
|
183
|
+
- it **requires a bearer token**: set `TENKI_MCP_HTTP_TOKEN` and send `Authorization: Bearer <token>`. It **refuses to start** on a non-loopback host without one.
|
|
184
|
+
- **DNS-rebinding protection** is on (Host-header allowlist), so a malicious web page can't drive your local server.
|
|
185
|
+
|
|
186
|
+
```bash
|
|
187
|
+
# expose to a network safely:
|
|
188
|
+
TENKI_MCP_TRANSPORT=http TENKI_MCP_HTTP_HOST=0.0.0.0 PORT=3000 \
|
|
189
|
+
TENKI_MCP_HTTP_TOKEN=$(openssl rand -hex 32) TENKI_API_KEY=… npx -y @tenkicloud/mcp
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
Point an HTTP-capable MCP client at `/mcp`. Static-key mode uses one shared `TENKI_API_KEY` for all sessions. Verified end-to-end (`test/http-transport.test.mjs`: auth gate, DNS-rebinding rejection, connect → tools/list → tool call over HTTP).
|
|
193
|
+
|
|
194
|
+
Hosted multi-tenant deployments instead use Tenki Identity's OAuth facade. The MCP server publishes RFC 9728 protected-resource metadata and exchanges each caller's access token through the private Identity service. Identity owns Hydra, validates the requested audience and scope, and issues a short-lived API delegation bound to the user, client, and workspace selected on the Tenki consent page. `tenki-mcp` has no Hydra Admin access and does not hold the delegation-signing secret.
|
|
195
|
+
|
|
196
|
+
## How it works
|
|
197
|
+
|
|
198
|
+
Tenki's API is **ConnectRPC** — JSON over HTTP/1.1, not REST. Every control-plane call is `POST https://api.tenki.cloud/tenki.sandbox.v1.SandboxService/{Method}` with a lowerCamelCase JSON body. Per-session file I/O runs on a **separate data-plane endpoint** returned at create time, authenticated with a short-lived session certificate. This server owns both transports so the tools stay one-liners.
|
|
199
|
+
|
|
200
|
+
Command output: `tenki_exec` and `tenki_run_code` capture `stdout`/`stderr` by redirecting to files (`sh -c '… > out 2> err'`) and reading them back over the data plane, so you get the output inline through a plain HTTP client.
|
|
201
|
+
|
|
202
|
+
The wire details are ported from the live-verified [n8n community node](https://github.com/opencolin/n8n-nodes-tenki).
|
|
203
|
+
|
|
204
|
+
## Security
|
|
205
|
+
|
|
206
|
+
This server holds a Tenki API key and can run code + spend credits, so treat it as a capability. Full model + [CSA MCP Server Top-10](https://modelcontextprotocol-security.io/top10/server/) mapping in **[SECURITY.md](SECURITY.md)**. Quick controls:
|
|
207
|
+
|
|
208
|
+
- **Least privilege:** every tool carries a title and MCP annotations: `readOnlyHint` on reads, and `destructiveHint` on every tool that can overwrite, modify, interrupt or delete existing state (only tools that create, resume, extend, attach or keep alive without changing existing state are non-destructive). Run `TENKI_MCP_READONLY=1` for an inspection-only server (read tools only), or `TENKI_MCP_DISABLED_TOOLS=tenki_run_code,…` to drop specific tools.
|
|
209
|
+
- **HTTP transport** is loopback-only by default and requires a bearer token to expose to a network (see [Host it over HTTP](#host-it-over-http-v20-beta)).
|
|
210
|
+
- **Audit:** `TENKI_MCP_AUDIT=1` logs each tool call's name to stderr.
|
|
211
|
+
- **Untrusted output:** `tenki_run_code`/`tenki_exec`/`tenki_read_file` return output from untrusted code — clients should treat tool results as data, not instructions.
|
|
212
|
+
|
|
213
|
+
Report vulnerabilities via a private [security advisory](https://github.com/LuxorLabs/tenki-mcp/security/advisories/new), not a public issue.
|
|
214
|
+
|
|
215
|
+
## Related
|
|
216
|
+
|
|
217
|
+
- **Tenki Sandbox** — the platform: https://tenki.cloud
|
|
218
|
+
- **n8n-nodes-tenki** — Tenki as an n8n node: https://github.com/opencolin/n8n-nodes-tenki
|
|
219
|
+
|
|
220
|
+
## License
|
|
221
|
+
|
|
222
|
+
MIT
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# Security
|
|
2
|
+
|
|
3
|
+
`tenki-mcp` gives an AI agent a capability: a disposable microVM it can create, run code in, and spend Tenki credits with. Treat the server and its credentials accordingly. This document is the threat model and the controls, mapped to the [CSA MCP Server Top 10](https://modelcontextprotocol-security.io/top10/server/).
|
|
4
|
+
|
|
5
|
+
## Reporting a vulnerability
|
|
6
|
+
Please **do not** open a public issue for security reports. Open a private [GitHub security advisory](https://github.com/LuxorLabs/tenki-mcp/security/advisories/new), or email **hello@luxor.tech**. We'll acknowledge within a few business days.
|
|
7
|
+
|
|
8
|
+
## Trust boundaries (read this first)
|
|
9
|
+
- **Credentials are capabilities.** Local and single-user modes authenticate with a `TENKI_API_KEY` or session token. Hosted OAuth mode exchanges each caller's token through Tenki Identity and receives only a short-lived, workspace-bound internal delegation to the Tenki API. Never commit credentials (`.env` is gitignored).
|
|
10
|
+
- **Sandbox output is untrusted.** `tenki_run_code` / `tenki_exec` / `tenki_read_file` return output produced by *untrusted, AI-generated code running in the sandbox*. That output flows back to the calling model as a tool result — a classic **indirect / output prompt-injection** vector. The microVM is the isolation boundary; the model should treat tool results as **data, not instructions**. (MCP clients are responsible for not executing instructions found in tool output.)
|
|
11
|
+
- **The HTTP endpoint is a capability.** In static-key HTTP mode, access to `/mcp` is equivalent to access to the configured key. Hosted mode requires a valid OAuth bearer on every request and binds the MCP session to its user, client, and workspace.
|
|
12
|
+
|
|
13
|
+
## Controls this server provides
|
|
14
|
+
|
|
15
|
+
### Least privilege (MCP-07 — the main lever)
|
|
16
|
+
Every tool has a human-readable **title** and MCP **annotations** so clients can surface/gate danger:
|
|
17
|
+
`readOnlyHint` on the 35 inspection tools; `destructiveHint` on the 30 that can overwrite, modify, interrupt or delete existing state (delete/terminate/detach, but also `tenki_exec`, `tenki_write_file`, `tenki_move_path`, `tenki_git_checkout`, every `update` tool, `tenki_pause_sandbox`, `tenki_build_template` (a named build repoints `workspace/name`), and `tenki_get_upload_url`, whose signed PUT can overwrite a file); neither on the 17 additive tools that only add, never change or remove what exists: create (sandboxes, forks, clones, snapshots, volumes, templates, egress policies, preview URLs, port exposures, directories, SSH certificates), resume, extend, attach a volume, keep-alive; `openWorldHint` on all (they call an external API). Destructive is the default: a new tool is non-destructive only once it is listed as additive in `src/server.ts`. Plus two env controls:
|
|
18
|
+
|
|
19
|
+
| Env | Effect |
|
|
20
|
+
|---|---|
|
|
21
|
+
| `TENKI_MCP_READONLY=1` | Register **only read-only tools** — no create, run, spend, or delete. For inspection-only deployments. |
|
|
22
|
+
| `TENKI_MCP_DISABLED_TOOLS=a,b,c` | Skip the named tools (e.g. disable `tenki_run_code` where code execution isn't wanted). |
|
|
23
|
+
|
|
24
|
+
Grant the smallest set that the use case needs.
|
|
25
|
+
|
|
26
|
+
### Transport security (MCP-05, MCP-10)
|
|
27
|
+
- **stdio** (default) has no network surface.
|
|
28
|
+
- **HTTP** (`TENKI_MCP_TRANSPORT=http`) binds **loopback-only** by default, **requires a bearer token** to bind to a non-loopback host, has **DNS-rebinding protection**, and caps sessions + body size. For network exposure put it behind a **TLS-terminating proxy**. (Hardening details in the transport module, `src/http.ts`.)
|
|
29
|
+
|
|
30
|
+
### Secrets & audit (MCP-04, Observability)
|
|
31
|
+
- Static credentials are read from env and sent only as auth headers. Hosted OAuth tokens are sent only to the private Tenki Identity exchange endpoint and are never forwarded to the Tenki API; the API receives a short-lived signed delegation instead.
|
|
32
|
+
- `TENKI_MCP_AUDIT=1` logs each tool call's **name + argument keys** to stderr (never values, content, or the token) for an operator audit trail.
|
|
33
|
+
|
|
34
|
+
## CSA MCP Server Top-10 mapping
|
|
35
|
+
|
|
36
|
+
| # | Risk | tenki-mcp posture |
|
|
37
|
+
|---|---|---|
|
|
38
|
+
| MCP-01 | Prompt Injection | zod-validates every tool arg pre-network; **sandbox output is untrusted** (treat tool results as data) |
|
|
39
|
+
| MCP-02 | Confused Deputy | static mode requires its own HTTP bearer; hosted mode authenticates every request and binds the session and API delegation to the authorized user, client, and workspace; `READONLY`/denylist further bound the blast radius |
|
|
40
|
+
| MCP-03 | Tool Poisoning | tool descriptions are static and authored (no dynamic/remote descriptions); verify the package via its npm provenance attestation (published from GitHub Actions, linking each release to its source commit) + MCP-registry namespace ownership |
|
|
41
|
+
| MCP-04 | Credential/Token Exposure | credentials are never logged/committed/echoed; hosted OAuth tokens go only to Tenki Identity and only short-lived internal delegations reach the API; audit logs keys not values |
|
|
42
|
+
| MCP-05 | Insecure Configuration | HTTP transport is loopback + token + DNS-rebinding-protected + DoS-capped by default |
|
|
43
|
+
| MCP-06 | Supply Chain | 2 direct runtime deps (`@modelcontextprotocol/sdk`, `zod`) — ~91 transitive, nearly all via the MCP SDK; lockfile committed; released only from CI with npm provenance, Actions pinned to commit SHAs |
|
|
44
|
+
| MCP-07 | Excessive Permissions | tool annotations + `TENKI_MCP_READONLY` + `TENKI_MCP_DISABLED_TOOLS` |
|
|
45
|
+
| MCP-08 | Data Exfiltration | microVM isolation; sandbox **outbound networking is off unless `allow_outbound` is set**; the server itself stores/forwards nothing |
|
|
46
|
+
| MCP-09 | Context Spoofing | tool results are raw API/sandbox output surfaced as data, not merged into instructions |
|
|
47
|
+
| MCP-10 | Insecure Communication | control plane is HTTPS; run the HTTP transport behind TLS for any non-loopback use |
|
|
48
|
+
|
|
49
|
+
## Cost / resource notes
|
|
50
|
+
`tenki_run_code` is cost-guarded (1 vCPU, 1 GB, 10-min cap, 5-min idle) and self-terminates. `tenki_create_sandbox` and other create tools **spend real credits** — bound them with your **Tenki workspace limits**, `TENKI_MCP_READONLY`/denylist, and `TENKI_MCP_AUDIT` for visibility.
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TenkiClient — a dependency-free client for the Tenki Cloud API.
|
|
3
|
+
*
|
|
4
|
+
* Tenki's API is ConnectRPC (JSON over HTTP/1.1), not REST. Every control-plane
|
|
5
|
+
* call is `POST {baseUrl}/tenki.sandbox.v1.SandboxService/{Method}` with a
|
|
6
|
+
* lowerCamelCase JSON body and a JSON response. Per-session file I/O runs on a
|
|
7
|
+
* separate data-plane endpoint returned at session-create time, authenticated
|
|
8
|
+
* with a short-lived session certificate.
|
|
9
|
+
*
|
|
10
|
+
* The wire details here (headers, the control/data-plane split, and capturing
|
|
11
|
+
* command stdout/stderr via an `sh -c` redirect + data-plane ReadFile) are
|
|
12
|
+
* ported from the live-verified n8n community node
|
|
13
|
+
* (github.com/opencolin/n8n-nodes-tenki).
|
|
14
|
+
*/
|
|
15
|
+
/**
|
|
16
|
+
* True when the call ended on THIS side — the client's deadline or an MCP-side
|
|
17
|
+
* cancel — so the server never answered and the operation may still complete.
|
|
18
|
+
* A server error never carries the marker, even when its message says "timed out".
|
|
19
|
+
*/
|
|
20
|
+
export declare function isClientAbort(e: unknown): boolean;
|
|
21
|
+
export type Language = "shell" | "python" | "javascript";
|
|
22
|
+
export interface ExecResult {
|
|
23
|
+
command: string;
|
|
24
|
+
args: string[];
|
|
25
|
+
stdout: string;
|
|
26
|
+
stderr: string;
|
|
27
|
+
exitCode: number;
|
|
28
|
+
ok: boolean;
|
|
29
|
+
captureError?: string;
|
|
30
|
+
stdoutTruncated?: boolean;
|
|
31
|
+
stderrTruncated?: boolean;
|
|
32
|
+
/** Sandbox path holding the FULL stdout, present only when stdout was truncated. */
|
|
33
|
+
stdoutPath?: string;
|
|
34
|
+
/** Sandbox path holding the FULL stderr, present only when stderr was truncated. */
|
|
35
|
+
stderrPath?: string;
|
|
36
|
+
}
|
|
37
|
+
export interface TenkiClientOptions {
|
|
38
|
+
/** Timeout for unary control/data-plane calls (default 30s). */
|
|
39
|
+
timeoutMs?: number;
|
|
40
|
+
/** Timeout for ExecuteCommand when the command itself has no timeout (default 630s). */
|
|
41
|
+
execTimeoutMs?: number;
|
|
42
|
+
/** Timeout for long storage/VM operations — see SLOW_METHOD (default 600s). */
|
|
43
|
+
slowTimeoutMs?: number;
|
|
44
|
+
/** Assumed session-credential lifetime when the API returns no parseable expiry (default 5min). */
|
|
45
|
+
credTtlMs?: number;
|
|
46
|
+
/** Workspace selected by an upstream delegated authorization grant. */
|
|
47
|
+
workspaceId?: string;
|
|
48
|
+
/** Supplies a short-lived Bearer credential for hosted delegated calls. */
|
|
49
|
+
bearerTokenProvider?: () => string;
|
|
50
|
+
}
|
|
51
|
+
export declare class TenkiClient {
|
|
52
|
+
private readonly token;
|
|
53
|
+
private readonly baseUrl;
|
|
54
|
+
private readonly credCache;
|
|
55
|
+
private readonly credInflight;
|
|
56
|
+
private readonly timeoutMs;
|
|
57
|
+
private readonly execTimeoutMs;
|
|
58
|
+
private readonly slowTimeoutMs;
|
|
59
|
+
private readonly credTtlMs;
|
|
60
|
+
private readonly workspaceId?;
|
|
61
|
+
private readonly bearerTokenProvider?;
|
|
62
|
+
constructor(token: string, baseUrl?: string, opts?: TenkiClientOptions);
|
|
63
|
+
private controlAuthHeaders;
|
|
64
|
+
/**
|
|
65
|
+
* ExecuteCommand blocks until the command finishes, so its timeout follows
|
|
66
|
+
* the command's own timeout (plus margin) instead of the unary default.
|
|
67
|
+
*/
|
|
68
|
+
private timeoutFor;
|
|
69
|
+
/**
|
|
70
|
+
* fetch + full body read under ONE timeout, with a readable error instead of
|
|
71
|
+
* a bare AbortError. Reading the body inside the guarded section matters: a
|
|
72
|
+
* response whose headers arrive but whose body stalls must fail with the
|
|
73
|
+
* same friendly, retryable timeout as a connection that never responds.
|
|
74
|
+
*/
|
|
75
|
+
private fetchTextWithTimeout;
|
|
76
|
+
/**
|
|
77
|
+
* Retry policy: rate limits always; `unavailable` and gateway-shaped
|
|
78
|
+
* 502/503/504 (load balancers and CDNs return these with an HTML or empty
|
|
79
|
+
* body, no ConnectRPC code) for every method that cannot double-apply.
|
|
80
|
+
* For a method that cannot double-apply, a Connect `resource_exhausted` (the
|
|
81
|
+
* handler's own quota or limit check; edge rate limits arrive as an uncoded
|
|
82
|
+
* 429) or any rejection carrying error details (a fork's partial snapshot and
|
|
83
|
+
* children) means the handler ran, so it is never retried.
|
|
84
|
+
*/
|
|
85
|
+
private shouldRetry;
|
|
86
|
+
/** Exponential backoff with half-jitter; a Retry-After header (seconds) wins, capped. */
|
|
87
|
+
private backoffMs;
|
|
88
|
+
/**
|
|
89
|
+
* Unary control-plane call. The WHOLE call — every attempt plus every
|
|
90
|
+
* backoff sleep — draws from one shared deadline (the method's timeout), so
|
|
91
|
+
* retries can never stack past it: an MCP client's own 60s default would
|
|
92
|
+
* otherwise give up before our per-attempt timeouts did. Rate-limit
|
|
93
|
+
* rejections retry with jittered backoff (honoring Retry-After); transient
|
|
94
|
+
* `unavailable`/gateway errors AND transport-level failures (timeout,
|
|
95
|
+
* connection reset, DNS) retry for every method that cannot double-apply.
|
|
96
|
+
* `service` defaults to SandboxService; pass another fully-qualified ConnectRPC
|
|
97
|
+
* service (e.g. the SSH gateway service) for methods hosted elsewhere.
|
|
98
|
+
* `cancel` (the MCP request's abort signal) drops the HTTP request when the
|
|
99
|
+
* MCP client gives up, so the API sees the disconnect; a cancelled call is
|
|
100
|
+
* never retried.
|
|
101
|
+
*/
|
|
102
|
+
control(method: string, body?: Record<string, unknown>, service?: string, cancel?: AbortSignal): Promise<Record<string, any>>;
|
|
103
|
+
/**
|
|
104
|
+
* Mint (and cache per session) the data-plane endpoint + session certificate.
|
|
105
|
+
* A credential with no parseable expiry is cached for credTtlMs — never
|
|
106
|
+
* forever — and a known expiry is shortened by a skew so an in-flight call
|
|
107
|
+
* can't straddle the real expiry. Minting is single-flight: concurrent data
|
|
108
|
+
* calls on a cold cache share one mint instead of each paying a
|
|
109
|
+
* control-plane round trip for its own certificate.
|
|
110
|
+
*/
|
|
111
|
+
private credentialFor;
|
|
112
|
+
private mintCredential;
|
|
113
|
+
/**
|
|
114
|
+
* Unary data-plane call. The inner request is wrapped as
|
|
115
|
+
* `{ request: { sessionId, ...request } }`. This is the path behind every
|
|
116
|
+
* file tool AND how tenki_exec reads back its own output, so it gets the
|
|
117
|
+
* same treatment as the control plane: one shared deadline, rate limits
|
|
118
|
+
* retried for every method, transient/gateway/transport failures retried
|
|
119
|
+
* for read-shaped methods. A stale-certificate failure (401/unauthenticated
|
|
120
|
+
* — the cert may have expired server-side regardless of its stated expiry)
|
|
121
|
+
* invalidates the cache and re-mints once; permission_denied is NOT an auth
|
|
122
|
+
* failure — a denied path is a normal user error a fresh cert can never
|
|
123
|
+
* fix, so re-minting would waste a control-plane round trip per denied call.
|
|
124
|
+
*/
|
|
125
|
+
data(sessionId: string, method: string, request?: Record<string, unknown>): Promise<Record<string, any>>;
|
|
126
|
+
/**
|
|
127
|
+
* Resolve the calling identity + a default workspace for CreateSession and
|
|
128
|
+
* other workspace-scoped calls. (Projects were removed from the API — the
|
|
129
|
+
* proto reserves every project_id field and WhoAmI no longer lists projects —
|
|
130
|
+
* so there is nothing narrower than a workspace to resolve.)
|
|
131
|
+
*
|
|
132
|
+
* CreateSession validates owner_type ∈ {SERVICE, USER} and requires a
|
|
133
|
+
* non-empty owner_id, but derives the real owner from the authenticated
|
|
134
|
+
* identity server-side. WhoAmI can return other owner types (e.g. WORKSPACE
|
|
135
|
+
* for workspace-scoped keys), which the validator rejects — so for those we
|
|
136
|
+
* send the same placeholder the first-party SDKs hardcode ("SERVICE"/"self").
|
|
137
|
+
*/
|
|
138
|
+
resolveOwner(): Promise<{
|
|
139
|
+
ownerType?: string;
|
|
140
|
+
ownerId?: string;
|
|
141
|
+
workspaceId?: string;
|
|
142
|
+
}>;
|
|
143
|
+
/** Poll GetSession until it reaches (or passes into) the target state. */
|
|
144
|
+
waitForState(sessionId: string, target?: string, { timeoutMs, intervalMs }?: {
|
|
145
|
+
timeoutMs?: number;
|
|
146
|
+
intervalMs?: number;
|
|
147
|
+
}): Promise<Record<string, any>>;
|
|
148
|
+
/** A file's raw bytes. The data plane's ReadFile has no range support, so this is always the whole file. */
|
|
149
|
+
readFileBytes(sessionId: string, path: string): Promise<Buffer>;
|
|
150
|
+
readTextFile(sessionId: string, path: string): Promise<string>;
|
|
151
|
+
writeTextFile(sessionId: string, path: string, text: string): Promise<Record<string, any>>;
|
|
152
|
+
/**
|
|
153
|
+
* Read a capture file back, capping what crosses the wire. Stat first: a
|
|
154
|
+
* file at or under the cap is read whole; a larger one gets a server-side
|
|
155
|
+
* head+tail preview (assembled by `sh` in the sandbox, so the full body
|
|
156
|
+
* never leaves it — ReadFile has no range support). The preview file is the
|
|
157
|
+
* caller's to clean up (returned as previewPath); the ORIGINAL is theirs to
|
|
158
|
+
* keep, so the user can page through the full output afterwards.
|
|
159
|
+
*/
|
|
160
|
+
private readCaptureFile;
|
|
161
|
+
/**
|
|
162
|
+
* Run a command in a session and return stdout/stderr inline.
|
|
163
|
+
*
|
|
164
|
+
* We wrap the command in `sh -c '<cmd> > out 2> err'`, then read the capture
|
|
165
|
+
* files back over the data plane, so output is available through a plain-HTTP
|
|
166
|
+
* client. Capture-read failures degrade gracefully into `captureError` rather
|
|
167
|
+
* than losing the run — but the result is marked NOT ok, because empty
|
|
168
|
+
* stdout/stderr next to a zero exit code would otherwise read as a clean,
|
|
169
|
+
* silent success. Streams over maxOutputBytes come back truncated
|
|
170
|
+
* (head+tail) with the full capture file retained in the sandbox — see
|
|
171
|
+
* readCaptureFile; truncation does not affect `ok`.
|
|
172
|
+
*/
|
|
173
|
+
execCaptured(sessionId: string, command: string, opts?: {
|
|
174
|
+
args?: string[];
|
|
175
|
+
cwd?: string;
|
|
176
|
+
env?: Record<string, string>;
|
|
177
|
+
timeoutSeconds?: number;
|
|
178
|
+
maxOutputBytes?: number;
|
|
179
|
+
}): Promise<ExecResult>;
|
|
180
|
+
/**
|
|
181
|
+
* One-shot: boot a throwaway sandbox, run code (shell / python / javascript),
|
|
182
|
+
* return its output, and terminate the sandbox. Cost-guarded (1 vCPU, 1 GB,
|
|
183
|
+
* 10-min cap, 5-min idle) so an ephemeral run can never leak a billing session.
|
|
184
|
+
*/
|
|
185
|
+
runCode(language: Language, code: string, opts?: {
|
|
186
|
+
env?: Record<string, string>;
|
|
187
|
+
timeoutSeconds?: number;
|
|
188
|
+
}): Promise<ExecResult & {
|
|
189
|
+
sessionId: string;
|
|
190
|
+
language: Language;
|
|
191
|
+
}>;
|
|
192
|
+
}
|