Skip to main content
Glama
clabrado

dtctl

by clabrado
README.md
# dtctl for VS Code

**Zero-install Dynatrace Platform tooling.** Everything `dtctl` does — DQL, documents,
workflows, SLOs, settings, entities — reimplemented in pure TypeScript and shipped
inside the extension. No CLI to download, no binary to unquarantine, no admin rights,
no OS permission prompts.

- **16 tools** exposed to **GitHub Copilot Chat** (VS Code Language Model Tools API)
- The same 16 tools exposed to **Claude / any MCP client** via a bundled stdio MCP
  server that VS Code auto-discovers — zero config
- **DQL query runner** webview (table + JSON + CSV export), **tenant explorer** tree,
  **status-bar** tenant indicator, and command-palette verbs
- **Credentials never touch settings.json and never appear in logs.** Browser-login
  tokens are stored as `0600` files in the extension's own storage directory (not the
  OS keychain, so nothing prompts for a keychain password); API tokens and OAuth
  client secrets continue to use VS Code SecretStorage

---

## Install (locked-down managed machine)

You need **VS Code and nothing else**. No admin rights are required at any step.

### Option A — sideload the VSIX (recommended for restricted environments)

1. Copy `dtctl-vscode-0.2.0.vsix` to the machine (email, share drive, USB — it is a
   single file, ~2 MB, no installer).
2. In VS Code: **Extensions** view → `…` overflow menu → **Install from VSIX…** →
   pick the file.
   Or from a terminal: `code --install-extension dtctl-vscode-0.2.0.vsix`
3. Reload the window when prompted. Done.

There is no post-install step. Nothing is written outside VS Code's own extension
folder in your user profile, no service is registered, and nothing asks for
elevation, Gatekeeper approval, or SmartScreen approval — because no native binary
ships in the package.

### Option B — Marketplace

Search the Extensions view for **dtctl — Dynatrace Platform Tools** and click Install.

### Where it runs

| Environment | Extension UI | Copilot Chat tools | MCP server |
| --- | --- | --- | --- |
| VS Code desktop (Win/macOS/Linux) | yes | yes | yes |
| GitHub Codespaces / dev containers | yes | yes | yes |
| Remote-SSH / WSL | yes | yes | yes |
| vscode.dev / github.dev (browser) | yes | yes | no (browser hosts cannot spawn a child process) |

The browser build is the same TypeScript, compiled to a web worker bundle. Only the
stdio MCP server — which by definition needs a process — is unavailable there.

---

## Login

### Browser login (recommended)

`Ctrl/Cmd+Shift+P` → **dtctl: Login (browser)**

1. Enter your Dynatrace environment URL (`https://<id>.apps.dynatrace.com`) and a
   profile name.
2. VS Code opens your browser at the Dynatrace sign-in page. Sign in as you
   normally would — SSO, MFA and all.
3. The tab shows "You can close this tab" and the profile appears in the status
   bar. Done.

**No admin-created OAuth client is needed.** Browser login uses Dynatrace's own
first-party public OAuth clients (the same ones the `dtctl` CLI uses), with
Authorization Code + PKCE (S256). There is no client secret anywhere — not in
the extension, not in your settings, not on disk. You do not have to ask a
tenant admin to mint a client ID/secret pair before you can use the extension.

> **Audit-log note.** Because browser login reuses the `dtctl` CLI's public OAuth
> client, OAuth grants and token activity for this extension appear in Dynatrace
> audit logs attributed to **`dt0s12.dtctl-prod`** (the dtctl CLI), not to this
> extension by name. The signed-in *user* is recorded correctly — everything runs
> with that user's own permissions — but the extension has no separate application
> identity, so it cannot be audited or revoked independently of the dtctl CLI. If
> your security team needs the extension to be distinguishable, it needs its own
> registered OAuth client; set `dtctl.oauthClientId` once you have one.

The environment you sign in to is detected from the URL you type
(`apps.dynatrace.com` → prod, `dev.apps.dynatracelabs.com` → dev,
`sprint.apps.dynatracelabs.com` → sprint/hardening), and the URL is sent as the
OAuth `resource` parameter so the issued token is bound to that tenant.

### The loopback callback port

Sign-in completes by redirecting to a local listener on
`http://localhost:<port>/auth/login`, bound explicitly to `127.0.0.1`.

The extension tries port **3232** first — the port `dtctl` uses — and falls back
to 3233/3234/3235 and then any free port if 3232 is busy. The Dynatrace OAuth
clients accept a loopback redirect on any port, so a port conflict is not a
failure mode; the actually-bound port is what goes into `redirect_uri`. If your
machine blocks local loopback sockets entirely (some endpoint-protection
policies do), login fails with a message saying exactly that.

Login times out after 5 minutes, and the listener is always closed afterwards —
a failed or cancelled login never leaves a port bound.

### Scopes (read-only by default)

