@github/copilot-sdk 1.0.7-preview.2 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -84,9 +84,11 @@ new CopilotClient(options?: CopilotClientOptions)
84
84
  **Options:**
85
85
 
86
86
  - `connection?: RuntimeConnection` - How to connect to the Copilot runtime. Construct via the factory functions on `RuntimeConnection`:
87
- - `RuntimeConnection.forStdio({ path?, args? })` (default) — spawn the runtime and communicate over its stdin/stdout.
88
- - `RuntimeConnection.forTcp({ port?, connectionToken?, path?, args? })` — spawn the runtime as a TCP server.
87
+ - `RuntimeConnection.forStdio({ path?, args?, env? })` (default) — spawn the runtime and communicate over its stdin/stdout.
88
+ - `RuntimeConnection.forTcp({ port?, connectionToken?, path?, args?, env? })` — spawn the runtime as a TCP server.
89
89
  - `RuntimeConnection.forUri(url, { connectionToken? })` — connect to an already-running runtime (mutually exclusive with `gitHubToken`/`useLoggedInUser`). There is no top-level `cliUrl` shortcut; use this factory for URL-based connections.
90
+ - `RuntimeConnection.forInProcess()` — host the runtime in-process over its native C ABI (FFI). **Experimental.** Because the runtime shares this process, `env`, `telemetry`, and `workingDirectory` are rejected with this transport; set them on the host process instead.
91
+ - The child-process transports (`forStdio`/`forTcp`) also accept a per-connection `env`. Set it there or via the top-level `env` option — not both (setting both throws).
90
92
  - `mode?: "empty" | "copilot-cli"` - Defaulting strategy. Use `"empty"` for multi-user server mode; defaults to `"copilot-cli"`.
91
93
  - `workingDirectory?: string` - Working directory for the runtime process (default: current process cwd).
92
94
  - `baseDirectory?: string` - Base directory for Copilot data (session state, config, etc.). Sets `COPILOT_HOME` on the spawned runtime. When not set, the runtime defaults to `~/.copilot`. Ignored when connecting via `RuntimeConnection.forUri`.
@@ -395,6 +395,21 @@ class CopilotClient {
395
395
  "workingDirectory is not supported with RuntimeConnection.forInProcess(): the in-process transport hosts the runtime in this process, so honoring it would require mutating the shared process-global cwd. Change the host process's working directory before constructing the client instead."
396
396
  );
397
397
  }
