@tenkicloud/mcp 0.1.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.
Files changed (51) hide show
  1. package/CHANGELOG.md +161 -0
  2. package/LICENSE +21 -0
  3. package/README.md +192 -0
  4. package/SECURITY.md +50 -0
  5. package/dist/client.d.ts +152 -0
  6. package/dist/client.js +499 -0
  7. package/dist/http.d.ts +19 -0
  8. package/dist/http.js +234 -0
  9. package/dist/index.d.ts +2 -0
  10. package/dist/index.js +59 -0
  11. package/dist/server.d.ts +30 -0
  12. package/dist/server.js +205 -0
  13. package/dist/tools/artifacts.d.ts +16 -0
  14. package/dist/tools/artifacts.js +19 -0
  15. package/dist/tools/auth_status.d.ts +35 -0
  16. package/dist/tools/auth_status.js +104 -0
  17. package/dist/tools/common.d.ts +33 -0
  18. package/dist/tools/common.js +42 -0
  19. package/dist/tools/exec.d.ts +4 -0
  20. package/dist/tools/exec.js +88 -0
  21. package/dist/tools/files.d.ts +4 -0
  22. package/dist/tools/files.js +22 -0
  23. package/dist/tools/files_ops.d.ts +12 -0
  24. package/dist/tools/files_ops.js +54 -0
  25. package/dist/tools/git.d.ts +4 -0
  26. package/dist/tools/git.js +30 -0
  27. package/dist/tools/identity.d.ts +4 -0
  28. package/dist/tools/identity.js +5 -0
  29. package/dist/tools/ports.d.ts +4 -0
  30. package/dist/tools/ports.js +6 -0
  31. package/dist/tools/previews.d.ts +18 -0
  32. package/dist/tools/previews.js +102 -0
  33. package/dist/tools/registry.d.ts +18 -0
  34. package/dist/tools/registry.js +98 -0
  35. package/dist/tools/run.d.ts +4 -0
  36. package/dist/tools/run.js +11 -0
  37. package/dist/tools/sandboxes.d.ts +4 -0
  38. package/dist/tools/sandboxes.js +66 -0
  39. package/dist/tools/sessions_admin.d.ts +9 -0
  40. package/dist/tools/sessions_admin.js +76 -0
  41. package/dist/tools/snapshots.d.ts +11 -0
  42. package/dist/tools/snapshots.js +91 -0
  43. package/dist/tools/ssh.d.ts +15 -0
  44. package/dist/tools/ssh.js +17 -0
  45. package/dist/tools/templates.d.ts +14 -0
  46. package/dist/tools/templates.js +151 -0
  47. package/dist/tools/volumes.d.ts +16 -0
  48. package/dist/tools/volumes.js +94 -0
  49. package/dist/tools/workspace.d.ts +4 -0
  50. package/dist/tools/workspace.js +129 -0
  51. package/package.json +61 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,161 @@
