Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
changeKind: feature
packages:
- "@typespec/compiler"
---

Add the foundation for sandboxed, permissioned execution of libraries and emitters. Libraries/emitters can declare the system capabilities they need via a permission manifest, and users approve them per emitter in `tspconfig.yaml`. By default an emitter/library is granted no access to any system API.

```ts
// In a library/emitter's `$lib`
export const $lib = createTypeSpecLibrary({
name: "@typespec/openapi3",
diagnostics: {},
permissions: [
{ permission: { kind: "fs-read", paths: ["./schemas"] }, reason: "Read shared JSON schemas" },
{ permission: { kind: "network", hosts: ["*.example.com"] }, reason: "Resolve remote refs" },
],
});
```

```yaml
# tspconfig.yaml — the user authorizes what the emitter requested
permissions:
"@typespec/openapi3":
fs-read:
- ./schemas
network:
- "*.example.com"
```
4 changes: 3 additions & 1 deletion packages/compiler/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@
},
"browser": {
"./dist/src/core/node-host.js": "./dist/src/core/node-host.browser.js",
"./dist/src/core/logger/console-sink.js": "./dist/src/core/logger/console-sink.browser.js"
"./dist/src/core/logger/console-sink.js": "./dist/src/core/logger/console-sink.browser.js",
"./dist/src/core/permissions/sandbox/runtime.js": "./dist/src/core/permissions/sandbox/runtime.browser.js",
"./dist/src/core/permissions/sandbox/emit-runner.js": "./dist/src/core/permissions/sandbox/emit-runner.browser.js"
},
"engines": {
"node": ">=22.0.0"
Expand Down
1 change: 1 addition & 0 deletions packages/compiler/src/config/config-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ async function loadConfigFile(
trace: typeof data.trace === "string" ? [data.trace] : data.trace,
emit,
options,
permissions: data.permissions,
linter: data.linter,
});
}
Expand Down
19 changes: 19 additions & 0 deletions packages/compiler/src/config/config-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,25 @@ export const TypeSpecConfigJsonSchema: JSONSchemaType<TypeSpecRawConfig> = {
required: [],
additionalProperties: emitterOptionsSchema,
},
permissions: {
type: "object",
nullable: true,
required: [],
additionalProperties: {
type: "object",
additionalProperties: false,
required: [],
properties: {
"fs-read": { type: "array", nullable: true, items: { type: "string" } },
"fs-write": { type: "array", nullable: true, items: { type: "string" } },
network: { type: "array", nullable: true, items: { type: "string" } },
env: { type: "array", nullable: true, items: { type: "string" } },
exec: {
oneOf: [{ type: "boolean" }, { type: "array", items: { type: "string" } }],
},
},
},
} as any, // ajv optional property typing https://github.com/ajv-validator/ajv/issues/1664
linter: {
type: "object",
nullable: true,
Expand Down
30 changes: 30 additions & 0 deletions packages/compiler/src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,13 @@ export interface TypeSpecConfig {
*/
options?: Record<string, EmitterOptions>;

/**
* Permissions granted to emitters/libraries when running in the sandbox.
* Keyed by the emitter/library name (matching its package name). By default
* an emitter/library is granted nothing and cannot access any system API.
*/
permissions?: Record<string, ConfigPermissionGrant>;

linter?: LinterConfig;
}

Expand Down Expand Up @@ -122,9 +129,32 @@ export interface TypeSpecRawConfig {
emit?: string[];
options?: Record<string, EmitterOptions>;

permissions?: Record<string, ConfigPermissionGrant>;

linter?: LinterConfig;
}

/**
* Permissions a user grants to a specific emitter/library in `tspconfig.yaml`.
* Anything not listed here is denied. Path scopes should be absolute or relative
* to the config file; they are resolved during configuration loading.
*/
export interface ConfigPermissionGrant {
/** Directory/file scopes the emitter/library may read. */
"fs-read"?: string[];
/**
* Directory/file scopes the emitter/library may write, in addition to its own
* emitter output directory which is always granted.
*/
"fs-write"?: string[];
/** Network host patterns the emitter/library may contact (supports `*` and `*.host`). */
network?: string[];
/** Environment variable names the emitter/library may read. */
env?: string[];
/** Allow spawning child processes: `true` for any command or a list of allowed commands. */
exec?: boolean | string[];
}

export interface ConfigEnvironmentVariable {
default: string;
}
Expand Down
6 changes: 6 additions & 0 deletions packages/compiler/src/core/messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -811,6 +811,12 @@ const diagnostics = {
default: paramMessage`Emitter '${"emitterName"}' requires '${"requiredImport"}' to be imported. Add 'import "${"requiredImport"}".`,
},
},
"permission-not-granted": {
severity: "error",
messages: {
default: paramMessage`Emitter '${"emitterName"}' requested permissions that were not granted: ${"permissions"}. To allow them, add the following to your tspconfig.yaml:\n${"suggestion"}`,
},
},

/**
* Linter
Expand Down
8 changes: 8 additions & 0 deletions packages/compiler/src/core/options.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,14 @@ export interface CompilerOptions {
/** Ruleset to enable for linting. */
linterRuleSet?: LinterRuleSet;

/**
* Run emitters inside an OS-isolated sandbox process whose file-system,
* network, environment and child-process access is constrained to the
* permissions the emitter declared and the user approved in `tspconfig.yaml`.
* Defaults to `false` (in-process execution) while the feature is opt-in.
*/
sandbox?: boolean;

/** @internal */
readonly configFile?: TypeSpecConfig;
}
44 changes: 44 additions & 0 deletions packages/compiler/src/core/permissions/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
export {
EMPTY_PERMISSION_SET,
PERMISSION_KINDS,
createPermissionSet,
findMissingPermissions,
formatPermission,
intersectPermissionSets,
isEmptyPermissionSet,
isHostWithinScopes,
isPathWithinScopes,
mergePermissionSets,
} from "./permission-set.js";
export {
PermissionDeniedError,
createPermissionedHost,
createPermissionedSystemHost,
} from "./permissioned-host.js";
export {
ESSENTIAL_ENV_NAMES,
buildSandboxEnv,
permissionSetToNodeArgs,
} from "./sandbox/node-args.js";
export type { NodeSandboxArgsOptions } from "./sandbox/node-args.js";
export { runInSandbox } from "./sandbox/runtime.js";
export type { RunInSandboxOptions } from "./sandbox/runtime.js";
export type { SandboxContext } from "./sandbox/bootstrap.js";
export {
configGrantToPermissionSet,
formatGrantSuggestion,
manifestToPermissionSet,
resolvePermissions,
} from "./resolve.js";
export type { PermissionGrantInput, PermissionResolution, ResolveGrantOptions } from "./resolve.js";
export type {
EnvPermission,
ExecPermission,
FsReadPermission,
FsWritePermission,
NetworkPermission,
Permission,
PermissionKind,
PermissionRequest,
PermissionSet,
} from "./types.js";
Loading
Loading