@zyntax/extension-sdk
v1.0.32
Published
Canonical public contracts and authoring helpers for Zyntax extensions.
Readme
Zyntax Extension SDK
@zyntax/extension-sdk is the canonical public contract for executable Zyntax
extension providers. It contains plain-data manifest and RPC types, canonical
capability names, and small authoring helpers. It has no editor, WebView,
filesystem, network, native-command, or app-private dependencies.
Extension manifests distinguish required dependencies from integrations.
Required dependencies are installed with the extension and must activate.
Integrations authorize composition with compatible optional packages. They are
never installed implicitly and never block activation. extensions.manage can
request a user-reviewed install of a declared integration. Both arrays are explicit.
Provider code is bundled by the Zyntax extension tooling into a self-contained
providers/*.js module. Import only this package for host DTOs and provider
helpers; do not import application source or tooling internals.
Tooling consumes the provider-methods, manifest-constants, and capabilities
subpaths so manifest validation and provider bundle validation share one public
vocabulary.
EXTENSION_HOST_API_METHODS and EXTENSION_HOST_API_INTERACTIVE_METHODS define
the host method vocabulary and which calls wait for user interaction or observed
work. The build exports runtime-contract.json from these constants and the
provider methods for non-JavaScript hosts. Host adapters consume this artifact
instead of maintaining separate method lists. Provider method types are generated
from the same TypeScript source as their runtime values.
import {
EXTENSION_API_VERSION,
defineCompletionProvider,
type ExtensionCompletionProvider,
} from "@zyntax/extension-sdk";
export const extensionApiVersion = EXTENSION_API_VERSION;
export const createCompletionProvider = defineCompletionProvider(
(): ExtensionCompletionProvider => ({
async provideCompletions(request, cancellation) {
await cancellation.checkpoint();
return null;
},
dispose() {},
}),
);Manifest validation remains host/tooling-owned. The SDK does not grant permissions or platform access.
App variants
An optional signed manifest field limits an extension to specific app editions:
"appVariants": ["full", "dev"]Omit appVariants to support all editions. When present, it must be a nonempty
list of unique lite, full, or dev values; order does not matter. Dev is an
explicit edition, not a bypass. The host checks compatibility when installing
and activating packages, including required dependencies. The existing package
signature protects this plain JSON field; it is not an encrypted licensing system.
Tool/runtime requirements and engines.zyntax remain separate checks.
Theme scope selectors
parseTextMateSelector(value) validates a nonempty, trimmed selector and returns
an immutable TextMateSelector AST; invalid or unsupported syntax throws
TypeError. It has no editor or matching dependency. This bounded subset of
TextMate scope selectors
supports descendant paths, direct-parent >, wildcard scope names, parentheses,
unary exclusion -, intersection &, alternatives | and comma, and binary
subtraction. Hyphens inside scope names remain literal: a-b is one name,
while a -b subtracts b.
In host matching, a whole dot-separated * atom matches one scope atom; normal
prefix specialization can still match further atoms. For example, source.*
matches source.js and source.js.embedded, but not source. Asterisks embedded
inside an atom remain literal, not a general glob expression. The parser retains
the existing scope-name vocabulary without rewriting it.
Paths bind first. Inside a group or selector, |, &, and binary - bind
equally and associate left to right, as in TextMate; comma is a lower-precedence
alternative. Thus a | b - c means (a | b) - c. Adjacent scope names form a
path; separate grouped expressions require an operator. Anchors (^, $),
side/injection filters (L:, R:, B:), and unsupported characters are
rejected rather than ignored. Only spaces and tabs are internal whitespace.
The AST uses path nodes with scopes and between-scope relations
(descendant or child), any/all nodes with terms, and not nodes
with a term. Subtraction is all(left, not(right)); groups retain their
meaning without a wrapper node. Host matchers evaluate exclusions against the
same scope stack as positive paths, not against a fallback stack after a miss.
Only matching positive paths contribute specificity; negated conditions filter
matches. The SDK defines syntax, not a rendering engine or VS Code theme-rendering
equivalence.
TEXTMATE_SELECTOR_RULES limits input to 512 UTF-16 code units, 24 total scope
names plus child operators, 64 emitted AST nodes, and nesting depth 8. A group
or unary minus increases nesting by one from zero; binary operators do not.
Paths, binary combinations, and each unary or subtraction negation count as
nodes; parentheses do not. Existing ASCII scope-name syntax is preserved in
scopePattern. The build exports these rules as textMateSelectors in
runtime-contract.json. JavaScript and native validators consume the same
textmate-selector-conformance.json fixture, including boundary and AST cases.
Unscoped theme defaults use the asset's empty scope list, not an empty selector.
Cooperative cancellation
Every provider cancellation token exposes checkpoint(). Awaiting it yields
isolated provider execution to the host event queue so queued cancellation and
control work can run, then rejects with the same host cancellation error as
throwIfCancellationRequested() when cancellation is requested. CPU-bound
provider work must await checkpoints at safe intervals; reading
isCancellationRequested or calling throwIfCancellationRequested() alone
does not yield execution to the host.
Providers must not publish partial state from work interrupted by a rejected
checkpoint. The synchronous token members remain useful before and after
already-asynchronous boundaries, while checkpoint() is the cooperative
boundary for long-running isolated work.
Language filename associations
A contributed editor language always declares extensions and may also declare
exact filenames or basename-only filenamePrefixes. Prefix matching is
case-insensitive. Each prefix must be non-empty and contain no / or \ path
separator; the host canonicalizes prefixes before matching.
When several associations match, the host chooses an exact filename first, then
the longest matching filename prefix, then the longest matching compound
extension. This lets a language claim a filename family such as Dockerfile.*
without enumerating every basename.
{
"contributes": {
"languages": [{
"id": "dockerfile",
"extensions": [],
"filenames": ["Dockerfile"],
"filenamePrefixes": ["Dockerfile."]
}]
}
}Notebook file associations
Every contributed notebook type explicitly declares both extensions and
filenames. At least one of those arrays must be non-empty. Extensions are
canonical lowercase suffixes that include the leading dot, such as .ipynb;
filenames are exact basenames without path separators. Matching is
case-insensitive, including compound extensions.
These signed associations let the host offer a serializer only for files it claims. The host also enforces the association when deserializing and serializing, so a notebook provider cannot be selected for an unrelated file. Notebook types do not create editor-language identities, and the host does not infer associations from a type name or inspect file contents to choose one.
{
"contributes": {
"notebookTypes": [{
"id": "jupyter-notebook",
"type": "jupyter-notebook",
"label": "Jupyter Notebook",
"module": "providers/jupyter-notebook.js",
"export": "createJupyterNotebookSerializerProvider",
"extensions": [".ipynb"],
"filenames": [],
"priority": 100
}]
}
}Bundled package lifecycle
An extension extension.json or managed-tool tool.json may declare the
optional top-level field "required": true. The field has authority only when
that exact package id is shipped in the APK's immutable package bundle. Such a
package remains visible and updateable through the normal package pipeline, but
the host does not allow its removal. Missing or false means removable. A Store
or file-installed package cannot make itself non-removable by declaring the
field, and an update cannot change the APK-owned lifecycle policy.
Bundling itself is established by placing a signed .zntx or .ztool in the
APK bundle directory; it is not another manifest field or a separate package
format. Managed-tool schema and archive validation remain owned by the managed
tooling, which applies the same optional boolean contract to tool.json.
Managed-tool package paths use exact case-sensitive POSIX identity on Android.
isManagedToolPath checks the shared path syntax: relative forward-slash paths,
printable ASCII, at most MANAGED_TOOL_MAX_PATH_BYTES (384) bytes overall and
MANAGED_TOOL_MAX_PATH_SEGMENT_BYTES (96) bytes per segment. Empty, . and ..
segments, surrounding spaces, trailing dots, and <>:"\\|?* are rejected. Paths
are never lowercased or rewritten. payload/include/Name.h and
payload/include/name.h are distinct files, as are case-distinct directories.
Payloads containing case-distinct names require a case-sensitive build filesystem,
such as a Linux container, so staging preserves both names.
Builders and installers still reject exact duplicate paths, file/directory/link
conflicts, unsafe paths, and links outside the declared package graph. Archive
metadata remains at the package root; tool files and resources remain under
payload/. This does not change extension archive paths or manifest version 1.
Notebook kernels
A notebook-kernel provider returns only a symbolic runtime descriptor. Its
executable names one manifest runtime requirement and one provider-neutral
command. Literal arguments and signed managed-tool resource references are
allowed; native paths, environment values, and fallback commands are not. The
host resolves the currently selected compatible runtime and signed resources,
then owns the adapter process and its full-duplex framed JSON-RPC session. The
public protocol defines initialization, execution, strictly ordered
status/output/comm events, kernel-to-host input requests, explicit interrupt and
restart generations, cancellation, and idempotent shutdown. Output events are
authoritative and are not duplicated in the final execution result. Event
sequence numbers increase across the complete adapter session, including kernel
restarts. A restart response commits the new generation before the adapter emits
any event for that generation.
Kernel adapters remain runtime-neutral. They must not expose native paths, process handles, transport sockets, or renderer objects, and the host never infers a runtime from a notebook language or file name.
const descriptor = {
kind: "runtime" as const,
executable: {
requirement: "kernel-runtime",
command: "runtime",
},
args: [{
$toolResource: {
tool: "publisher.kernel-adapter",
resource: "adapter",
},
}],
protocol: "zyntax-notebook-jsonrpc" as const,
};Runtime Manager
Runtime Manager owns development runtimes and their activation in each project.
The user selects a device default for each stable family such as python, node,
java, or rust; a shared project selection can override it. Each extension
runtime requirement explicitly supports the selected runtime, one exact
extension-managed runtime, or both. The host never searches terminal PATH.
A consumer declares runtimeRequirements. The runtimes.execute permission
authorizes runtime execution and the project-scoped snapshot, select and
observe host methods. The three exact source forms are:
{ "selected": true }uses only a compatible project selection or inherited device default.{ "managed": { "tool": "publisher.runtime-tool" } }uses only that exact owned managed-tool dependency.{ "selected": true, "managed": { "tool": "publisher.runtime-tool" } }uses an explicit project selection when present. Without a project override, it uses a compatible device default first and the managed runtime when no usable default is available.
For requirements allowing selected runtimes, an explicit project selection that becomes missing, changed, incompatible or unverifiable blocks new execution until it is replaced or cleared; it never switches to a managed fallback. The declared fallback for an unusable device default does not change the shared selection, and the default's failure remains diagnosable even when managed execution can continue.
SDK 1.0.31 adds host["runtimes.execute"]. Runtime selection requests name an explicit
project and one manifest-local requirement; project: null means the current
Explorer project, while a string names an owned selected project belonging to
the active Explorer context. Stale or unrelated project references are rejected.
The host keys shared selections by the active Explorer root's native identity and
runtime family; selected subprojects share their parent Explorer project's choice.
Extensions targeting that project observe the same choice even when their project
references and requirement IDs differ.
snapshot({ project, requirement }, cancellation) returns
{ project, requirement, revision, candidates, selection }. Candidates contain
compatible, selectable terminal-package runtimes and native-admitted existing
installations. Extension-managed
fallbacks remain host-resolved and cannot be selected through this API. The
effective selection reuses the existing ready/unavailable runtime type. With no
Explorer project, project: null can read the device default resolution.
select({ project, requirement, expectedRevision, identity }, cancellation)
sets the shared project override to an exact identity from the snapshot.
identity: null removes that override and resumes the declared default
resolution. The requirement must declare sources.selected: true. Stale
revisions, changed installations and unauthorized sources are rejected; with no
Explorer project, project: null fails with projectRequired. Extensions cannot
change the device default. Runtime resolution and command activation remain
host-owned; runtime selection requests supply no executable paths or environment maps.
observe({ project, requirement, afterRevision }, cancellation) returns the
current snapshot for zero, then waits for a newer revision without timer polling.
Revisions are safe nonnegative integers; negative, unsafe and future revisions
are rejected. Changes may coalesce into one current snapshot. Cancelling an
observation leaves the selection and project work intact, and owner disposal
cancels pending waits. snapshot and observe do not change the selection.
SDK 1.0.32 adds
selectInstallation({ project, requirement, expectedRevision, installation }, cancellation).
It admits an existing app-private installation and selects it atomically through
the same project/family authority. The caller needs terminal, a requirement
with sources.selected: true, the referenced path permissions and a trusted
current project. It cannot write device defaults or register for another owner.
No download, installation, copying or startup-file edit is performed.
const installation = {
id: "chosen-runtime",
label: "Selected runtime installation",
capabilities: ["process.execute"],
commands: [{
id: "runtime",
path: { kind: "selectedPath", file: selectedDirectory.id, path: ["bin", "runtime"] },
}],
versionProbe: { command: "runtime", args: ["--version"], stream: "stdout", prefix: "runtime " },
activation: {
environment: { TOOL_HOME: { kind: "selectedPath", file: selectedDirectory.id, path: [] } },
path: [{ kind: "selectedPath", file: selectedDirectory.id, path: ["bin"] }],
},
} satisfies ExtensionRuntimeInstallation;The host verifies executable authority independently of picker grants, derives
the version with the existing bounded probe and fingerprints the admitted launch
graph. A grant to read a directory does not permit executing an external binary.
The resulting source is { kind: "installed", owner, installationId } inside
the existing opaque runtime identity. Other compatible extensions and the app
can select it using ordinary select; there is no second custom-tool selection.
Disabling/removing its owner or changing an admitted installation makes explicit
selections unavailable. An owner-local installation ID must identify one intended
installation; replacing its declaration never silently rebinds pinned identities.
Installation command IDs and capability arrays are sorted and unique; bash
and sh remain reserved shell aliases. Existing provider limits apply: one to
32 commands, one to 32 base capabilities and at most 32 per command (64 in their
union), at most 16 probe arguments of 512 control-free code units each, a nonempty
probe prefix up to 160 code units and a label up to 120 code units. The complete
declaration is limited to 64 KiB UTF-8 JSON. Activation metadata accepts only
existing typed filesystem references, with the same environment/PATH bounds
described below; it cannot depend on another selected runtime or store secrets.
Terminal-package descriptors additionally expose optional verified
package: { repository, name } metadata. Match this provenance to declared
development-stack requirements; never guess provider IDs or installation paths,
or select a package solely because its version resembles the requested tool.
{
"permissions": ["runtimes.execute"],
"toolRequirements": [{
"id": "zyntax.python-runtime",
"version": ">=3.14.6 <4",
"platforms": [{ "os": "android", "architecture": "arm64-v8a" }],
"capabilities": ["runtime.python"]
}],
"runtimeRequirements": [{
"id": "python",
"runtime": "python",
"minimumVersion": "3.12.0",
"capabilities": ["process.execute"],
"sources": {
"selected": true,
"managed": { "tool": "zyntax.python-runtime" }
}
}]
}The managed tool must name an entry in the same manifest's
toolRequirements. Its signed release must be a runtime tool whose
projectRuntime family, version, and capabilities satisfy the requirement.
Runtime executions continue to declare only { requirement, command }; for a
managed runtime, the host resolves that command through the signed
projectRuntime.commands mapping. Extensions never duplicate its entrypoint,
runtime root, executable path, or environment.
Runtime inventory, verification, selection, and execution are host-owned. A
declarative contributes.runtimeProviders entry may describe how a signed
terminal-package repository exposes an installed runtime; it does not inspect
packages or execute code. The host verifies the exact owning package and
prefix-relative command files, performs the bounded version probe, and then adds
the candidate to Runtime Manager. It never searches PATH.
{
"permissions": ["terminal.packages"],
"contributes": {
"runtimeProviders": [{
"id": "termux.node",
"runtime": "node",
"label": "Terminal Node.js",
"capabilities": ["process.execute"],
"package": { "repository": "termux-main", "name": "nodejs" },
"commands": [
{ "id": "node", "path": "bin/node" },
{
"id": "npm",
"path": "lib/node_modules/npm/bin/npm-cli.js",
"package": { "repository": "termux-main", "name": "npm" },
"capabilities": ["package-manager.npm"]
}
],
"versionProbe": {
"command": "node",
"args": ["--version"],
"stream": "stdout",
"prefix": "v"
}
}]
}
}The top-level package is the required primary owner. Commands use that owner
unless they declare an optional companion package. Companion commands and
their sorted, unique capabilities appear only while that package owns the
declared file. An owned command may be an ELF or a bounded shebang script. A
script is exposed only when its interpreter resolves to another exact command
declared by the same provider; the host materializes that interpreter and the
script path without consulting ambient PATH, and rejects missing or cyclic
interpreter chains. The version probe must use a primary-owned command, so a missing
companion never removes the runtime itself. Managed .ztool runtimes instead
declare the runtime they actually contain; they do not act as package-provider
registries. Selected and managed runtimes share the same provider-neutral
symbolic commands; no provider can declare an absolute native path, shell
fragment, or inferred fallback command.
SDK 1.0.32 adds optional provider activation metadata, selected atomically
with that provider's commands. environment maps nonreserved keys to
{ path, package? }; path is an ordered list of the same references. Each path
is terminal-prefix-relative and must remain owned by the primary package or
the explicit companion package. For example, a Java provider can declare
JAVA_HOME: { path: "lib/jvm/java-21-openjdk" } and a PATH directory
{ path: "lib/jvm/java-21-openjdk/bin" }. This keeps command and location
selection under the same authority without tool-specific host logic.
Shared project activation
SDK 1.0.32 also adds persistent project activation through the existing
runtimes.execute permission and runtime manager revision. Declare
runtimeActivation: { environment: ["TOOL_HOME", "BUILD_MODE"], commands: ["build"] }
and both runtimes.execute and terminal permissions. Runtime requirements are
not needed for an activation-only extension. Empty declaration arrays permit a
PATH-only contribution. Environment and command names must be unique; aliases
match [a-z0-9][a-z0-9._-]{0,63}, excluding the reserved bash and sh routes.
activationSnapshot({ project }, cancellation)returns{ project, revision, activation }, containing only the calling extension's effective contribution, never another owner's selected-file references.activationUpdate({ project, expectedRevision, activation }, cancellation)atomically replaces that owner's entire project contribution.activation: nullremoves its override and resumes inherited defaults; an empty definition is an explicit empty contribution. Other owners' state is not replaced.activationObserve({ project, afterRevision }, cancellation)uses the same cancellable observation rules asobserve; zero reads immediately.
Project scope has the same Explorer identity and selected-project checks as
runtime selection. Writes require a trusted current project, and extensions
cannot write device defaults. Without an Explorer project, project: null may
read inherited defaults but updates fail with projectRequired.
An activation definition has three required fields:
const activation = {
environment: {
TOOL_HOME: { kind: "selectedPath", file: selectedDirectory.id, path: [] },
BUILD_MODE: "release",
},
path: [{ kind: "selectedPath", file: selectedDirectory.id, path: ["bin"] }],
commands: {
build: {
command: "bash",
args: [{ kind: "extensionResource", path: "resources/build.bash" }, "build"],
},
},
} satisfies ExtensionRuntimeActivationDefinition;Environment values and prefix argv reuse typed selectedPath, packagePath
and extensionResource task bindings, or bounded literals. Persistent activation
excludes projectPath and secret bindings. { kind: "installedPath",
path: absolutePath }
references an existing cached installation inside the shared terminal home or
prefix. The host canonicalizes and revalidates it, rejecting controls, colons,
traversal, symlink escapes and extension-private locations. Explicit custom
locations outside those roots must use an approved selectedPath. These methods
do not install, download, copy or relocate tools; existing caches remain in use.
Task environment/argv and activation environment/argv can also contain
{ kind: "runtimeEnvironment", requirement: "java", name: "JAVA_HOME" }.
This read-only reference resolves a location declared in the selected provider's
activation.environment. The requirement must belong to the caller and allow
selected runtimes. Its project scope comes from the enclosing task or activation,
and its value is captured with the same runtime graph as executable commands.
It never reads ambient environment variables or exports a raw native path to the
extension. Missing metadata or an unavailable selection fails explicitly. This
keeps custom and packaged runtime commands and homes under one shared selection.
managedTool tasks reject this binding just as they reject packagePath; it is
also excluded from PATH lists and installation declarations to prevent recursive
runtime dependencies.
There are at most 32 environment bindings (24 KiB resolved total), 16 PATH
directories, 32 command aliases and 32 prefix arguments per alias. Environment
literals retain the 512-code-unit, control-free task limit; command literals
retain the 16 KiB UTF-8 per-literal and 64 KiB resolved-argv limits. Alias targets
are bare terminal commands matching [A-Za-z0-9][A-Za-z0-9._+-]{0,127}, resolved
natively before activation aliases are applied; caller argv follows the prefix.
An extension can supply its signed launcher resource as an argument to native
Bash, keeping tool-specific flags and configuration in the extension. The host
does not generate launcher scripts or redirect explicit paths such as ./gradlew.
PATH is an ordered list of admitted directories, not a writable environment key.
The task-input protected variables remain reserved: PATH, SHELL, PREFIX,
HOME, TMPDIR, BASH_ENV, ENV, TERMUX_*, ZYNTAX_*, LD_* and Android
substrate variables. Undeclared keys, conflicting owner contributions, stale
revisions and invalidated references are rejected. Native launch admission
revalidates enabled owners and resolves one immutable environment/PATH/command
snapshot for new shells and terminal-domain tasks; running processes keep their
captured activation. Consumers should observe this shared state instead of
reapplying stale local copies of tool selections.
The exported isExtensionRuntimeActivationDeclaration,
isExtensionRuntimeActivationDefinition, isExtensionRuntimeActivationPath and
isExtensionRuntimeActivationEnvironmentKey guards share strict wire validation
with host adapters. They do not grant filesystem authority or replace native
ownership, permission, existence and resolved-size checks.
isExtensionRuntimeInstallation and isExtensionRuntimeEnvironmentReference
provide the corresponding structural guards for existing installations and
symbolic runtime metadata references.
A runtime task references its manifest-local requirement and a provider-neutral command id. Its required console mode chooses an interactive PTY or captured output without changing runtime resolution:
const execution = {
kind: "runtime" as const,
requirement: "python",
command: "python",
args: ["main.py"],
workingDirectory: [],
console: "terminal" as const,
};ExtensionRuntimeCommandReference is the same two-field executable reference
for typed host process descriptors. Arbitrary provider configuration uses the
reserved, explicit ExtensionRuntimeCommandConfigurationReference sentinel:
{
"executable": {
"$runtimeCommand": {
"requirement": "language-runtime",
"command": "runtime"
}
}
}The host interprets only the tagged value; an ordinary JSON object containing
requirement and command remains provider-owned JSON. The two strict
validators check the exact path-free shapes. The host additionally verifies
that requirement belongs to the installed manifest and that the selected
provider exposes command.
Public runtime identities contain only an opaque installed-source identity, stable candidate id, and exact observation fingerprint. Native locations never cross the extension boundary. If an installation changes, its fingerprint changes and the affected selection becomes explicitly unavailable until a current candidate is selected.
Incremental structural documents
A structural-region provider owns incremental parser state for each source
document that the host opens. openDocument receives an opaque, non-empty
documentId and the only complete source-text snapshot transferred for that
document generation. The provider retains the corresponding text and parser
state.
The host serializes lifecycle calls for each documentId.
applyDocumentChanges advances the open document from baseVersion to exactly
baseVersion + 1 without changing its generation. Its changes are ordered,
non-overlapping UTF-16 ranges in the base revision, and every range in the batch
uses that same coordinate space. Lifecycle mutations commit atomically. The host
does not cancel them during ordinary editing; cancellation is reserved for
teardown. A generation change closes the old document and opens a new identity
with a fresh source snapshot.
Each successful open or change acknowledges the exact documentId, version,
and generation that was committed. provideRegion asynchronously returns the
validated root-to-owner path for one position and association.
provideRegionDocument asynchronously returns the complete region document
when a document-wide consumer needs it. Both target an exact version and
generation, and both results echo the documentId, source URI, version,
generation, and provider id so the host can reject stale or cross-document
output. The host cancels superseded queries and retains only the latest result;
implementations must yield cooperatively and observe the cancellation token
while preparing structure by awaiting cancellation.checkpoint() at safe
intervals.
closeDocument is required and idempotent. It echoes the released
documentId; disposing the provider releases every still-open document. The
contract has no stateless provideRegions(snapshot) method or compatibility
alias.
Language-server mappings declare only verified routes. Position routes are
completion, hover, signatureHelp, definition, implementation,
typeDefinition, references, rename, codeActions, typeHierarchy, and
documentHighlights. Document/workspace routes are diagnostics,
semanticTokens, workspaceSymbols, documentSymbols, foldingRanges, and
documentFormatting. The host negotiates the matching standard LSP methods and
routes parser-owned source or virtual coordinates; it never infers a claim from
the server, language, filename, or installed package.
A language-server contribution may declare projectFiles, a non-empty list of
at most EXTENSION_LSP_MAX_PROJECT_FILES (32) distinct project-relative file paths.
The server is eligible when any listed path is an actual regular file in the
open project. Matching is exact and case-sensitive, uses the current filesystem
rather than unsaved editor buffers, and is re-evaluated when the project or those
files change. No directory scan or glob matching is performed. Omitting
projectFiles makes the server unconditional.
For evaluated project settings, declare configurationProvider: { module, export,
watchFiles } on the language server instead of inline initializationOptions or
workspaceConfiguration. Export a defineLanguageServerConfigurationProvider
factory from the usual isolated provider module. Its
provideLanguageServerConfiguration({ project, projectUri, serverId }, cancellation)
returns the complete { initializationOptions?, workspaceConfiguration? } JSON.
The host uses the server's granted project scope and existing cancellation/payload
limits. Results do not grant resource access or executable authority.
watchFiles uses the same exact paths and non-empty 32-file limit as projectFiles.
Creation, content changes and deletion invalidate only the affected server's
configuration. The host cancels stale work and restarts that server with current
document snapshots; it does not evaluate project code or poll on keystrokes.
Malformed configuration fails explicitly; no old configuration is substituted.
The provider kind languageServerConfiguration belongs to its server and has no
independent activation event. Inline and provider configurations cannot be combined.
isExtensionProjectFilePath validates the shared path syntax. Paths use forward
slashes and contain at most EXTENSION_LSP_MAX_PROJECT_FILE_LENGTH (384) UTF-16
code units. Unicode and interior spaces are allowed; absolute paths, empty,
. or .. segments, surrounding segment whitespace, control characters,
backslashes, colons, and glob characters (*?[]{}) are rejected. Conditions
never authorize access outside the open project.
Eligible servers retain their declared language priority. At equal priority, a matching project-conditioned exclusive server takes precedence over an unconditional exclusive server. Equal priority and equal project specificity remain an explicit conflict; additive servers remain additive. Catalog default language recommendations consider only unconditional servers, since a catalog has no project context.
Declarative TextMate contributions reference exact UTF-8 JSON
(syntaxes/*.tmLanguage.json) or XML plist (syntaxes/*.tmLanguage) assets. The
host validates and loads the declared format directly; extensions do not convert
or execute grammar assets. A primary grammar declares its sorted canonical
languages bindings; one immutable asset may serve multiple identities when the
upstream grammar is genuinely shared. Injection-only grammars omit languages.
The published package contains compiled ESM and declaration files in dist/.
Extension builds never execute TypeScript source directly from node_modules.
The SDK also defines bounded plain-data contracts for inline completions,
assisted edits, AI code actions, decorations, document colors, hover, tasks, terminal
profiles, SCM state, and project templates. A task provider returns one reviewed
execution plan. A managedTool plan selects a declared tool/entrypoint and requires
tools.execute; a runtime plan selects a declared requirement/command and requires
runtimes.execute; a command plan selects a bare command from the user's terminal
environment and requires terminal. Every route explicitly selects terminal or
captured console output, bounded arguments and contained working-directory segments.
Arguments may include ExtensionTaskPath bindings. Optional process inputs bind
environment values without exposing host routing variables. Package paths belong
to declared installed stack requirements; signed resources/* assets belong to
the exact extension generation. These references do not change execution domains:
managed execution cannot acquire terminal-package paths or switch to terminal tools.
The host resolves and reviews inputs before launch and owns cancellation. Terminal-profile
and project-template providers likewise return descriptors or host-reviewed plans
while the host owns process launch, filesystem writes, approvals, and transactions.
Assisted-edit providers expose both bounded unary proposals and one ordered push
stream. A streaming invocation emits progress or proposal events through the
host-provided sink, observes the cancellation token, and completes once. The native
transport owns ordering, byte/count limits, cancellation, and terminal delivery;
providers never poll or open a second request.
Hover providers receive the exact parser-owned source region at the requested position and return only bounded plain-text or Markdown content with a snapshot-bound source range. The host sanitizes and renders that content in the same tooltip surface as LSP hover, suppresses the surface while completion is pending or active, and owns pointer and touch activation. Providers cannot return HTML, DOM, editor objects, or infer embedded languages from filenames or source scans.
Managed-tool adapters use the exported EXTENSION_MANAGED_TOOL_INVOCATION_METHOD
and JSON-RPC request/response types over the host's Content-Length-framed stdio
transport. Extension code selects only a manifest-declared tool and entrypoint;
it never supplies an executable path, environment, or terminal command.
The SDK exports the canonical managed-tool request, result, frame, and JSON
structure limits plus extensionJsonUtf8ByteLength for exact host-neutral
checks. Diagnostics providers have high per-provider item and encoded-byte
ceilings plus one document-wide aggregate budget; providers must bound their
own results before crossing either RPC boundary.
Long-running managed tools use the declarative persistentServices contribution and the
services.manage permission. A service selects only a signed tool and entrypoint and declares
bounded stop, health-probe, and log policy; it cannot declare argv, an environment, a path, or
shell text. Providers operate only their own symbolic service ids. The host owns the canonical
workspace/trust decision, process group, health, bounded UTF-8 logs, and exact activation cleanup.
Terminal development dependencies use contributes.developmentStacks and the independent
terminal.packages permission. A stack contains one to 32 required package symbols from the fixed
host-known zyntax or termux-main repository identity. It cannot contain repository URLs, keys,
versions, package-manager options, commands, paths, environment values, or scripts.
inspectStack returns installed, candidate and available versions from current indexes.
refreshStack(stack, cancellation) first refreshes host-configured signed indexes through
the existing package-operation queue, then returns the same inspection. Use it before
selecting missing packages; it does not install or upgrade anything. Transactions require
{ stack, intent, packages: [{ id, version }] }: exact declared requirement selections for
install, repair, update or remove (the installed version for removal). The existing
Package Manager reviews the complete impact and owns the mutation queue; stale selections
are rejected. This permission does not grant execution. Separate SDK/NDK components remain
the responsibility of their installers, not a second package manager in the SDK.
waitTransaction(transaction, cancellation) waits for a terminal state without polling.
Cancelling that wait leaves the transaction running; cancelTransaction stops it.
Both refresh and completion waits are cancellable interactive host methods. SDK 1.0.25
adds only these generic operations, with no project-specific policy or version allowlist.
{
"permissions": ["extension.execute", "terminal.packages"],
"contributes": {
"developmentStacks": [{
"id": "web-runtime",
"label": "Web runtime",
"packages": [{ "id": "node", "repository": "termux-main", "package": "nodejs-lts" }]
}]
}
}Managed execution arguments may contain a signed resource reference instead of
a literal string. The native host resolves it immediately before launch and the
path never enters extension-provider code. Node entrypoints may additionally
declare a sorted nodeModules array which binds a valid npm package name to a
signed package-directory resource. The host projects the entrypoint and those
packages into one immutable module tree and launches it with normal Node module
resolution. Manifests never declare NODE_PATH, installation directories, or
package-specific host behavior; each resource package must carry a matching
package.json name.
A signed managed-tool tool.json entrypoint may also declare non-empty static
args. Each value is either a bounded literal string or the same
$toolResource reference, restricted to a direct managed-tool dependency. The
host resolves these static arguments immediately before launch and places them
before invocation arguments. Omit args when there are no static arguments;
an empty array is not a second canonical representation. This gives an adapter
an exact signed dependency resource through ordinary process arguments without
declaring an installation path, environment variable, shell fragment, or host
filesystem layout.
A managed-tool capability probe may declare non-empty stdin containing at most
MANAGED_TOOL_PROBE_MAX_STDIN_BYTES (65,536) UTF-8 bytes. The host writes those
exact bytes and then closes stdin before it waits for the bounded probe result.
When stdin is omitted, the host closes stdin immediately. This supports
protocol-aware checks without exposing paths, environments, shell commands, or
process control to package metadata.
Managed document formatters declare their exact stdio protocol. Use
framed-jsonrpc for an adapter that implements the canonical formatting RPC, or
raw-stdio for a direct CLI which accepts the exact document bytes on stdin and
returns one bounded UTF-8 document on stdout. Raw formatter arguments are fixed
manifest strings or signed resource references; the host does not substitute a
language, URI, option, or shell fragment at runtime. The explicit current-document
path reference is available only to document formatters. Contribute separate formatter descriptors when a CLI
requires a different constant argument for each canonical language.
Project-template providers return either an atomic UTF-8 file plan or a symbolic managed-tool plan. Managed generators run only after host review, in a new app-owned staging root, and must produce the declared regular-file markers before the host atomically publishes the project. The provider and managed tool receive JSON and canonical URIs only; native paths never cross the provider boundary.
Debugger configuration providers receive the active project and document as
canonical URIs. Use debugDocumentPath(documentUri) for configuration fields
such as program; the host validates that URI against the active project and
resolves the native path only when it sends the DAP launch or attach request.
Providers and manifests never contain app-private filesystem paths.
For a launch field which the user must select, use
debugProjectPath(id, label, kind). The consumer extension can persist the selected
canonical document URI per project. The symbolic slot remains in provider
requests; only the generic managed transport resolves it to a native path when
sending the protocol message.
SDK 1.0.29 adds provider preparation (debug.execute), protocol-neutral framed
processes (processes.execute) and owner-scoped editor integration (editor).
An optional debugger extension owns its UI, DAP request sequencing, protocol state,
breakpoints, watches, inspection and console. The core retains only provider/tool
authority, project trust, bounded transport, process cleanup and shared editor
rendering; it does not implement a second debugger engine or mandatory panel.
context({ project: null }, cancellation) reads the Explorer project, current
document/language and available paired configuration/adapter providers. A
selected scope is accepted only when it resolves to that same
Explorer project; another root is rejected explicitly. Refresh context when
opening the view or refreshing configurations. No project is represented by
projectUri: null, not by silently picking a folder.
configurations returns provider configurations with opaque launch IDs;
bindConfiguration binds an extension-stored configuration to the current
provider. The host keeps provider identity private and rejects launch IDs after
that provider changes. selectPath reviews a declared $projectPath slot and
returns a project-confined canonical URI (or null when dismissed). prepare
accepts previously persisted URI selections for that project's declared path slots;
it revalidates each selection's declared kind and no-follow project confinement,
rejecting undeclared slots. Valid saved selections need no new picker or per-launch
confirmation. It revalidates project trust and provider binding, then resolves the
configuration and descriptor exactly once through that provider. It returns the
configuration plus an opaque single-use process ticket sealed to the provider,
consumer and project. Tagged document paths remain symbolic. Callers cannot claim
another provider's executable or managed-tool authority.
An extension may prepare its own signed managed tool with
processes.execute.prepare({ project, tool, entrypoint, credentials? }, cancellation).
The tool must be in its manifest's toolRequirements with process.framed-json;
both processes.execute and tools.execute permissions are required. No executable
path or dynamic arguments are accepted. Preparation does not start a process and
returns an opaque, one-use ticket bound to the exact calling principal, activation
and current trusted Explorer project. credentials: true enables the native-only
credential channel described below and requires secrets permission. No environment
bindings are accepted. Native start rechecks the manifest, entrypoint, project and
credential-channel permission.
Native jobs, tasks and services with the owner's storage permission receive
ZYNTAX_EXTENSION_DATA, an app-private directory owned by the stable extension ID,
without exposing a path API to the view or accepting a caller-selected directory.
Without that permission no data directory is created or exposed. Store durable
sessions there, not in generation-scoped managed HOME. It survives project
closure, disable, updates and extension reset; uninstall removes it only after
process cleanup. This does not create an OS sandbox.
processes.execute.open(ticket) consumes that authority to open a managed
Content-Length framed JSON process. Inspect its returned state and error: a
startup failure after a managed handle exists can return a terminated snapshot,
whose ID remains usable for cleanup retries. Failures before a handle exists and
cancelled opens reject. send writes one bounded JSON frame, resolving
structured {$documentPath: canonicalUri} references through the trusted project
with no-follow confinement. The host does not interpret DAP/LSP commands.
observe(process, cursor, cancellation) waits for ordered frames and lifecycle
changes without timer polling. Zero starts observation; subsequent calls acknowledge
the last returned cursor. Unsafe, future or expired cursors fail explicitly.
Queues are bounded and lossless: overflow stops the process with an explicit
error, never silently drops protocol messages. Cancelling observation or hiding
a panel does not stop execution. Project closure, trust loss, provider removal
or owner disposal does. stop retries failed cleanup (cleanupPending);
release discards results only after termination and complete cleanup.
editor.context supplies the active source and modified documents. save flushes
that project's editors. replaceMarkers publishes a named owner-scoped group of
themed circles/arrows and optional line highlights, using one-based source
coordinates from 1 through 2147483647. Each call accepts up to 512 markers with
unique IDs (192 characters), a group name (96), labels (256) and canonical project
URIs (4096). These strings must be nonempty and contain no control characters.
Shared marker-count, group-count and encoded-byte capacity limits fail explicitly;
valid per-call input does not reserve global capacity. Interactive groups receive
gutter clicks; mapped marker changes
follow edits. observe batches context, click and marker events. Context and group
snapshots may coalesce, but click loss/queue overflow is explicit; cursor zero
resynchronizes current context and marker groups. The extension stores its own
annotation meaning and settings. reveal navigates to a project-confined source;
resolvePath maps an adapter's path data to a validated canonical URI or null,
never filesystem authority outside the project. No breakpoint-specific storage or
debug session state is required in core. Generic bug, pause, stop,
step-over, step-into and step-out workbench icons follow the active app theme.
Providers with workspace.read can query the host-owned project index through
findFiles({ project, include, exclude?, maxResults }, cancellation). project: null
explicitly means the explorer workspace; a selected context ID means that project.
Globs are relative to that scope; a basename-only glob matches at any depth. Results contain
only canonical document URIs and project-relative paths, are deterministic and
bounded, and remain subject to the same project-confined readText boundary.
readText, readTextIfExists and relativePath take { project, uri }.
relativePath returns its project-relative identity, or null outside the selected
project. The isolated provider never receives a native path or direct filesystem
access. workspace.write.applyEdits likewise takes { project, edits }.
workspace.read.snapshot({ project, uri }, cancellation) reads the current Explorer
project's UTF-8 text, including unsaved editor changes, normalized to LF. It returns
{ uri, exists, text, revision, dirty }; a file absent from both disk and editors
has empty text and exists: false. The opaque revision binds this owner, project
activation and exact disk/editor state. Text is limited to 512 KiB; binary files,
non-file targets and symlinks fail explicitly. listDirectory({ project, uri },
cancellation) lists direct files and directories, including empty folders, as
{ uri, name, kind }. It never follows or returns symlinks and fails above 1024
entries or 512 KiB instead of silently truncating.
workspace.write.writeText({ project, uri, revision, text }, cancellation) proposes
one replacement or new file in the current trusted Explorer project. The host
shows a themed diff overlay; approval saves atomically and updates an open editor
through its normal undo/persistence machinery. Unsaved changes are included in the
review and explicitly saved on approval. Parent directories must already exist.
Changed revisions, revoked access and pre-commit cancellation fail before writing;
the caller must read again rather than blindly retry. Dismissal returns
{ applied: false }. A committed write returns { applied: true, snapshot? };
the snapshot is omitted if cancellation or project closure prevents a fresh read
after the definitive commit. Existing newline conventions are preserved.
Project panels and owned tasks
Native runtime credential channel
A managed process prepared with credentials: true and the extension's secrets
permission can use createRuntimeCredentialClient({ send }) inside its native
worker. send writes its existing Content-Length framed stdout transport. Pass
stdin JSON to client.accept(message) before ordinary protocol dispatch; consumed
responses must never enter UI events, logs or transcripts. Call dispose() when
the worker shuts down. No Node-specific import is required by the helper.
The reserved envelope is { "$zyntax": "credentials", id, method, ...fields }.
Native code consumes it before output retention or bridge events and replies on
ordered stdin as { "$zyntax": "credentials", id, ok: true, result } or
{ "$zyntax": "credentials", id, ok: false, error }, with error limited to
invalid, conflict or unavailable. Public processes.execute.send cannot send
reserved envelopes. Ownership and activation come from the admitted job, never
from an envelope field. Access expires on process stop, owner revocation or project
closure. The encrypted runtime namespace is separate from UI secret references
and existing account stores; uninstall removes it after process cleanup.
get(key) returns { value: string | null, revision: string | null }.
set(key, value, revision) requires that exact revision (null for absent) and
returns { revision }; delete(key, revision) has the same comparison.
list() returns sorted keys only. Concurrent updates fail with an explicit
conflict, not an implicit retry. Keys are 1–128 control-free characters, values
are at most 32 KiB UTF-8, and the owner store is bounded to 128 keys/256 KiB encoded.
Control frames are bounded to 256 KiB; the helper allows 32 pending calls with a
30-second deadline. A timeout does not undo a committed write; read before retrying.
For private interactive input, request an ordinary host-owned secrets.request
prompt through the extension controller and send only its opaque reference to the
worker. resolve(reference, { consume: true }) returns the value directly from
native code and atomically removes that one-use reference. The wire request carries
method: "resolve", reference and boolean consume; omitting helper options
uses false for reusable references. No secret plaintext crosses provider/view RPC.
This boundary is not an OS sandbox against a malicious native program that prints
or transmits credentials it was explicitly granted.
projects.select opens the host's location picker without changing the explorer.
Relative input is explorer-relative; ~/ uses terminal home. Selection keys are
saved locally per extension/workspace, while returned IDs never rebind to another
location. projects.bindIntegrations explicitly shares a selected project with
declared optional integrations after review; it shares no file/secret selections.
Providers receive their scope in context.workspace.project, and task requests
carry both scope and canonical project URI. Reads and language services must use
that scope, not silently substitute the explorer project.
files.select resolves relative input from its explicit project, supports user
selection outside that root, and returns an owned file/directory reference for
process bindings. Descendant paths must remain contained; a file reference allows
no descendants. files.export copies a scoped artifact to a user-chosen destination.
secrets.request uses host-owned private input and encrypted storage. Only references
cross provider RPC and enter task environment bindings, never raw values or argv.
Programs receiving credentials can print them; this is not a sandbox against a
malicious build script. Host records and errors must not disclose credentials.
tasks.execute operates the extension's declared task providers through one
list/start/sessions/observe/reveal/stop/release lifecycle. start resolves scope
and inputs once. Cancellation before a successful response also stops newly spawned
work; after that response, stop owns cancellation. Observation waits for changes
after a monotonic cursor and returns bounded output plus current lifecycle state;
it does not poll or stop work when cancelled. Closing a panel does not stop builds.
Exit frees the PTY, results stay until released, and extension disposal stops owned
work. Root changes affect future launches, not already-running tasks.
Command/runtime task arguments accept ordinary script text, including tab, CR and
LF: at most 32 caller arguments, 16 KiB UTF-8 per literal and 64 KiB total resolved
argv, including native path bindings and runtime prefixes. NUL, other ISO control
characters and malformed Unicode are rejected. Managed-tool arguments and all
environment literals keep their existing 512-code-unit, control-free policy.
This does not change executable selection, project trust, permissions or routing.
isExtensionTaskArgumentLiteral and the EXTENSION_TASK_MAX_ARGUMENT_BYTES /
EXTENSION_TASK_MAX_ARGUMENTS_BYTES constants expose the command/runtime limits.
workbench updates/reveals/closes manifest-declared panels, including dialog
placement. Reuse list/tree/form/detail descriptors and host controls, not a separate
UI framework. Updates preserve stable-ID form drafts unless resetForm is requested.
Per-control disabled state keeps actions such as Stop usable while a panel is busy.
Semantic icons and file/folder icons resolve through app presentation; theme changes
apply live. Isolated custom views use presentation(), revision-bound resolveIcons
and presentation events for the same colors, typography and icons. They receive
no app component imports, stylesheet paths or native asset paths.
SDK 1.0.28 adds the generic projectLifecycle event to the existing custom-view
message bridge. When Explorer closes or replaces its project, closedProjects
contains only this extension's selected project IDs matching that root or a
descendant; unrelated selections are not included. revision increases for the
host lifetime, but a view may skip revisions. Queued notices before bridge readiness
can combine closed IDs and use the latest revision. Trust-only changes do not close
projects. The shared conformance fixture includes single-project and combined notices.
Listen before restoring selected project state, retaining notices until restoration finishes, and stop/release work for matching IDs through the ordinary task and project APIs. Hidden views receive the event too; it does not destroy the renderer or revoke references needed for cleanup. Hiding a panel is still presentation-only and emits no project closure event. This event grants no additional access and exposes no other extension's selections or private paths.
Optional system file actions
SDK 1.0.24 adds kind: "fileOpener" in contributes.workbench, with
placement: "explorer.file", id, label, optional themed icon, and the usual
when/group/order presentation fields. Declare extensions, filenames, an
exact mimeType, and requiredPermissions. Both association arrays are required;
at least one must be nonempty. Extensions are lowercase literal suffixes, including
compound suffixes; filenames are literal basenames. Matching is case-insensitive.
parseFileOpenerOptions and matchesFileOpener provide the shared rules; native
hosts consume the same bounds and patterns from runtime-contract.json.
The package declares files and workbench permissions. It needs no provider,
tool, activation event or executable code. requiredPermissions lists platform
manifest requirements only (use [] when none); it cannot request or grant them.
Unavailable actions are hidden. A user-declined platform setting is distinct from
an absent manifest permission: the operating system owns its consent UI.
The host offers the action for explicit file clicks and the selected file's menu. Existing language/preview handlers retain click priority; multiple matching actions require user selection, never an arbitrary winner. Restore, code navigation and programmatic extension commands cannot launch these actions. Both project files and user-selected provider documents are supported without exposing native paths or file bytes to extension code. The host validates active owner identity, source access, association and manifest requirements before preparing data and launching, grants only read access to the selected file, and cancels pending work when its owner is disposed. Launching a handler does not mean installation or viewing has completed. This contract has no arbitrary intent, package-target or URL parameters.
Preview providers may select registered editor languages or declare strict paths
globs. Project files expose their project-relative path; external and untitled
documents expose their canonical display name. Path selectors route previews without
claiming syntax or language support. Every provider declares one input mode and
one required placement: adjacent toggles in the editor area on phones and sits
beside the editor on wider screens, while replace-editor owns the editor area for
visual resources that have no useful source view. The host derives layout only from
this declaration, never from a package id or filename. The required encoding is
utf8 for text documents or base64 for binary source snapshots; text input
always requires utf8. The same selected provider metadata controls the bounded
file read and read-only editor policy.
text receives the bounded canonical text snapshot and may return structured,
table, plain-text, or bounded HTML markup data. Markup declares linked or
independent scrolling and is limited to inert semantic HTML: scripts, event handlers,
active controls, remote resources, classes, ids, and extension styling are not part of
the surface. The host sanitizes it and applies theme-aware semantic styling inside the
preview pane. document receives metadata only and may select a declared HTML, SVG, or
raster renderer for the exact source snapshot; it never receives bytes, resource URLs,
markup, DOM, Vue, filesystem or native handles, or a WebView.
HTML resources are always script-blocked and declare only whether canonical project
subresources are available. The host validates the bounded result,
mints and disposes the canonical resource session, confines project resources,
and owns the renderer.
Preview request and result budgets are exported as
EXTENSION_PREVIEW_MAX_INPUT_BYTES, EXTENSION_PREVIEW_MAX_BINARY_INPUT_BYTES,
and EXTENSION_PREVIEW_MAX_RESULT_BYTES.
{
"contributes": {
"previewProviders": [{
"id": "preview",
"module": "providers/preview.js",
"export": "createPreview",
"input": "text",
"placement": "adjacent",
"encoding": "utf8",
"languages": [{
"id": "example",
"schemes": ["file", "untitled"],
"group": "example.preview",
"priority": 100,
"composition": "exclusive"
}]
}]
}
}