Browser login requests dtctl's **read-only** scope set by default. Write scopes
are opt-in via `dtctl.loginScopeLevel`, which mirrors dtctl's safety levels:

| `dtctl.loginScopeLevel` | Requests |
| --- | --- |
| `readonly` *(default)* | 40 read scopes |
| `readwrite-mine` | read + write/delete of resources you own |
| `readwrite-all` | read + write/delete of all resources |
| `dangerously-unrestricted` | every scope dtctl knows about |

`openid` and `offline_access` are always requested — `offline_access` is what
returns a refresh token. Access tokens are short-lived (~10 minutes) and are
refreshed transparently in the background; you are only asked to sign in again
if the refresh itself fails. Changing the setting takes effect on the next
login. Write *tools* remain independently gated by `dtctl.allowWriteTools`.

`dtctl.oauthClientId` overrides the built-in client ID if your organisation
issues a dedicated public OAuth client for this extension.

### Where browser-login tokens are stored

Interactive-login tokens are written to **disk**, not the OS keychain:

```
<VS Code user data>/User/globalStorage/dtctl-tools.dtctl-vscode/oauth-tokens/<profile>.json
```

VS Code owns that directory (`context.globalStorageUri`) and resolves it to the
right per-user location on Windows, macOS and Linux. It is outside your repo and
workspace, so token files can never be committed or packaged into the VSIX.

This is deliberate. VS Code's `SecretStorage` is backed by the OS credential
vault (macOS Keychain / Windows Credential Manager / libsecret), which can
prompt for a password and is frequently unavailable on managed machines. `dtctl`
has the same escape hatch (`DTCTL_TOKEN_STORAGE=file`).

Security properties:

- Token files are created and `chmod`ed **0600** (owner read/write only). On
  POSIX that is real protection. **On Windows the POSIX mode is largely a
  no-op** — the practical protection there is that the file lives under your own
  user profile directory, which is ACL'd to you. We do not claim more than that.
- Writes are atomic: a temp file in the same directory is renamed over the
  target, so a crash mid-write cannot corrupt or truncate an existing token
  file, and no partial token file is left behind.
- **dtctl: Logout (browser login)** deletes the token file from disk — it does
  not merely clear an in-memory cache.
- Tokens are never logged. All diagnostic output goes through `redact()`.

### Keeping OAuth constants in sync with dtctl