398
+ if (conn.kind === "inprocess" && options.env !== void 0) {
399
+ throw new Error(
400
+ "env is not supported with RuntimeConnection.forInProcess(): the in-process transport loads the native runtime into the shared host process, whose single environment block cannot carry per-client values. Set the variables on the host process environment instead."
401
+ );
402
+ }
403
+ if (conn.kind === "inprocess" && options.telemetry !== void 0) {
404
+ throw new Error(
405
+ "telemetry is not supported with RuntimeConnection.forInProcess(): telemetry configuration is lowered to environment variables read by native runtime code running in the shared host process, so per-client telemetry cannot be honored in-process. Configure telemetry via the host process environment, or use a child-process transport."
406
+ );
407
+ }
408
+ if ((conn.kind === "stdio" || conn.kind === "tcp") && conn.env !== void 0 && options.env !== void 0) {
409
+ throw new Error(
410
+ "Set environment variables via either the client-level env option or the connection's env (RuntimeConnection.forStdio/forTcp), not both. Prefer the connection-level env for child-process transports."
411
+ );
412
+ }
398
413
  if (conn.kind === "tcp" && conn.connectionToken !== void 0) {
399
414
  if (typeof conn.connectionToken !== "string" || conn.connectionToken.length === 0) {
400
415
  throw new Error("connectionToken must be a non-empty string");
@@ -423,7 +438,8 @@ class CopilotClient {
423
438
  this.requestHandler = options.requestHandler ?? null;
424
439
  this.onGitHubTelemetry = options.onGitHubTelemetry;
425
440
  this.setupClientGlobalHandlers();
426
- const effectiveEnv = options.env ?? process.env;
441
+ const connEnv = conn.kind === "stdio" || conn.kind === "tcp" ? conn.env : void 0;
442
+ const effectiveEnv = connEnv ?? options.env ?? process.env;
427
443
  this.resolvedEnv = effectiveEnv;
428
444
  this.resolvedCliPath = conn.kind === "stdio" || conn.kind === "tcp" ? conn.path ?? effectiveEnv.COPILOT_CLI_PATH ?? getBundledCliPath() : void 0;
429
445
  const connArgs = conn.kind === "stdio" || conn.kind === "tcp" ? conn.args ?? [] : [];
@@ -504,6 +520,9 @@ class CopilotClient {
504
520
  }
505
521
  setupClientGlobalHandlers() {
506
522
  const handlers = {};
523
+ handlers.hooks = {
524
+ invoke: async (params) => await this.handleHooksInvoke(params)
525
+ };
507
526
  if (this.requestHandler) {
508
527
  handlers.llmInference = (0, import_copilotRequestHandler.createCopilotRequestAdapter)(this.requestHandler, () => {
509
528
  if (!this.connection) {
@@ -1058,8 +1077,10 @@ class CopilotClient {
1058
1077
  parameters: toJsonSchema(tool.parameters),
1059
1078
  overridesBuiltInTool: tool.overridesBuiltInTool,
1060
1079
  skipPermission: tool.skipPermission,
1061
- defer: tool.defer
1080
+ defer: tool.defer,
1081
+ metadata: tool.metadata
1062
1082
  })),
1083
+ toolSearch: config.toolSearch,
1063
1084
  canvases: config.canvases?.map((canvas) => canvas.declaration),
1064
1085
  requestCanvasRenderer: config.requestCanvasRenderer,
1065
1086
  requestExtensions: config.requestExtensions,
@@ -1256,8 +1277,10 @@ class CopilotClient {
1256
1277
  parameters: toJsonSchema(tool.parameters),
1257
1278
  overridesBuiltInTool: tool.overridesBuiltInTool,
1258
1279
  skipPermission: tool.skipPermission,
1259
- defer: tool.defer
1280
+ defer: tool.defer,
1281
+ metadata: tool.metadata
1260
1282
  })),
1283
+ toolSearch: config.toolSearch,
1261
1284
  canvases: config.canvases?.map((canvas) => canvas.declaration),
1262
1285
  requestCanvasRenderer: config.requestCanvasRenderer,
1263
1286
  requestExtensions: config.requestExtensions,
@@ -1861,21 +1884,42 @@ stderr: ${stderrOutput}`
1861
1884
  return this.connectViaTcp();
1862
1885
  }
1863
1886
  }
1864
- /**
1865
- * Start the in-process FFI runtime host: resolve the CLI entrypoint and native
1866
- * runtime library, then let the native host spawn the CLI worker.
1867
- *
1868
- * The worker inherits this host process's ambient environment; per-client options
1869
- * that lower to environment variables (`env`, `telemetry`, `gitHubToken`,
1870
- * `baseDirectory`) are intentionally not applied here, because the native runtime
1871
- * loads into the shared host process and a single env block cannot carry per-client
1872
- * values. Configure the in-process runtime via the host process environment instead.
1873
- * See https://github.com/github/copilot-sdk/issues/1934.
1874
- */
1887
+ /** Starts the in-process FFI runtime with SDK-managed typed options. */
1875
1888
  async startInProcessFfi() {
1876
1889
  const entrypoint = this.resolveCliPathForFfi();
1877
1890
  const { FfiRuntimeHost } = await import("./ffiRuntimeHost.js");
1878
- const host = FfiRuntimeHost.create(entrypoint, CopilotClient.getNapiPrebuildsFolder());
1891
+ const environment = {};
1892
+ if (this.options.gitHubToken) {
1893
+ environment.COPILOT_SDK_AUTH_TOKEN = this.options.gitHubToken;
1894
+ }
1895
+ if (this.options.baseDirectory) {
1896
+ environment.COPILOT_HOME = this.options.baseDirectory;
1897
+ }
1898
+ if (this.options.mode === "empty") {
1899
+ environment.COPILOT_DISABLE_KEYTAR = "1";
1900
+ }
1901
+ const args = [];
1902
+ if (this.options.logLevel) {
1903
+ args.push("--log-level", this.options.logLevel);
1904
+ }
1905
+ if (this.options.gitHubToken) {
1906
+ args.push("--auth-token-env", "COPILOT_SDK_AUTH_TOKEN");
1907
+ }
1908
+ if (!this.options.useLoggedInUser) {
1909
+ args.push("--no-auto-login");
1910
+ }
1911
+ if (this.options.sessionIdleTimeoutSeconds > 0) {
1912
+ args.push("--session-idle-timeout", this.options.sessionIdleTimeoutSeconds.toString());
1913
+ }
1914
+ if (this.options.enableRemoteSessions) {
1915
+ args.push("--remote");
1916
+ }
1917
+ const host = FfiRuntimeHost.create(
1918
+ entrypoint,
1919
+ CopilotClient.getNapiPrebuildsFolder(entrypoint),
1920
+ environment,
1921
+ args
1922
+ );
1879
1923
  this.ffiHost = host;
1880
1924
  await host.start();
1881
1925
  }
@@ -1905,14 +1949,30 @@ stderr: ${stderrOutput}`
1905
1949
  /**
1906
1950
  * Returns the napi prebuilds folder name for the current host — the
1907
1951
  * `<node-platform>-<arch>` convention (e.g. `win32-x64`, `darwin-arm64`,
1908
- * `linux-x64`) under which the runtime ships `prebuilds/<folder>/runtime.node`.
1952
+ * `linux-x64`, `linuxmusl-x64`) under which the runtime ships
1953
+ * `prebuilds/<folder>/runtime.node`.
1909
1954
  */
1910
- static getNapiPrebuildsFolder() {
1955
+ static getNapiPrebuildsFolder(entrypoint) {
1911
1956
  const arch = process.arch;
1912
1957
  if (arch !== "x64" && arch !== "arm64") {
1913
1958
  throw new Error(`Unsupported architecture '${arch}' for in-process FFI hosting.`);
1914
1959
  }
1915
- return `${process.platform}-${arch}`;
1960
+ let platform = process.platform;
1961
+ if (platform === "linux" && CopilotClient.isMusl(entrypoint)) {
1962
+ platform = "linuxmusl";
1963
+ }
1964
+ return `${platform}-${arch}`;
1965
+ }
1966
+ static isMusl(entrypoint) {
1967
+ if (entrypoint.includes(`copilot-linuxmusl-${process.arch}`)) {
1968
+ return true;
1969
+ }
1970
+ if (entrypoint.includes(`copilot-linux-${process.arch}`)) {
1971
+ return false;
1972
+ }
1973
+ const report = process.report?.getReport();
1974
+ const header = report && "header" in report ? report.header : void 0;
1975
+ return header !== void 0 && header.glibcVersionRuntime === void 0;
1916
1976
  }
1917
1977
  /**
1918
1978
  * Connect to child via stdio pipes
@@ -2008,10 +2068,6 @@ stderr: ${stderrOutput}`
2008
2068
  "autoModeSwitch.request",
2009
2069
  async (params) => await this.handleAutoModeSwitchRequest(params)
2010
2070
  );
2011
- this.connection.onRequest(
2012
- "hooks.invoke",
2013
- async (params) => await this.handleHooksInvoke(params)
2014
- );
2015
2071
  this.connection.onRequest(
2016
2072
  "systemMessage.transform",
2017
2073
  async (params) => await this.handleSystemMessageTransform(params)
@@ -84,8 +84,9 @@ function loadLibrary(libraryPath) {
84
84
  loadedLibraryPath = libraryPath;
85
85
  return loadedLibrary;
86
86
  }
87
- function buildArgvJson(cliEntrypoint) {
87
+ function buildArgvJson(cliEntrypoint, args) {
88
88
  const argv = cliEntrypoint.toLowerCase().endsWith(".js") ? ["node", cliEntrypoint, "--embedded-host", "--no-auto-update"] : [cliEntrypoint, "--embedded-host", "--no-auto-update"];
89
+ argv.push(...args);
89
90
  return Buffer.from(JSON.stringify(argv), "utf8");
90
91
  }
91
92
  function buildEnvJson(environment) {
@@ -104,10 +105,11 @@ function buildEnvJson(environment) {
104
105
  return Buffer.from(JSON.stringify(obj), "utf8");
105
106
  }
106
107
  class FfiRuntimeHost {
107
- constructor(libraryPath, cliEntrypoint, environment) {
108
+ constructor(libraryPath, cliEntrypoint, environment, args) {
108
109
  this.libraryPath = libraryPath;
109
110
  this.cliEntrypoint = cliEntrypoint;
110
111
  this.environment = environment;
112
+ this.args = args;
111
113
  this.lib = loadLibrary(libraryPath);
112
114
  this.receiveStream = new import_node_stream.PassThrough();
113
115
  this.sendStream = new import_node_stream.Writable({
@@ -126,6 +128,7 @@ class FfiRuntimeHost {
126
128
  libraryPath;
127
129
  cliEntrypoint;
128
130
  environment;
131
+ args;
129
132
  lib;
130
133
  serverId = 0;
131
134
  connectionId = 0;
@@ -142,21 +145,21 @@ class FfiRuntimeHost {
142
145
  * the entrypoint directory (the napi-rs `<node-platform>-<arch>` layout, e.g.
143
146
  * `linux-x64`). Throws if it cannot be found.
144
147
  */
145
- static create(cliEntrypoint, prebuildsFolder, environment) {
148
+ static create(cliEntrypoint, prebuildsFolder, environment, args) {
146
149
  const fullEntrypoint = (0, import_node_path.resolve)(cliEntrypoint);
147
150
  const distDir = (0, import_node_path.dirname)(fullEntrypoint);
148
151
  const libraryPath = (0, import_node_path.join)(distDir, "prebuilds", prebuildsFolder, "runtime.node");
149
152
  if (!(0, import_node_fs.existsSync)(libraryPath)) {
150
153
  throw new Error(`FFI runtime library not found. Looked for '${libraryPath}'.`);
151
154
  }
152
- return new FfiRuntimeHost(libraryPath, fullEntrypoint, environment);
155
+ return new FfiRuntimeHost(libraryPath, fullEntrypoint, environment, args);
153
156
  }
154
157
  /**
155
158
  * Starts the in-process runtime: spawns the CLI worker via the native host,
156
159
  * waits for readiness, and opens the FFI JSON-RPC connection.
157
160
  */
158
161
  async start() {
159
- const argvJson = buildArgvJson(this.cliEntrypoint);
162
+ const argvJson = buildArgvJson(this.cliEntrypoint, this.args);
160
163
  const envJson = buildEnvJson(this.environment);
161
164
  this.serverId = await new Promise((resolvePromise, rejectPromise) => {
162
165
  this.lib.hostStart.async(
@@ -223,7 +223,7 @@ function createServerRpc(connection) {
223
223
  /**
224
224
  * Registers a new marketplace from a source (owner/repo, URL, or local path).
225
225
  *
226
- * @param params Marketplace source to register.
226
+ * @param params Marketplace source and optional working directory for relative-path resolution.
227
227
  *
228
228
  * @returns Result of registering a new marketplace.
229
229
  */
@@ -1119,7 +1119,7 @@ function createSessionRpc(connection, sessionId) {
1119
1119
  */
1120
1120
  list: async () => connection.sendRequest("session.mcp.list", { sessionId }),
1121
1121
  /**
1122
- * Lists the tools exposed by a connected MCP server on this session's host.
1122
+ * Lists the tools exposed by a connected MCP server on this session's host. This performs a live `tools/list` request. Tool UI metadata is returned independently of whether MCP Apps rendering is enabled for the session.
1123
1123
  *
1124
1124
  * @param params Server name whose tool list should be returned.
1125
1125
  *
@@ -1231,13 +1231,11 @@ function createSessionRpc(connection, sessionId) {
1231
1231
  /** @experimental */
1232
1232
  apps: {
1233
1233
  /**
1234
- * Deprecated/obsolete alias for `session.mcp.resources.read`; retained for backwards compatibility with earlier MCP Apps host integrations.
1234
+ * Fetch an MCP resource (typically a `ui://` MCP App bundle, per SEP-1865) from a connected server. Requires the `mcp-apps` session capability.
1235
1235
  *
1236
- * @param params Deprecated/obsolete MCP Apps alias for `McpResourcesReadRequest`; use `session.mcp.resources.read` instead.
1237
- *
1238
- * @returns Deprecated/obsolete MCP Apps alias for `McpResourcesReadResult`; use `session.mcp.resources.read` instead.
1236
+ * @param params MCP server and resource URI to fetch.
1239
1237
  *
1240
- * @deprecated
1238
+ * @returns Resource contents returned by the MCP server.
1241
1239
  */
1242
1240
  readResource: async (params) => connection.sendRequest("session.mcp.apps.readResource", { sessionId, ...params }),
1243
1241
  /**
@@ -1804,11 +1802,11 @@ function createSessionRpc(connection, sessionId) {
1804
1802
  */
1805
1803
  recordContextChange: async (params) => connection.sendRequest("session.metadata.recordContextChange", { sessionId, ...params }),
1806
1804
  /**
1807
- * Updates the session's recorded working directory.
1805
+ * Updates the session's working directory. For local sessions the target is validated first (an absolute path that exists on disk) and the permission primary directory is re-based; a rejected validation fails the call before any session state changes.
1808
1806
  *
1809
- * @param params Absolute path to set as the session's new working directory.
1807
+ * @param params Absolute path to set as the session's new working directory. For local sessions the path must be absolute and exist on disk: it is validated before any session state changes, and a failing validation rejects the call with nothing mutated, persisted, or emitted. Remote sessions record the path as-is.
1810
1808
  *
1811
- * @returns Update the session's working directory. Used by the host when the user explicitly changes cwd (e.g., the `/cd` slash command). The host is responsible for `process.chdir` and any related side-effects (file index, etc.); this method only updates the session's own recorded path.
1809
+ * @returns Update the session's working directory. Used by the host when the user explicitly changes cwd (e.g., the `/cd` slash command). The host is responsible for any related side-effects (file index, etc.); it does NOT change the process working directory (a session's cwd is per-session, not process-global). For local sessions the runtime validates the target first (an absolute path that exists on disk) and re-bases the permission primary directory; a rejected validation fails the call before anything is mutated, persisted, or emitted. Location-scoped permission rules are then re-keyed to the new directory (best-effort). Remote sessions only record the path.
1812
1810
  */
1813
1811
  setWorkingDirectory: async (params) => connection.sendRequest("session.metadata.setWorkingDirectory", { sessionId, ...params }),
1814
1812
  /**
@@ -2147,6 +2145,11 @@ function registerClientSessionApiHandlers(connection, getHandlers) {
2147
2145
  });
2148
2146
  }
2149
2147
  function registerClientGlobalApiHandlers(connection, handlers) {
2148
+ connection.onRequest("hooks.invoke", async (params) => {
2149
+ const handler = handlers.hooks;
2150
+ if (!handler) throw new Error("No hooks client-global handler registered");
2151
+ return handler.invoke(params);
2152
+ });
2150
2153
  connection.onRequest("llmInference.httpRequestStart", async (params) => {
2151
2154
  const handler = handlers.llmInference;
2152
2155
  if (!handler) throw new Error("No llmInference client-global handler registered");
@@ -40,6 +40,7 @@ function isOpenCanvasInstance(value) {
40
40
  const instance = value;
41
41
  return typeof instance.instanceId === "string" && instance.instanceId.length > 0 && typeof instance.extensionId === "string" && instance.extensionId.length > 0 && typeof instance.canvasId === "string" && instance.canvasId.length > 0;
42
42
  }
43
+ const TOOL_SEARCH_TOOL_NAME = "tool_search_tool";
43
44
  class CopilotSession {
44
45
  /**
45
46
  * Creates a new CopilotSession instance.
@@ -352,11 +353,21 @@ class CopilotSession {
352
353
  */
353
354
  async _executeToolAndRespond(requestId, toolName, toolCallId, args, handler, traceparent, tracestate) {
354
355
  try {
356
+ let availableTools;
357
+ if (toolName === TOOL_SEARCH_TOOL_NAME) {
358
+ try {
359
+ const metadata = await this.rpc.tools.getCurrentMetadata();
360
+ availableTools = metadata.tools ?? void 0;
361
+ } catch {
362
+ availableTools = void 0;
363
+ }
364
+ }
355
365
  const rawResult = await handler(args, {
356
366
  sessionId: this.sessionId,
357
367
  toolCallId,
358
368
  toolName,
359
369
  arguments: args,
370
+ availableTools,
360
371
  traceparent,
361
372
  tracestate
362
373
  });
@@ -1009,7 +1020,7 @@ class CopilotSession {
1009
1020
  *
1010
1021
  * @example
1011
1022
  * ```typescript
1012
- * await session.setModel("gpt-4.1");
1023
+ * await session.setModel("gpt-5.4");
1013
1024
  * await session.setModel("claude-sonnet-4.6", { reasoningEffort: "high" });
1014
1025
  * ```
1015
1026
  */
package/dist/cjs/types.js CHANGED
@@ -39,7 +39,7 @@ const RuntimeConnection = {
39
39
  * This is the default if no {@link CopilotClientOptions.connection} is set.
40
40
  */
41
41
  forStdio(opts = {}) {
42
- return { kind: "stdio", path: opts.path, args: opts.args };
42
+ return { kind: "stdio", path: opts.path, args: opts.args, env: opts.env };
43
43
  },
44
44
  /**
45
45
  * Spawn a runtime child process that listens on a TCP socket and connect to it.
@@ -50,7 +50,8 @@ const RuntimeConnection = {
50
50
  port: opts.port,
51
51
  connectionToken: opts.connectionToken,
52
52
  path: opts.path,
53
- args: opts.args
53
+ args: opts.args,
54
+ env: opts.env
54
55
  };
55
56
  },
56
57
  /**
package/dist/client.d.ts CHANGED
@@ -435,17 +435,7 @@ export declare class CopilotClient {
435
435
  * Connect to the CLI server (via socket or stdio)
436
436
  */
437
437
  private connectToServer;
438
- /**
439
- * Start the in-process FFI runtime host: resolve the CLI entrypoint and native
440
- * runtime library, then let the native host spawn the CLI worker.
441
- *
442
- * The worker inherits this host process's ambient environment; per-client options
443
- * that lower to environment variables (`env`, `telemetry`, `gitHubToken`,
444
- * `baseDirectory`) are intentionally not applied here, because the native runtime
445
- * loads into the shared host process and a single env block cannot carry per-client
446
- * values. Configure the in-process runtime via the host process environment instead.
447
- * See https://github.com/github/copilot-sdk/issues/1934.
448
- */
438
+ /** Starts the in-process FFI runtime with SDK-managed typed options. */
449
439
  private startInProcessFfi;
450
440
  /**
451
441
  * Connect to the in-process FFI runtime host over its receive/send streams,
@@ -460,9 +450,11 @@ export declare class CopilotClient {
460
450
  /**
461
451
  * Returns the napi prebuilds folder name for the current host — the
462
452
  * `<node-platform>-<arch>` convention (e.g. `win32-x64`, `darwin-arm64`,
463
- * `linux-x64`) under which the runtime ships `prebuilds/<folder>/runtime.node`.
453
+ * `linux-x64`, `linuxmusl-x64`) under which the runtime ships
454
+ * `prebuilds/<folder>/runtime.node`.
464
455
  */
465
456
  private static getNapiPrebuildsFolder;
457
+ private static isMusl;
466
458
  /**
467
459
  * Connect to child via stdio pipes
468
460
  */
package/dist/client.js CHANGED
@@ -372,6 +372,21 @@ class CopilotClient {
372
372
  "workingDirectory is not supported with RuntimeConnection.forInProcess(): the in-process transport hosts the runtime in this process, so honoring it would require mutating the shared process-global cwd. Change the host process's working directory before constructing the client instead."
373
373
  );
374
374
  }
375
+ if (conn.kind === "inprocess" && options.env !== void 0) {
376
+ throw new Error(
377
+ "env is not supported with RuntimeConnection.forInProcess(): the in-process transport loads the native runtime into the shared host process, whose single environment block cannot carry per-client values. Set the variables on the host process environment instead."
378
+ );
379
+ }
380
+ if (conn.kind === "inprocess" && options.telemetry !== void 0) {
381
+ throw new Error(
382
+ "telemetry is not supported with RuntimeConnection.forInProcess(): telemetry configuration is lowered to environment variables read by native runtime code running in the shared host process, so per-client telemetry cannot be honored in-process. Configure telemetry via the host process environment, or use a child-process transport."
383
+ );
384
+ }
385
+ if ((conn.kind === "stdio" || conn.kind === "tcp") && conn.env !== void 0 && options.env !== void 0) {
386
+ throw new Error(
387
+ "Set environment variables via either the client-level env option or the connection's env (RuntimeConnection.forStdio/forTcp), not both. Prefer the connection-level env for child-process transports."
388
+ );
389
+ }
375
390
  if (conn.kind === "tcp" && conn.connectionToken !== void 0) {
376
391
  if (typeof conn.connectionToken !== "string" || conn.connectionToken.length === 0) {
377
392
  throw new Error("connectionToken must be a non-empty string");
@@ -400,7 +415,8 @@ class CopilotClient {
400
415
  this.requestHandler = options.requestHandler ?? null;
401
416
  this.onGitHubTelemetry = options.onGitHubTelemetry;
402
417
  this.setupClientGlobalHandlers();
403
- const effectiveEnv = options.env ?? process.env;
418
+ const connEnv = conn.kind === "stdio" || conn.kind === "tcp" ? conn.env : void 0;
419
+ const effectiveEnv = connEnv ?? options.env ?? process.env;
404
420
  this.resolvedEnv = effectiveEnv;
405
421
  this.resolvedCliPath = conn.kind === "stdio" || conn.kind === "tcp" ? conn.path ?? effectiveEnv.COPILOT_CLI_PATH ?? getBundledCliPath() : void 0;
406
422
  const connArgs = conn.kind === "stdio" || conn.kind === "tcp" ? conn.args ?? [] : [];
@@ -481,6 +497,9 @@ class CopilotClient {
481
497
  }
482
498
  setupClientGlobalHandlers() {
483
499
  const handlers = {};
500
+ handlers.hooks = {
501
+ invoke: async (params) => await this.handleHooksInvoke(params)
502
+ };
484
503
  if (this.requestHandler) {
485
504
  handlers.llmInference = createCopilotRequestAdapter(this.requestHandler, () => {
486
505
  if (!this.connection) {
@@ -1035,8 +1054,10 @@ class CopilotClient {
1035
1054
  parameters: toJsonSchema(tool.parameters),
1036
1055
  overridesBuiltInTool: tool.overridesBuiltInTool,
1037
1056
  skipPermission: tool.skipPermission,
1038
- defer: tool.defer
1057
+ defer: tool.defer,
1058
+ metadata: tool.metadata
1039
1059
  })),
1060
+ toolSearch: config.toolSearch,
1040
1061
  canvases: config.canvases?.map((canvas) => canvas.declaration),
1041
1062
  requestCanvasRenderer: config.requestCanvasRenderer,
1042
1063
  requestExtensions: config.requestExtensions,
@@ -1233,8 +1254,10 @@ class CopilotClient {
1233
1254
  parameters: toJsonSchema(tool.parameters),
1234
1255
  overridesBuiltInTool: tool.overridesBuiltInTool,
1235
1256
  skipPermission: tool.skipPermission,
1236
- defer: tool.defer
1257
+ defer: tool.defer,
1258
+ metadata: tool.metadata
1237
1259
  })),
1260
+ toolSearch: config.toolSearch,
1238
1261
  canvases: config.canvases?.map((canvas) => canvas.declaration),
1239
1262
  requestCanvasRenderer: config.requestCanvasRenderer,
1240
1263
  requestExtensions: config.requestExtensions,
@@ -1838,21 +1861,42 @@ stderr: ${stderrOutput}`
1838
1861
  return this.connectViaTcp();
1839
1862
  }
1840
1863
  }
1841
- /**
1842
- * Start the in-process FFI runtime host: resolve the CLI entrypoint and native
1843
- * runtime library, then let the native host spawn the CLI worker.
1844
- *
1845
- * The worker inherits this host process's ambient environment; per-client options
1846
- * that lower to environment variables (`env`, `telemetry`, `gitHubToken`,
1847
- * `baseDirectory`) are intentionally not applied here, because the native runtime
1848
- * loads into the shared host process and a single env block cannot carry per-client
1849
- * values. Configure the in-process runtime via the host process environment instead.
1850
- * See https://github.com/github/copilot-sdk/issues/1934.
1851
- */
1864
+ /** Starts the in-process FFI runtime with SDK-managed typed options. */
1852
1865
  async startInProcessFfi() {
1853
1866
  const entrypoint = this.resolveCliPathForFfi();
1854
1867
  const { FfiRuntimeHost } = await import("./ffiRuntimeHost.js");
1855
- const host = FfiRuntimeHost.create(entrypoint, CopilotClient.getNapiPrebuildsFolder());
1868
+ const environment = {};
1869
+ if (this.options.gitHubToken) {
1870
+ environment.COPILOT_SDK_AUTH_TOKEN = this.options.gitHubToken;
1871
+ }
1872
+ if (this.options.baseDirectory) {
1873
+ environment.COPILOT_HOME = this.options.baseDirectory;
1874
+ }
1875
+ if (this.options.mode === "empty") {
1876
+ environment.COPILOT_DISABLE_KEYTAR = "1";
1877
+ }
1878
+ const args = [];
1879
+ if (this.options.logLevel) {
1880
+ args.push("--log-level", this.options.logLevel);
1881
+ }
1882
+ if (this.options.gitHubToken) {
1883
+ args.push("--auth-token-env", "COPILOT_SDK_AUTH_TOKEN");
1884
+ }
1885
+ if (!this.options.useLoggedInUser) {
1886
+ args.push("--no-auto-login");
1887
+ }
1888
+ if (this.options.sessionIdleTimeoutSeconds > 0) {
1889
+ args.push("--session-idle-timeout", this.options.sessionIdleTimeoutSeconds.toString());
1890
+ }
1891
+ if (this.options.enableRemoteSessions) {
1892
+ args.push("--remote");
1893
+ }
1894
+ const host = FfiRuntimeHost.create(
1895
+ entrypoint,
1896
+ CopilotClient.getNapiPrebuildsFolder(entrypoint),
1897
+ environment,
1898
+ args
1899
+ );
1856
1900
  this.ffiHost = host;
1857
1901
  await host.start();
1858
1902
  }
@@ -1882,14 +1926,30 @@ stderr: ${stderrOutput}`
1882
1926
  /**
1883
1927
  * Returns the napi prebuilds folder name for the current host — the
1884
1928
  * `<node-platform>-<arch>` convention (e.g. `win32-x64`, `darwin-arm64`,
1885
- * `linux-x64`) under which the runtime ships `prebuilds/<folder>/runtime.node`.
1929
+ * `linux-x64`, `linuxmusl-x64`) under which the runtime ships
1930
+ * `prebuilds/<folder>/runtime.node`.
1886
1931
  */
1887
- static getNapiPrebuildsFolder() {
1932
+ static getNapiPrebuildsFolder(entrypoint) {
1888
1933
  const arch = process.arch;
1889
1934
  if (arch !== "x64" && arch !== "arm64") {
1890
1935
  throw new Error(`Unsupported architecture '${arch}' for in-process FFI hosting.`);
1891
1936
  }
1892
- return `${process.platform}-${arch}`;
1937
+ let platform = process.platform;
1938
+ if (platform === "linux" && CopilotClient.isMusl(entrypoint)) {
1939
+ platform = "linuxmusl";
1940
+ }
1941
+ return `${platform}-${arch}`;
1942
+ }
1943
+ static isMusl(entrypoint) {
1944
+ if (entrypoint.includes(`copilot-linuxmusl-${process.arch}`)) {
1945
+ return true;
1946
+ }
1947
+ if (entrypoint.includes(`copilot-linux-${process.arch}`)) {
1948
+ return false;
1949
+ }
1950
+ const report = process.report?.getReport();
1951
+ const header = report && "header" in report ? report.header : void 0;
1952
+ return header !== void 0 && header.glibcVersionRuntime === void 0;
1893
1953
  }
1894
1954
  /**
1895
1955
  * Connect to child via stdio pipes
@@ -1985,10 +2045,6 @@ stderr: ${stderrOutput}`
1985
2045
  "autoModeSwitch.request",
1986
2046
  async (params) => await this.handleAutoModeSwitchRequest(params)
1987
2047
  );
1988
- this.connection.onRequest(
1989
- "hooks.invoke",
1990
- async (params) => await this.handleHooksInvoke(params)
1991
- );
1992
2048
  this.connection.onRequest(
1993
2049
  "systemMessage.transform",
1994
2050
  async (params) => await this.handleSystemMessageTransform(params)
@@ -2,7 +2,8 @@ import { PassThrough, Writable } from "node:stream";
2
2
  export declare class FfiRuntimeHost {
3
3
  private readonly libraryPath;
4
4
  private readonly cliEntrypoint;
5
- private readonly environment?;
5
+ private readonly environment;
6
+ private readonly args;
6
7
  private readonly lib;
7
8
  private serverId;
8
9
  private connectionId;
@@ -20,7 +21,7 @@ export declare class FfiRuntimeHost {
20
21
  * the entrypoint directory (the napi-rs `<node-platform>-<arch>` layout, e.g.
21
22
  * `linux-x64`). Throws if it cannot be found.
22
23
  */
23
- static create(cliEntrypoint: string, prebuildsFolder: string, environment?: Record<string, string | undefined>): FfiRuntimeHost;
24
+ static create(cliEntrypoint: string, prebuildsFolder: string, environment: Record<string, string | undefined> | undefined, args: readonly string[]): FfiRuntimeHost;
24
25
  /**
25
26
  * Starts the in-process runtime: spawns the CLI worker via the native host,
26
27
  * waits for readiness, and opens the FFI JSON-RPC connection.
@@ -51,8 +51,9 @@ function loadLibrary(libraryPath) {
51
51
  loadedLibraryPath = libraryPath;
52
52
  return loadedLibrary;
53
53
  }
54
- function buildArgvJson(cliEntrypoint) {
54
+ function buildArgvJson(cliEntrypoint, args) {
55
55
  const argv = cliEntrypoint.toLowerCase().endsWith(".js") ? ["node", cliEntrypoint, "--embedded-host", "--no-auto-update"] : [cliEntrypoint, "--embedded-host", "--no-auto-update"];
56
+ argv.push(...args);
56
57
  return Buffer.from(JSON.stringify(argv), "utf8");
57
58
  }
58
59
  function buildEnvJson(environment) {
@@ -71,10 +72,11 @@ function buildEnvJson(environment) {
71
72
  return Buffer.from(JSON.stringify(obj), "utf8");
72
73
  }
73
74
  class FfiRuntimeHost {
74
- constructor(libraryPath, cliEntrypoint, environment) {
75
+ constructor(libraryPath, cliEntrypoint, environment, args) {
75
76
  this.libraryPath = libraryPath;
76
77
  this.cliEntrypoint = cliEntrypoint;
77
78
  this.environment = environment;
79
+ this.args = args;
78
80
  this.lib = loadLibrary(libraryPath);
79
81
  this.receiveStream = new PassThrough();
80
82
  this.sendStream = new Writable({
@@ -93,6 +95,7 @@ class FfiRuntimeHost {
93
95
  libraryPath;
94
96
  cliEntrypoint;
95
97
  environment;
98
+ args;
96
99
  lib;
97
100
  serverId = 0;
98
101
  connectionId = 0;
@@ -109,21 +112,21 @@ class FfiRuntimeHost {
109
112
  * the entrypoint directory (the napi-rs `<node-platform>-<arch>` layout, e.g.
110
113
  * `linux-x64`). Throws if it cannot be found.
111
114
  */
112
- static create(cliEntrypoint, prebuildsFolder, environment) {
115
+ static create(cliEntrypoint, prebuildsFolder, environment, args) {
113
116
  const fullEntrypoint = resolve(cliEntrypoint);
114
117
  const distDir = dirname(fullEntrypoint);
115
118
  const libraryPath = join(distDir, "prebuilds", prebuildsFolder, "runtime.node");
116
119
  if (!existsSync(libraryPath)) {
117
120
  throw new Error(`FFI runtime library not found. Looked for '${libraryPath}'.`);
118
121
  }
119
- return new FfiRuntimeHost(libraryPath, fullEntrypoint, environment);
122
+ return new FfiRuntimeHost(libraryPath, fullEntrypoint, environment, args);
120
123
  }
121
124
  /**
122
125
  * Starts the in-process runtime: spawns the CLI worker via the native host,
123
126
  * waits for readiness, and opens the FFI JSON-RPC connection.
124
127
  */
125
128
  async start() {
126
- const argvJson = buildArgvJson(this.cliEntrypoint);
129
+ const argvJson = buildArgvJson(this.cliEntrypoint, this.args);
127
130
  const envJson = buildEnvJson(this.environment);
128
131
  this.serverId = await new Promise((resolvePromise, rejectPromise) => {
129
132
  this.lib.hostStart.async(