1
+ # Changelog
2
+
3
+ All notable changes to tenki-mcp. This project follows semantic versioning.
4
+
5
+ ## [0.1.0] — 2026-08-03 — First npm release: typed git, network hardening, structured output
6
+
7
+ Published as **`@tenkicloud/mcp`**, matching the `@tenkicloud/sandbox` SDK. Install with `npx -y @tenkicloud/mcp`; the command it provides is still `tenki-mcp`.
8
+
9
+ ### Fixed by an end-to-end sweep of all 85 tools against the live API
10
+
11
+ - **`tenki_create_snapshot` and `tenki_pause_sandbox` reported failure on operations that succeeded.** Both RPCs block until the underlying storage work finishes — measured at 59s and 41s — so the 30s default HTTP budget introduced with the timeout work cut them off. The caller got `timed out after 30000ms` while the snapshot (or a multi-GB pause snapshot) had in fact been created, leaving an orphaned resource whose id it never saw, and an agent that retried burned quota. Both now use a 600s budget, tunable with `TENKI_MCP_SLOW_TIMEOUT_MS` (`TENKI_MCP_TIMEOUT_MS` tunes the ordinary 30s one). Async methods that return a handle in under a second — `BuildTemplate`, `PublishRegistryImage` — deliberately keep the short budget.
12
+ - **`tenki_update_workspace_settings` and `tenki_update_snapshot_retention_settings` did nothing at all, and reported success.** Every field they sent (`defaultIdleTimeoutMinutes`, `defaultMaxDurationSeconds`, `snapshotRetentionDays`, `retentionDays`) is absent from the API; connect-go discards unknown fields, so the server returned 200 with the settings unchanged. They now send the real contract — `pauseRetention`/`snapshotRetention` as protobuf Durations with their `clear*` companions, plus the `max*` quotas — reject a call that would change nothing, and `clear_retention` makes "keep snapshots indefinitely" expressible for the first time. The retention pair is also documented as deprecated upstream in favour of the workspace-settings pair.
13
+ - **`tenki_list_files` hid every dotfile.** The data plane omits dot-prefixed entries unless asked, so `.git`, `.env` and `.gitignore` were invisible and an agent would conclude they did not exist. It now sends `include_hidden` (default true, overridable) and always returns an `entries` array instead of a bare `{}` for an empty directory.
14
+ - **`tenki_list_preview_urls` advertised a filter the API does not have.** `ListPreviewUrls` takes no session id, so the one this tool sent was silently dropped and every session's preview URLs came back as if filtered. The filter is now applied client-side and says so in the result; `page_size`, `page_token` and `workspace_id` — all real fields the tool never exposed — are now available, and the deprecated `project_id` is gone.
15
+ - **`tenki_publish_image`'s documented example could never work:** its own description showed `myws/myimage:latest`, and the API rejects any tag or `@snapshot` on publish. The tagless form is now enforced client-side with an explanatory message.
16
+ - **`tenki_unshare_image` silently revoked nothing** when called with neither `grant_id` nor `grantee_workspace_id`, returning the image object like a success. It now refuses, matching the guard `tenki_delete_image` already had.
17
+ - **`tenki_open_preview` hands back an unreachable URL** for any port but the web terminal (7681): the API deliberately drops the regional host for other ports and returns a fallback that has no edge route, so the URL 404s. The description now says so and points at `tenki_expose_port`/`tenki_create_preview_url`. Platform-side issue; documented rather than worked around.
18
+
19
+ ### A missing credential no longer looks like a broken server
20
+
21
+ - **New tool `tenki_auth_status`** (85 tools total) — reports whether a usable credential is configured, which kind (`api_key` for `tk_…`, `oauth_session_token` for `ory_st_…`, or `session_cookie`), which env var it came from, the endpoint targeted, and whether a live identity probe succeeded. Never returns the token. Structured output, annotated read-only, and available under `TENKI_MCP_READONLY` (it is how you diagnose a credential problem in that posture) — `TENKI_MCP_DISABLED_TOOLS` can still drop it.
22
+ - **The server starts without a credential instead of exiting 1.** Exiting made MCP clients report an opaque "server failed to start" while swallowing the stderr line that explained why, leaving the user with a broken server and no cause. It now boots with `tenki_auth_status` as the only registered tool, so an agent can ask what is wrong and relay the fix; the reported `toolsRegistered` count makes that degraded mode explicit. Registering all 84 API tools in that state would only offer tools that cannot work.
23
+ - Reporting status is the whole scope: nothing here logs in, opens a browser, or writes credentials. Get a credential with `tenki login` or from the dashboard.
24
+
25
+ ### Tool-schema DX (git enum, shared schemas, README)
26
+
27
+ - **`tenki_git` now validates `operation` as an enum of what the API actually supports: `clone`, `checkout`, `diff`, `log`.** The tool previously advertised ten operations, but six of them (status/add/commit/pull/push/fetchPR) are rejected by the API's own validation (`GitOperationRequest` allows exactly four) — a model following the old description was guaranteed a server error. Per-operation arg keys are now documented in the tool description, grounded in the engine's arg builder: clone `{repo, branch?, depth?, directory?}`, checkout `{ref, create?}`, diff `{range? | base?+head?, path?}`, log `{max_count?, range?, path?}`. Other git commands: use `tenki_exec` with `git ...`.
28
+ - **Shared input schemas** in `common.ts`: `sessionIdSchema` (described; trimmed and non-empty, so a whitespace-only id is rejected client-side — previously bare and undescribed in 8 of 14 modules) — reused across ports/previews/files/exec/ssh/snapshots/artifacts/sandboxes/volumes/sessions-admin, including the `session_ids` array in `tenki_terminate_sandboxes`; `portSchema` (1-65535 — `tenki_expose_port` previously accepted port 99999 and learned the constraint from a server error) — ports; `slugSchema` (3-63 chars, lowercase/digits/hyphens, no leading/trailing hyphen — matching the server's shared preview-slug validation, which `ExposePort` also applies) — ports/previews.
29
+ - `tenki_git` accepts numbers and booleans as arg values (`depth: 1`, `create: true`) and coerces them to the strings the wire expects, instead of rejecting values that would serialize identically.
30
+ - **Shared `pathSchema`** (trimmed, non-empty) across every file-path argument (read/write/list/stat/mkdir/remove/move/upload/mount) — an empty or whitespace-only path is rejected client-side instead of by a server error.
31
+ - **`tenki_git` description documents a live-verified caveat:** checkout/diff/log run in the session's working directory and cannot target a repo cloned into a subdirectory (the API has no directory arg for them) — use `tenki_exec` with `git -C <dir> ...` for those until the API grows one. `clone` works as documented.
32
+ - **README**: install forms for Claude Code (`claude mcp add`), Claude Desktop, and Cursor; a consolidated environment-variable table; auth precedence documented (`TENKI_AUTH_TOKEN` wins over `TENKI_API_KEY`); the Tools table no longer lists unsupported git operations.
33
+
34
+ ### Network-layer hardening (timeouts, retry policy, credential cache)
35
+
36
+ - **Every fetch now carries a timeout** — a hung control- or data-plane connection, or a response whose body stalls after the headers arrive, fails the tool call with a clear `timed out after Nms` error naming the method instead of blocking it forever. Unary calls default to 30s; `ExecuteCommand` follows the command's own timeout + 30s margin (630s when the command has none). Tunable via `TenkiClient` options. Each call has ONE shared deadline: retries and backoff sleeps draw from it, so a call can never stack attempts past its timeout.
37
+ - **Retry policy is shaped by double-apply risk, not by method naming.** Methods that must never run twice (`Create*`, `Build*`, `Publish*`, `Resume*`, `Extend*`, `ExecuteCommand`) never retry transient failures — a half-applied `CreateSession` retried could boot and bill a second sandbox. Everything else — reads AND teardown (`Terminate*`/`Delete*`/`Detach*`, where a repeat lands in the same state) — retries `unavailable`, gateway-shaped 502/503/504 (HTML/empty bodies from load balancers included), and transport-level failures (timeout, connection reset, DNS). Rate-limit rejections still retry for every method, with jittered backoff and `Retry-After` honored (capped at 30s).
38
+ - **The data plane retries too.** Read-shaped data-plane methods (`ReadFile`/`Stat`/`List*`) — the path behind every file tool and how `tenki_exec` reads back its own output — now retry rate limits, transient/gateway errors, and transport failures under the same shared deadline. Write-shaped data methods surface failures immediately.
39
+ - **Session-credential cache can no longer go permanently stale.** A credential whose expiry the API omits (or that fails to parse) was previously cached *forever* — once the cert actually expired, every file operation for that session failed for the life of the process. Now: missing expiry → 5-minute TTL; known expiry → refreshed 30s early (floored at 5s so a short-lived credential still caches; a credential already past its expiry is not cached at all); a stale-certificate failure (401/`unauthenticated`) invalidates the cached credential and retries once with a fresh one (never loops; `permission_denied` does NOT re-mint — a denied path is a user error a fresh cert can't fix); and minting is single-flight, so concurrent cold-cache calls share one mint.
40
+ - New offline regression suite `test/client-net.test.mjs` (local `node:http` stub, zero external network) covering all of the above; wired into `npm test` and CI.
41
+
42
+ ### Structured tool output (first tool: `tenki_exec`)
43
+
44
+ - **`tenki_exec` now declares an `outputSchema` and returns `structuredContent`**, so MCP clients can consume `stdout`/`stderr`/`exitCode`/`ok` as typed JSON instead of re-parsing a stringified blob. The SDK validates every successful result against the schema.
45
+ - **`tenki_exec` returns two text blocks:** the serialized JSON result first (the MCP spec's backwards-compatibility contract for structured content — existing text-parsing consumers keep working), then a human-readable rendering headed by `exit <code>` and character-count markers. Characters that can misrepresent output in a terminal — C0/C1 controls, bidi overrides, zero-width marks — are escaped to visible `\xNN`/`\uNNNN` in the rendering (sandbox output is untrusted); `structuredContent` keeps the raw strings.
46
+ - **Registration guard extended to `registerTool`** — the modern SDK registration API (required for `outputSchema`) now passes through the same least-privilege guard as the legacy `.tool()` form: name-derived annotations, `TENKI_MCP_READONLY`, `TENKI_MCP_DISABLED_TOOLS`, and `TENKI_MCP_AUDIT` all apply. Previously a module using `registerTool` would have silently bypassed all four.
47
+ - **Behavior change:** `tenki_exec` / `tenki_run_code` results now report `ok: false` when the command ran but its output could not be read back (`captureError` set). Previously a capture failure with exit code 0 reported `ok: true` with empty stdout/stderr, which read as a clean silent success. `tenki_move_path` (exec-backed) is unaffected: its `ok` tracks the `mv` exit code, with any `captureError` surfaced separately.
48
+ - An unparseable exit code from the API is normalized to `-1` so the run's output is still returned instead of failing schema validation.
49
+ - Offline regression checks: `tenki_exec` advertises the schema, still carries guard annotations via the `registerTool` path, and exactly one tool declares an `outputSchema` (bump the count when migrating more tools). A new offline suite (`test/exec-output.test.mjs`) calls the tool in-memory and locks down the content blocks, escaping, output validation, and `tenki_move_path` semantics.
50
+
51
+ ### Fixed
52
+
53
+ - **Path validation no longer rewrites the path.** `pathSchema` used zod's `.trim()`, which is a *transform*: leading/trailing whitespace (legal in POSIX filenames) was silently stripped before sending, so `tenki_remove_path`/`tenki_move_path` on such a file operated on a *different* path than the one named. Paths now reach the API verbatim, with blank/whitespace-only values still rejected client-side. `tenki_exec`'s `cwd` had the same trim and is fixed too. (`sessionIdSchema` keeps trimming: a session id is a UUID, so surrounding whitespace is always an accident.)
54
+ - **Importing the package no longer terminates the importing process.** `main` pointed at the CLI entrypoint, whose token check runs at module load and calls `process.exit(1)` — so `import "tenki-mcp"` killed the host process outright (and with a token set, would have booted a stdio server inside it). The package is now bin-only: `npx tenki-mcp` and every MCP client config are unaffected (they resolve `bin`), and importing raises a catchable module-not-found error instead.
55
+ - **A path or working directory beginning with a hyphen is no longer read as a command option.** Shell-quoting doesn't prevent it: `cd '-L'` silently succeeds into `$HOME`, so `tenki_exec` with such a `cwd` ran the command in the wrong directory, and `mv '-file' …` failed outright. Both now pass `--` before their operands.
56
+ - **Sandbox creation was broken for workspace-scoped API keys.** `resolveOwner` forwarded WhoAmI's `ownerType` verbatim into `CreateSession`, but the API validates `owner_type ∈ {SERVICE, USER}` and now returns `WORKSPACE` for workspace-scoped keys → every `tenki_create_sandbox`/`tenki_run_code` failed with `400 invalid_argument`. Fix: send the same placeholder the first-party SDKs hardcode (`"SERVICE"`/`"self"`) when WhoAmI returns a type CreateSession rejects — the server derives the real owner from the authenticated identity regardless. Verified live: create → exec (structured) → terminate, 12/12 checks.
57
+ - `test/http-transport.test.mjs` no longer asserts `ownerType === "USER"` (stale against the same API change); it accepts any authenticated owner type.
58
+
59
+ ## [2.0.0-alpha.2] — 2026-07-27 — MCP security hardening + registry fixes
60
+
61
+ ### MCP security hardening (least privilege + annotations)
62
+
63
+ Applies MCP-native security controls, mapped to the CSA MCP Server Top-10 (esp. MCP-07 excessive permissions). See SECURITY.md.
64
+
65
+ - **Tool annotations** on all 84 tools (`readOnlyHint` on 36 read tools, `destructiveHint` on the 13 delete/terminate/revoke tools, `openWorldHint` on all) so clients can surface/gate dangerous tools.
66
+ - **Least-privilege env controls:** `TENKI_MCP_READONLY=1` registers only read tools (no create/run/delete/spend); `TENKI_MCP_DISABLED_TOOLS=a,b` drops named tools. Applied centrally via a registration guard — no change to the tool modules.
67
+ - **Audit logging:** `TENKI_MCP_AUDIT=1` logs each tool call name + arg keys (never values/content/token) to stderr.
68
+ - **SECURITY.md** — threat model, trust boundaries (the key + endpoint are capabilities; sandbox output is untrusted), and a full CSA Top-10 mapping. README security section + disclosure policy.
69
+ - Regression test `test/security.test.mjs` (10 offline checks).
70
+
71
+ ### Registry/preview/artifact request-shape fixes
72
+
73
+ An API-contract review of the write paths found **11 more request-shape bugs** beyond the two already fixed — the registry module was almost entirely non-functional (it sent `reference` where the API wants `ref` / `imageId` / `imageRef`). All fixed and verified (each now reaches a clean not-found instead of a validation error); regression-guarded by test/registry-shapes.test.mjs.
74
+
75
+ - **registry** (9): `get_image`, `resolve_image_ref` (needs `ref`+`workspaceId`), `set_image_visibility` (needs `ref`+`REGISTRY_VISIBILITY_*` enum), `delete_image` (whole=`ref`; version=`imageId`+`snapshotId`), `share_image` (`imageRef`+`targetWorkspaceId`), `unshare_image` (`ref`), `revoke_image_share_grant` (`grantId`), `publish_image` (needs `ref`+`kind`+`snapshotId`/`sourceTemplateId`, not a session).
76
+ - **previews** (1): `touch_preview` takes a `previewToken`, not session/port.
77
+ - **artifacts** (1): `get_download_url` supports download-by-artifact-id only (the API rejects a path); the non-functional `path` option was removed.
78
+
79
+ ## [2.0.0-alpha.1] — 2026-07-21 — Harden the HTTP transport (security)
80
+
81
+ An independent security review of the v2.0.0-alpha.0 HTTP transport found three coupled HIGH issues (the endpoint holds a shared TENKI_API_KEY and exposes code-execution + credit-spend tools). All fixed:
82
+
83
+ - **Loopback-only by default** — binds `127.0.0.1`, not `0.0.0.0`; the banner now shows the real host. Expose with `TENKI_MCP_HTTP_HOST`.
84
+ - **Bearer auth** — set `TENKI_MCP_HTTP_TOKEN` (constant-time checked); the server **refuses to bind to a non-loopback host without it**.
85
+ - **DNS-rebinding protection ON** — Host-header allowlist, so a malicious web page cannot drive the local server (verified: forged Host → 403).
86
+
87
+ Also: session count cap + idle reaping (init-flood DoS), a 1 MiB request-body cap enforced on both the Content-Length header and the streamed byte count (memory DoS), malformed JSON → JSON-RPC `-32700`/400, no internals in error responses, and SIGTERM/SIGINT graceful shutdown. Offline regression `test/http-input.test.mjs` asserts the 413 + a clean init; `test/http-transport.test.mjs` asserts the auth gate + rebinding rejection (7/7). stdio + tool parity unchanged (84).
88
+
89
+ ## [2.0.0-alpha.0] — 2026-07-21 — HTTP transport (v2 begins)
90
+
91
+ The server now speaks **Streamable HTTP** in addition to stdio, so it can be hosted for remote MCP clients (`TENKI_MCP_TRANSPORT=http PORT=3000`). Verified end-to-end (connect → tools/list → tool call over HTTP; stdio unchanged, 84 tools). Server construction refactored into a shared `createServer()` factory (src/server.ts) used by both transports.
92
+
93
+ Streaming exec (`StreamCommandOutput`) and per-request HTTP auth are not yet implemented. Alpha: not for production hosting yet.
94
+
95
+ ## [1.0.2] — 2026-07-21 — Fixes from comprehensive testing
96
+
97
+ A 34-scenario test matrix run against live Tenki found **two real request-shape bugs**, both fixed and verified end-to-end:
98
+
99
+ - **tenki_attach_volume** sent a flat request; AttachVolumeRequest nests the target under a `volume` sub-message (`{sessionId, volume:{volumeId, mountPath, readOnly?}}`). Every attach was rejected — this broke the volume warm-cache workflow. Fixed.
100
+ - **tenki_list_image_share_grants** sent `reference`; the API field is `ref` (required). It was silently ignored, 400-ing every call and making the ACL-read surface unreachable. Fixed.
101
+
102
+ Also: a real MCP **test suite** (`test/`) — a cleanup-safe harness driving the actual MCP protocol + 5 suites (coverage, client-integration, errors-edge, journeys, admin-previews) — **68 checks, all green**. Doc fixes (workspace tool names; create_template setup_script). No tool count change (84).
103
+
104
+ ## [1.0.1] — 2026-07-20 — Fix ResizeVolume field
105
+
106
+ Live-verifying the volume write path (once a workspace volume-quota block was cleared) surfaced one real bug: `tenki_resize_volume` sent `sizeBytes` but the API expects `newSizeBytes`, so resizes were rejected. Fixed. Full volume lifecycle (create → get → update → resize → delete) now live-verified end-to-end.
107
+
108
+ ## [1.0.0] — 2026-07-20 — Full CLI parity
109
+
110
+ **84 tools — parity with the entire 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; streaming methods are deferred to v2.0). This release closes the long tail on top of v0.7: binary artifact transfer (get_upload_url / get_download_url), SSH access (update_ssh_keys / issue_ssh_cert / list_ssh_gateways), the preview-URL primitives (get/delete/touch/bind/unbind/resolve), project-scoped list variants (volumes/snapshots/templates), snapshot-retention settings, and registry grant-revoke. New read paths live-verified against api.tenki.cloud; write/advanced additions are grounded in the published API surface and labeled where not exercised end-to-end.
111
+
112
+ **Tools:** +18 to reach 84 (artifacts x2, ssh x3, preview extras x6, list variants x4, retention x2, revoke-grant x1)
113
+
114
+ ## [0.7.0] — 2026-07-20 — Workspace administration
115
+
116
+ Workspace-level administration: sandbox usage reporting and get/update of workspace sandbox settings. Live-verified.
117
+
118
+ **Tools:** tenki_get_workspace_usage, tenki_get_workspace_settings, tenki_update_workspace_settings
119
+
120
+ ## [0.6.0] — 2026-07-20 — Custom runtimes — templates & registry
121
+
122
+ Bring-your-own-runtime: define an environment once and boot into it warm. Templates add the platform's first async job surface (build, poll, cancel). The registry publishes versioned custom images with a private-by-default ACL surface. List paths live-verified.
123
+
124
+ **Tools:** 9 template tools (create/get/list/update/delete + build/cancel-build/get-build/list-active-builds) + 9 registry tools (publish/get/list/set-visibility/delete/delete-version/resolve-ref/share/list-share-grants)
125
+
126
+ ## [0.5.0] — 2026-07-20 — Persistent state — snapshots & volumes
127
+
128
+ Persistent state for the iterative agent loop. Snapshots checkpoint a known-good sandbox to branch or roll back from; volumes are durable disks that carry a cache or dataset across otherwise-ephemeral sandboxes. Destructive verbs are explicit-target-only. Snapshots live-verified; volume shapes verified against the SDK (write path blocked only by a workspace volume quota during testing).
129
+
130
+ **Tools:** 8 snapshot tools + 8 volume tools (create/get/list/update/delete/resize/attach/detach)
131
+
132
+ ## [0.4.0] — 2026-07-20 — Preview URLs — the ship surface
133
+
134
+ The ship surface: turn an exposed port into a public, shareable preview URL an agent can hand back. Project-scoped (live-verified: requires projectId + a validated slug). Completes the ports resource with unexpose.
135
+
136
+ **Tools:** tenki_create_preview_url, tenki_open_preview, tenki_list_preview_urls, tenki_unexpose_port
137
+
138
+ ## [0.3.0] — 2026-07-20 — Session lifecycle & fleet control
139
+
140
+ Extended session control: extend a sandbox's wall-clock lifetime, update mutable fields (name/tags/idle-timeout/max-duration), bulk-terminate (explicit-id-only, irreversible), an activity heartbeat, and workspace/project-scoped fleet listing. Live-verified.
141
+
142
+ **Tools:** tenki_extend_sandbox, tenki_update_sandbox, tenki_terminate_sandboxes, tenki_report_sandbox_activity, tenki_list_workspace_sandboxes, tenki_list_project_sandboxes
143
+
144
+ ## [0.2.0] — 2026-07-20 — Filesystem completion (data plane)
145
+
146
+ Data-plane filesystem metadata + mutation, completing the file surface beyond read/write/list: stat, mkdir (recursive), remove (recursive), and move (exec-backed mv, since the data plane exposes no Move RPC). Live-verified against api.tenki.cloud.
147
+
148
+ **Tools:** tenki_stat_path, tenki_make_dir, tenki_remove_path, tenki_move_path
149
+
150
+ ## [0.1.0] — 2026-07-20
151
+ Initial release. **15 MCP tools over stdio, live-verified against `api.tenki.cloud`.**
152
+
153
+ - `tenki_whoami`
154
+ - `tenki_run_code` — ephemeral sandbox: boot → run shell/python/javascript → terminate
155
+ - Sandbox lifecycle: `tenki_create_sandbox`, `tenki_get_sandbox`, `tenki_list_sandboxes`, `tenki_terminate_sandbox`, `tenki_pause_sandbox`, `tenki_resume_sandbox`
156
+ - `tenki_exec` — run a command in a sandbox (stdout/stderr/exit inline)
157
+ - Files: `tenki_read_file`, `tenki_write_file`, `tenki_list_files`
158
+ - `tenki_git`
159
+ - Ports: `tenki_expose_port`, `tenki_list_exposed_ports`
160
+
161
+ Dependency-free ConnectRPC client (control + data plane), ported from the live-verified [n8n node](https://github.com/opencolin/n8n-nodes-tenki). Tools organized into self-registering modules under `src/tools/`.
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 ADDED
@@ -0,0 +1,192 @@
1
+ # tenki-mcp — `@tenkicloud/mcp`
2
+
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
+ > **Heads up — pending first npm publish.** Until `@tenkicloud/mcp` lands on npm, use [Run it from a clone](#run-it-from-a-clone-instead) below. Every `npx -y @tenkicloud/mcp` form here activates the moment it's published.
24
+
25
+ ### Use it in Claude Code
26
+
27
+ ```bash
28
+ claude mcp add tenki --env TENKI_API_KEY=tk_your_key_here -- npx -y @tenkicloud/mcp
29
+ ```
30
+
31
+ ### Use it in Claude Desktop
32
+
33
+ Add to `claude_desktop_config.json`:
34
+
35
+ ```json
36
+ {
37
+ "mcpServers": {
38
+ "tenki": {
39
+ "command": "npx",
40
+ "args": ["-y", "@tenkicloud/mcp"],
41
+ "env": { "TENKI_API_KEY": "tk_your_key_here" }
42
+ }
43
+ }
44
+ }
45
+ ```
46
+
47
+ ### Use it in Cursor
48
+
49
+ Add the same block to `.cursor/mcp.json` (project) or `~/.cursor/mcp.json` (global):
50
+
51
+ ```json
52
+ {
53
+ "mcpServers": {
54
+ "tenki": {
55
+ "command": "npx",
56
+ "args": ["-y", "@tenkicloud/mcp"],
57
+ "env": { "TENKI_API_KEY": "tk_your_key_here" }
58
+ }
59
+ }
60
+ }
61
+ ```
62
+
63
+ ### Use it in Codex
64
+
65
+ The OpenAI Codex CLI reads MCP servers from `~/.codex/config.toml`. Add:
66
+
67
+ ```toml
68
+ [mcp_servers.tenki]
69
+ command = "npx"
70
+ args = ["-y", "@tenkicloud/mcp"]
71
+ env = { TENKI_API_KEY = "tk_your_key_here" }
72
+ ```
73
+
74
+ ### Confirm it's working
75
+
76
+ After adding the server, **start a fresh session** so the client loads it, then ask your agent one of:
77
+
78
+ - *"Check tenki auth status."* — confirms your credential was picked up.
79
+ - *"Run `print(2+2)` in a fresh Tenki sandbox."* — a full boot → run → teardown round-trip.
80
+
81
+ If the `tenki_*` tools don't show up, restart the client so it re-reads its config.
82
+
83
+ 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).
84
+
85
+ ### Run it from a clone instead
86
+
87
+ For development, or to run an unreleased change:
88
+
89
+ ```bash
90
+ git clone https://github.com/LuxorLabs/tenki-mcp.git && cd tenki-mcp
91
+ npm install && npm run build
92
+ TENKI_API_KEY=tk_your_key_here node dist/index.js
93
+ ```
94
+
95
+ 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.
96
+
97
+ ### Environment variables
98
+
99
+ | Variable | Default | Purpose |
100
+ |---|---|---|
101
+ | `TENKI_API_KEY` | — | API key (`tk_…`). One of this or `TENKI_AUTH_TOKEN` is required. |
102
+ | `TENKI_AUTH_TOKEN` | — | Session token (`ory_st_…` or cookie value). **Takes precedence over `TENKI_API_KEY`** when both are set. |
103
+ | `TENKI_API_ENDPOINT` | `https://api.tenki.cloud` | Control-plane base URL (`TENKI_API_URL` is an alias). |
104
+ | `TENKI_MCP_READONLY` | off | `1` registers only read tools (no create/run/delete/spend). |
105
+ | `TENKI_MCP_DISABLED_TOOLS` | — | Comma-separated tool names to skip registering. |
106
+ | `TENKI_MCP_AUDIT` | off | `1` logs each tool call name + arg keys to stderr. |
107
+ | `TENKI_MCP_TRANSPORT` | `stdio` | `http` serves Streamable HTTP instead (see below). |
108
+ | `PORT` | `3000` | HTTP transport port. |
109
+ | `TENKI_MCP_HTTP_HOST` | `127.0.0.1` | HTTP bind host; non-loopback requires `TENKI_MCP_HTTP_TOKEN`. |
110
+ | `TENKI_MCP_HTTP_TOKEN` | — | Bearer token for the HTTP endpoint; optional on loopback, required on a non-loopback host. |
111
+
112
+ ## Tools
113
+
114
+ **85 tools** — 84 with full parity to the Tenki unary API (enforced by a CI [parity audit](scripts/parity-audit.mjs)), plus `tenki_auth_status`. Grouped by domain:
115
+
116
+ | Domain | Tools |
117
+ |---|---|
118
+ | **Auth** | `tenki_auth_status` (which credential is configured, and does it work — the only tool available when none is) |
119
+ | **Identity** | `tenki_whoami` |
120
+ | **Run** | `tenki_run_code` (one-shot: boot → run shell/python/js → tear down) |
121
+ | **Sandboxes** | `tenki_create_sandbox` · `tenki_get_sandbox` · `tenki_list_sandboxes` · `tenki_terminate_sandbox` · `tenki_pause_sandbox` · `tenki_resume_sandbox` |
122
+ | **Session admin** | `tenki_extend_sandbox` · `tenki_update_sandbox` · `tenki_terminate_sandboxes` (bulk) · `tenki_report_sandbox_activity` · `tenki_list_workspace_sandboxes` · `tenki_list_project_sandboxes` |
123
+ | **Exec** | `tenki_exec` (stdout/stderr/exit inline) |
124
+ | **Files** | `tenki_read_file` · `tenki_write_file` · `tenki_list_files` · `tenki_stat_path` · `tenki_make_dir` · `tenki_remove_path` · `tenki_move_path` |
125
+ | **Git** | `tenki_git` (clone/checkout/diff/log — the API supports exactly these four; run other git commands via `tenki_exec`) |
126
+ | **Ports & previews** | expose · list-exposed · unexpose · create-preview-url · open-preview · list/get/delete-preview-url · touch-preview · bind/unbind-preview-url · resolve-preview-token |
127
+ | **Artifacts** (binary transfer) | `tenki_get_upload_url` · `tenki_get_download_url` (signed URLs for binary PUT/GET) |
128
+ | **SSH** | `tenki_update_ssh_keys` · `tenki_issue_ssh_cert` · `tenki_list_ssh_gateways` |
129
+ | **Snapshots** | create · get · list · list-session · list-project · list-workspace · list-dangling · update · delete · get-download-url |
130
+ | **Volumes** | create · get · list · list-project · update · delete · resize · attach · detach |
131
+ | **Templates** | create · get · list · list-project · update · delete · build · cancel-build · get-build · list-active-builds |
132
+ | **Registry** (custom images) | publish · get · list · set-visibility · delete · resolve-ref · share · unshare · list-share-grants · revoke-share-grant |
133
+ | **Workspace** | `tenki_get_workspace_usage` · `tenki_get_workspace_settings` · `tenki_update_workspace_settings` · `tenki_get_snapshot_retention_settings` · `tenki_update_snapshot_retention_settings` |
134
+
135
+ Full per-release breakdown in [CHANGELOG.md](CHANGELOG.md).
136
+
137
+ ## Auth
138
+
139
+ 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`).
140
+
141
+ **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.
142
+
143
+ ## Host it over HTTP (v2.0-beta)
144
+
145
+ Besides stdio, the server speaks **Streamable HTTP** so it can be hosted for remote MCP clients:
146
+
147
+ ```bash
148
+ TENKI_MCP_TRANSPORT=http PORT=3000 TENKI_API_KEY=… npx -y @tenkicloud/mcp
149
+ # → tenki-mcp running on http://127.0.0.1:3000/mcp (Streamable HTTP) [loopback only, no auth]
150
+ ```
151
+
152
+ **Security — this endpoint is a capability.** In HTTP mode the process holds one shared `TENKI_API_KEY` and exposes every tool, including arbitrary code execution and credit spend. So by default it:
153
+
154
+ - **binds to loopback (`127.0.0.1`) only** — set `TENKI_MCP_HTTP_HOST=0.0.0.0` to expose it, but then
155
+ - 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.
156
+ - **DNS-rebinding protection** is on (Host-header allowlist), so a malicious web page can't drive your local server.
157
+
158
+ ```bash
159
+ # expose to a network safely:
160
+ TENKI_MCP_TRANSPORT=http TENKI_MCP_HTTP_HOST=0.0.0.0 PORT=3000 \
161
+ TENKI_MCP_HTTP_TOKEN=$(openssl rand -hex 32) TENKI_API_KEY=… npx -y @tenkicloud/mcp
162
+ ```
163
+
164
+ Point an HTTP-capable MCP client at `/mcp`. v2.0-beta uses one shared `TENKI_API_KEY` for all sessions; per-request auth (multi-tenant hosting) is not yet implemented. Verified end-to-end (`test/http-transport.test.mjs`: auth gate, DNS-rebinding rejection, connect → tools/list → tool call over HTTP).
165
+
166
+ ## How it works
167
+
168
+ 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.
169
+
170
+ 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.
171
+
172
+ The wire details are ported from the live-verified [n8n community node](https://github.com/opencolin/n8n-nodes-tenki).
173
+
174
+ ## Security
175
+
176
+ 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:
177
+
178
+ - **Least privilege:** every tool carries MCP annotations (`readOnlyHint` / `destructiveHint`). 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.
179
+ - **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)).
180
+ - **Audit:** `TENKI_MCP_AUDIT=1` logs each tool call's name to stderr.
181
+ - **Untrusted output:** `tenki_run_code`/`tenki_exec`/`tenki_read_file` return output from untrusted code — clients should treat tool results as data, not instructions.
182
+
183
+ Report vulnerabilities via a private [security advisory](https://github.com/LuxorLabs/tenki-mcp/security/advisories/new), not a public issue.
184
+
185
+ ## Related
186
+
187
+ - **Tenki Sandbox** — the platform: https://tenki.cloud
188
+ - **n8n-nodes-tenki** — Tenki as an n8n node: https://github.com/opencolin/n8n-nodes-tenki
189
+
190
+ ## License
191
+
192
+ 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 the API key it holds — 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
+ - **The API key is a capability.** In every mode the process authenticates to Tenki with one `TENKI_API_KEY` and can create sandboxes, run arbitrary code, and spend credits. Anyone who can invoke the server can do those things. Scope the key if your Tenki plan allows it; never commit it (`.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 HTTP mode the `/mcp` endpoint is equivalent to handing out the key — protect it (below).
12
+
13
+ ## Controls this server provides
14
+
15
+ ### Least privilege (MCP-07 — the main lever)
16
+ Every tool is tagged with MCP **annotations** so clients can surface/gate danger:
17
+ `readOnlyHint` on inspection tools, `destructiveHint` on the 13 that delete/terminate/revoke, `openWorldHint` on all (they call an external API). 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
+ - The key is read from env and sent only as an auth header — never logged, never in error responses.
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 | single shared key; HTTP endpoint requires its own bearer token; `READONLY`/denylist bound the blast radius; per-request auth is not yet implemented |
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 | key via env only; never logged/committed/echoed; 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.
@@ -0,0 +1,152 @@
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
+ export type Language = "shell" | "python" | "javascript";
16
+ export interface ExecResult {
17
+ command: string;
18
+ args: string[];
19
+ stdout: string;
20
+ stderr: string;
21
+ exitCode: number;
22
+ ok: boolean;
23
+ captureError?: string;
24
+ }
25
+ export interface TenkiClientOptions {
26
+ /** Timeout for unary control/data-plane calls (default 30s). */
27
+ timeoutMs?: number;
28
+ /** Timeout for ExecuteCommand when the command itself has no timeout (default 630s). */
29
+ execTimeoutMs?: number;
30
+ /** Timeout for long storage/VM operations — see SLOW_METHOD (default 600s). */
31
+ slowTimeoutMs?: number;
32
+ /** Assumed session-credential lifetime when the API returns no parseable expiry (default 5min). */
33
+ credTtlMs?: number;
34
+ }
35
+ export declare class TenkiClient {
36
+ private readonly token;
37
+ private readonly baseUrl;
38
+ private readonly credCache;
39
+ private readonly credInflight;
40
+ private readonly timeoutMs;
41
+ private readonly execTimeoutMs;
42
+ private readonly slowTimeoutMs;
43
+ private readonly credTtlMs;
44
+ constructor(token: string, baseUrl?: string, opts?: TenkiClientOptions);
45
+ /**
46
+ * ExecuteCommand blocks until the command finishes, so its timeout follows
47
+ * the command's own timeout (plus margin) instead of the unary default.
48
+ */
49
+ private timeoutFor;
50
+ /**
51
+ * fetch + full body read under ONE timeout, with a readable error instead of
52
+ * a bare AbortError. Reading the body inside the guarded section matters: a
53
+ * response whose headers arrive but whose body stalls must fail with the
54
+ * same friendly, retryable timeout as a connection that never responds.
55
+ */
56
+ private fetchTextWithTimeout;
57
+ /**
58
+ * Retry policy: rate limits always; `unavailable` and gateway-shaped
59
+ * 502/503/504 (load balancers and CDNs return these with an HTML or empty
60
+ * body, no ConnectRPC code) for every method that cannot double-apply.
61
+ */
62
+ private shouldRetry;
63
+ /** Exponential backoff with half-jitter; a Retry-After header (seconds) wins, capped. */
64
+ private backoffMs;
65
+ /**
66
+ * Unary control-plane call. The WHOLE call — every attempt plus every
67
+ * backoff sleep — draws from one shared deadline (the method's timeout), so
68
+ * retries can never stack past it: an MCP client's own 60s default would
69
+ * otherwise give up before our per-attempt timeouts did. Rate-limit
70
+ * rejections retry with jittered backoff (honoring Retry-After); transient
71
+ * `unavailable`/gateway errors AND transport-level failures (timeout,
72
+ * connection reset, DNS) retry for every method that cannot double-apply.
73
+ * `service` defaults to SandboxService; pass another fully-qualified ConnectRPC
74
+ * service (e.g. the SSH gateway service) for methods hosted elsewhere.
75
+ */
76
+ control(method: string, body?: Record<string, unknown>, service?: string): Promise<Record<string, any>>;
77
+ /**
78
+ * Mint (and cache per session) the data-plane endpoint + session certificate.
79
+ * A credential with no parseable expiry is cached for credTtlMs — never
80
+ * forever — and a known expiry is shortened by a skew so an in-flight call
81
+ * can't straddle the real expiry. Minting is single-flight: concurrent data
82
+ * calls on a cold cache share one mint instead of each paying a
83
+ * control-plane round trip for its own certificate.
84
+ */
85
+ private credentialFor;
86
+ private mintCredential;
87
+ /**
88
+ * Unary data-plane call. The inner request is wrapped as
89
+ * `{ request: { sessionId, ...request } }`. This is the path behind every
90
+ * file tool AND how tenki_exec reads back its own output, so it gets the
91
+ * same treatment as the control plane: one shared deadline, rate limits
92
+ * retried for every method, transient/gateway/transport failures retried
93
+ * for read-shaped methods. A stale-certificate failure (401/unauthenticated
94
+ * — the cert may have expired server-side regardless of its stated expiry)
95
+ * invalidates the cache and re-mints once; permission_denied is NOT an auth
96
+ * failure — a denied path is a normal user error a fresh cert can never
97
+ * fix, so re-minting would waste a control-plane round trip per denied call.
98
+ */
99
+ data(sessionId: string, method: string, request?: Record<string, unknown>): Promise<Record<string, any>>;
100
+ /**
101
+ * Resolve the calling identity + a default workspace/project for CreateSession
102
+ * (which requires a projectId). Picks the first workspace that has a project so
103
+ * the (workspace, project) pair stays consistent.
104
+ *
105
+ * CreateSession validates owner_type ∈ {SERVICE, USER} and requires a
106
+ * non-empty owner_id, but derives the real owner from the authenticated
107
+ * identity server-side. WhoAmI can return other owner types (e.g. WORKSPACE
108
+ * for workspace-scoped keys), which the validator rejects — so for those we
109
+ * send the same placeholder the first-party SDKs hardcode ("SERVICE"/"self").
110
+ */
111
+ resolveOwner(): Promise<{
112
+ ownerType?: string;
113
+ ownerId?: string;
114
+ workspaceId?: string;
115
+ projectId?: string;
116
+ }>;
117
+ /** Poll GetSession until it reaches (or passes into) the target state. */
118
+ waitForState(sessionId: string, target?: string, { timeoutMs, intervalMs }?: {
119
+ timeoutMs?: number;
120
+ intervalMs?: number;
121
+ }): Promise<Record<string, any>>;
122
+ readTextFile(sessionId: string, path: string): Promise<string>;
123
+ writeTextFile(sessionId: string, path: string, text: string): Promise<Record<string, any>>;
124
+ /**
125
+ * Run a command in a session and return stdout/stderr inline.
126
+ *
127
+ * We wrap the command in `sh -c '<cmd> > out 2> err'`, then read the capture
128
+ * files back over the data plane, so output is available through a plain-HTTP
129
+ * client. Capture-read failures degrade gracefully into `captureError` rather
130
+ * than losing the run — but the result is marked NOT ok, because empty
131
+ * stdout/stderr next to a zero exit code would otherwise read as a clean,
132
+ * silent success.
133
+ */
134
+ execCaptured(sessionId: string, command: string, opts?: {
135
+ args?: string[];
136
+ cwd?: string;
137
+ env?: Record<string, string>;
138
+ timeoutSeconds?: number;
139
+ }): Promise<ExecResult>;
140
+ /**
141
+ * One-shot: boot a throwaway sandbox, run code (shell / python / javascript),
142
+ * return its output, and terminate the sandbox. Cost-guarded (1 vCPU, 1 GB,
143
+ * 10-min cap, 5-min idle) so an ephemeral run can never leak a billing session.
144
+ */
145
+ runCode(language: Language, code: string, opts?: {
146
+ env?: Record<string, string>;
147
+ timeoutSeconds?: number;
148
+ }): Promise<ExecResult & {
149
+ sessionId: string;
150
+ language: Language;
151
+ }>;
152
+ }