The SSO endpoints, client IDs and callback contract are not hand-maintained.
They are generated into `src/kernel/dtctl-oauth-constants.ts` from dtctl's own
[`sdk/session/oauth_flow.go`](https://raw.githubusercontent.com/dynatrace-oss/dtctl/main/sdk/session/oauth_flow.go):

```bash
npm run sync:dtctl             # fetch upstream and regenerate the constants
npm run sync:dtctl -- --check  # exit 1 if the checked-in constants are stale
```

`test/contract/dtctl-drift.contract.test.ts` runs as part of `npm test` and
fails with a per-constant diff if Dynatrace changes any of these upstream:

```
dtctl OAuth constants have CHANGED UPSTREAM.

  prod.tokenUrl
    checked-in: https://token.dynatrace.com/sso/oauth2/token
    upstream:   https://token.dynatrace.example/sso/oauth2/token

Fix: run `npm run sync:dtctl` to regenerate …
```

It is **network-optional**: with no network access it skips with an explanatory
message rather than failing, so offline development and egress-restricted CI
are unaffected.

This is **build-time only**. The extension never fetches dtctl source, and
never fetches OIDC discovery, at run time — the login path resolves its
endpoints entirely offline from the compiled-in constants. There is no code
path by which a change upstream could redirect a live OAuth flow without a
human running the sync and reviewing the diff.

### Token-based profiles (unchanged)

**dtctl: Add Tenant Profile** still supports OAuth client credentials and
classic `dt0c01.…` API tokens. Those credentials continue to live in VS Code
`SecretStorage` exactly as before.

---

## First-run setup (about 60 seconds)

> For most users **dtctl: Login (browser)** above is the faster path — it needs
> no admin-created OAuth client. Use the flow below when you already have a
> client ID/secret or a classic API token.

1. `Ctrl/Cmd+Shift+P` → **dtctl: Add Tenant Profile**
2. Enter a profile name (e.g. `prod`), your tenant URL
   (`https://<id>.apps.dynatrace.com`), and pick the auth type:
   - **OAuth client credentials** — enter client ID, optional scopes, then the client
     secret
   - **Classic API token** — paste the `dt0c01.…` token
3. The credential is stored with `vscode.SecretStorage` (OS keychain / Keychain
   Access / libsecret / Credential Manager, managed entirely by VS Code). The
   extension never writes it to a settings file, workspace file, log, or the VSIX.

Add as many profiles as you have tenants; the status bar shows the active one and
clicking it switches profiles. `dtctl.defaultProfile` sets the startup profile.

### Required permissions on the tenant

Read-only usage needs the standard read scopes (`storage:*:read`,
`document:documents:read`, `automation:workflows:read`, `settings:objects:read`,
`environment-api:entities:read`). Write verbs additionally need
`document:documents:write` and the relevant write scopes.

---

## Using it

### Command palette

| Command | What it does |
| --- | --- |
| `dtctl: Login (browser)` | browser sign-in with PKCE — no OAuth client needed |
| `dtctl: Logout (browser login)` | deletes the stored tokens from disk |
| `dtctl: Add / Switch / Remove Tenant Profile` | multi-tenant profile management |
| `dtctl: Open DQL Query Runner` | webview with table/JSON views and CSV export |
| `dtctl: Run DQL Query` | runs the current editor selection as DQL |
| `dtctl: Export Query Results as CSV` | opens the last result set as a CSV document |
| `dtctl: Show Current User` / `List Profiles` | `whoami` / `contexts` |
| `dtctl: Find Resource` / `Get Resource by ID` | resource browse and fetch |
| `dtctl: Apply Resource from File` | upsert the open JSON document (write-gated) |
| `dtctl: Refresh Explorer` | reload the tenant tree |
| `dtctl: Copy MCP Server Config` | config snippet for an external MCP client |

### Tenant explorer

The **Dynatrace Tenants** view in the Explorer sidebar lists profiles → resource
types → live resources. Clicking a resource fetches and opens it as JSON.

### Write safety

All write and destructive verbs are **disabled by default**. Set
`dtctl.allowWriteTools: true` to enable them. Even then, destructive tools require an
explicit `confirm: true` argument, agents see a confirmation dialog before execution,
and the MCP server does not advertise write tools at all while the setting is off.

---

## Agent access

See `docs/SE-ONE-PAGER.md` for the customer-facing enablement page. In short:

- **GitHub Copilot Chat** — tools register on activation. Type `#dtctl_query` (or any
  other tool name) in chat, or just ask a question and let the agent pick.
- **Claude / MCP clients in VS Code** — the extension registers an MCP server
  definition provider, so the server appears automatically with your active profile's
  credentials injected at start time. Nothing to paste, nothing to configure.
- **Standalone MCP clients** — run `dtctl: Copy MCP Server Config` and paste the
  snippet into your client's config, then substitute your own token.

---

## Tool surface (16)

Read tier — always available: `dtctl_query`, `dtctl_verify`, `dtctl_get`,
`dtctl_describe`, `dtctl_history`, `dtctl_find`, `dtctl_logs`, `dtctl_whoami`,
`dtctl_contexts`, `dtctl_commands`.

Write tier — requires `dtctl.allowWriteTools`: `dtctl_apply`, `dtctl_share`,
`dtctl_unshare`.

Destructive tier — requires `dtctl.allowWriteTools` **and** `confirm: true`:
`dtctl_delete`, `dtctl_exec`, `dtctl_restore`.

The registry in `src/kernel/tools/index.ts` is the single source of truth: the
`package.json` contributions, the MCP `tools/list` payload, and the runtime handlers
are all generated from or driven by it, and `npm run gen:contributions -- --check`
fails the build if they drift.

---

## Architecture

```
src/kernel/          pure TypeScript, zero vscode imports, zero native code
  auth.ts            OAuth client-credentials + classic token, cache/refresh/retry
  transport.ts       Live / Replay / Record HTTP transports
  dql.ts             DQL execute + poll + paging + CSV
  api/               Platform (gen3) and Classic API clients
  tools/             the tool registry and handlers
src/adapters/
  shared.ts          one executor -> byte-identical envelopes on every surface
  ui/core.ts  ui.ts  tree view, DQL webview, status bar, commands
  lm/core.ts  lm.ts  vscode.lm.registerTool  (Copilot Chat)
  mcp/core.ts mcp.ts vscode.lm.registerMcpServerDefinitionProvider (Claude/MCP)
src/mcp/server.ts    bundled stdio MCP server -> dist/mcp-server.js
```

**Why no binary.** A per-platform Go binary would need `chmod +x`, survive macOS
Gatekeeper quarantine and Windows SmartScreen, and would not run in vscode.dev or a
browser Codespace at all. Reimplementing the surface over `fetch` removes every one
of those failure modes; the tradeoff is that parity with the Go CLI is maintained by
hand (the registry + contract tests are how that is enforced). No fallback binary
ships in this VSIX.

---

## Development

```bash
npm ci
npm run build          # gen contributions + strict tsc + esbuild (3 bundles)
npm run test           # unit + contract + end-to-end MCP smoke
npm run package        # produces dtctl-vscode-<version>.vsix
```

`npm run test:smoke` stands up a fake tenant, spawns the **bundled**
`dist/mcp-server.js` as a real child process, and drives a real MCP JSON-RPC
conversation (initialize → tools/list → tools/call) against it.

## License

Apache-2.0.