@github/copilot-sdk 1.0.0-beta.8 → 1.0.0-beta.9

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/dist/client.js CHANGED
@@ -21,6 +21,7 @@ import { getSdkProtocolVersion } from "./sdkProtocolVersion.js";
21
21
  import { CopilotSession } from "./session.js";
22
22
  import { createSessionFsAdapter } from "./sessionFsProvider.js";
23
23
  import { getTraceContext } from "./telemetry.js";
24
+ import { ToolSet } from "./toolSet.js";
24
25
  import { defaultJoinSessionPermissionHandler } from "./types.js";
25
26
  const MIN_PROTOCOL_VERSION = 3;
26
27
  function isZodSchema(value) {
@@ -53,6 +54,24 @@ function toWireCustomAgents(agents) {
53
54
  return { ...rest, mcpServers: toWireMcpServers(mcpServers) };
54
55
  });
55
56
  }
57
+ function toolFilterListToArray(value) {
58
+ if (value === void 0) {
59
+ return void 0;
60
+ }
61
+ return value instanceof ToolSet ? value.toArray() : value;
62
+ }
63
+ function validateToolFilterList(field, list) {
64
+ if (!list) {
65
+ return;
66
+ }
67
+ for (const entry of list) {
68
+ if (entry === "*") {
69
+ throw new Error(
70
+ `Invalid ${field} entry '*': there is no bare wildcard. Use one or more of \`new ToolSet().addBuiltIn('*')\`, \`.addMcp('*')\`, or \`.addCustom('*')\` to target a specific source.`
71
+ );
72
+ }
73
+ }
74
+ }
56
75
  function extractTransformCallbacks(systemMessage) {
57
76
  if (!systemMessage || systemMessage.mode !== "customize" || !systemMessage.sections) {
58
77
  return { wirePayload: systemMessage, transformCallbacks: void 0 };
@@ -235,8 +254,19 @@ class CopilotClient {
235
254
  telemetry: options.telemetry,
236
255
  baseDirectory: options.baseDirectory,
237
256
  sessionIdleTimeoutSeconds: options.sessionIdleTimeoutSeconds ?? 0,
238
- enableRemoteSessions: options.enableRemoteSessions ?? false
257
+ enableRemoteSessions: options.enableRemoteSessions ?? false,
258
+ mode: options.mode ?? "copilot-cli"
239
259
  };
260
+ if (this.options.mode === "empty") {
261
+ const hasPersistence = this.options.baseDirectory !== void 0 || this.sessionFsConfig !== null || // External runtimes manage their own persistence layer; the SDK
262
+ // can't enforce it from here.
263
+ conn.kind === "uri" || conn.kind === "parent-process";
264
+ if (!hasPersistence) {
265
+ throw new Error(
266
+ "CopilotClient was created with mode: 'empty' but neither 'baseDirectory' nor 'sessionFs' was set. Empty mode requires an explicit per-session persistence location; pick one."
267
+ );
268
+ }
269
+ }
240
270
  }
241
271
  connectionExtraArgs = [];
242
272
  /**
@@ -546,10 +576,122 @@ class CopilotClient {
546
576
  * });
547
577
  * ```
548
578
  */
579
+ /**
580
+ * Normalizes session-level tool filter options. Converts {@link ToolSet}
581
+ * instances to plain string arrays, rejects misuse (bare `"*"`) and the
582
+ * missing-availableTools case in `mode = "empty"`.
583
+ *
584
+ * The SDK always sends `toolFilterPrecedence: "excluded"` so callers can
585
+ * compose include + exclude lists naturally (e.g. "everything matching X
586
+ * except Y") regardless of mode. Allowlist-precedence is intentionally not
587
+ * exposed — it's available on the runtime side as a CLI-only concession to
588
+ * legacy behavior, but SDK consumers always get the composable semantics.
589
+ *
590
+ * @internal
591
+ */
592
+ resolveToolFilterOptions(config) {
593
+ const availableTools = toolFilterListToArray(config.availableTools);
594
+ const excludedTools = toolFilterListToArray(config.excludedTools);
595
+ validateToolFilterList("availableTools", availableTools);
596
+ validateToolFilterList("excludedTools", excludedTools);
597
+ if (this.options.mode === "empty") {
598
+ if (availableTools === void 0) {
599
+ throw new Error(
600
+ "CopilotClient is in mode: 'empty' but the session config did not specify 'availableTools'. Empty mode requires every session to explicitly opt into the tools it wants \u2014 e.g. `new ToolSet().addBuiltIn(BuiltInTools.Isolated)`."
601
+ );
602
+ }
603
+ }
604
+ return { availableTools, excludedTools, toolFilterPrecedence: "excluded" };
605
+ }
606
+ /** Mode-specific defaults spread under the caller's config (app values win). */
607
+ configDefaultsForMode() {
608
+ if (this.options.mode === "empty") {
609
+ return { enableSessionTelemetry: false };
610
+ }
611
+ return {};
612
+ }
613
+ /**
614
+ * Returns the systemMessage config to use, adjusted for the current mode.
615
+ * In empty mode we ensure the environment_context section is removed
616
+ * unless the app has already taken control of it. `append` (and
617
+ * unspecified) mode is promoted to `customize` so we can also strip
618
+ * environment_context; the caller's `content` is preserved verbatim
619
+ * because the runtime appends it as additional instructions in both
620
+ * customize and append modes.
621
+ */
622
+ getSystemMessageConfigForMode(supplied) {
623
+ if (this.options.mode !== "empty") return supplied;
624
+ if (!supplied) {
625
+ return {
626
+ mode: "customize",
627
+ sections: { environment_context: { action: "remove" } }
628
+ };
629
+ }
630
+ switch (supplied.mode) {
631
+ case "replace":
632
+ return supplied;
633
+ case "customize":
634
+ if (supplied.sections?.environment_context) return supplied;
635
+ return {
636
+ ...supplied,
637
+ sections: {
638
+ ...supplied.sections,
639
+ environment_context: { action: "remove" }
640
+ }
641
+ };
642
+ case "append":
643
+ case void 0:
644
+ return {
645
+ mode: "customize",
646
+ content: supplied.content,
647
+ sections: { environment_context: { action: "remove" } }
648
+ };
649
+ }
650
+ }
651
+ /**
652
+ * Mode-specific options applied via session.options.update after create/resume.
653
+ *
654
+ * In empty mode, defaults the four overridable feature flags to safe values
655
+ * (caller values from `config` win). `installedPlugins=[]` is unconditional
656
+ * in empty mode — apps that need custom plugins should switch modes.
657
+ */
658
+ async updateSessionOptionsForMode(session, config) {
659
+ const patch = {};
660
+ if (this.options.mode === "empty") {
661
+ patch.skipCustomInstructions = config.skipCustomInstructions ?? true;
662
+ patch.customAgentsLocalOnly = config.customAgentsLocalOnly ?? true;
663
+ patch.coauthorEnabled = config.coauthorEnabled ?? false;
664
+ patch.manageScheduleEnabled = config.manageScheduleEnabled ?? false;
665
+ patch.installedPlugins = [];
666
+ } else {
667
+ if (config.skipCustomInstructions !== void 0)
668
+ patch.skipCustomInstructions = config.skipCustomInstructions;
669
+ if (config.customAgentsLocalOnly !== void 0)
670
+ patch.customAgentsLocalOnly = config.customAgentsLocalOnly;
671
+ if (config.coauthorEnabled !== void 0)
672
+ patch.coauthorEnabled = config.coauthorEnabled;
673
+ if (config.manageScheduleEnabled !== void 0)
674
+ patch.manageScheduleEnabled = config.manageScheduleEnabled;
675
+ }
676
+ if (Object.keys(patch).length === 0) {
677
+ return;
678
+ }
679
+ try {
680
+ await session.rpc.options.update(patch);
681
+ } catch (e) {
682
+ try {
683
+ await session.disconnect();
684
+ } catch {
685
+ }
686
+ throw e;
687
+ }
688
+ }
549
689
  async createSession(config) {
550
690
  if (!this.connection) {
551
691
  await this.start();
552
692
  }
693
+ config = { ...this.configDefaultsForMode(), ...config };
694
+ config.systemMessage = this.getSystemMessageConfigForMode(config.systemMessage);
553
695
  const sessionId = config.sessionId ?? randomUUID();
554
696
  const session = new CopilotSession(
555
697
  sessionId,
@@ -587,6 +729,7 @@ class CopilotClient {
587
729
  }
588
730
  this.sessions.set(sessionId, session);
589
731
  this.setupSessionFs(session, config);
732
+ const toolFilterOptions = this.resolveToolFilterOptions(config);
590
733
  try {
591
734
  const response = await this.connection.sendRequest("session.create", {
592
735
  ...await getTraceContext(this.onGetTraceContext),
@@ -610,8 +753,9 @@ class CopilotClient {
610
753
  description: cmd.description
611
754
  })),
612
755
  systemMessage: wireSystemMessage,
613
- availableTools: config.availableTools,
614
- excludedTools: config.excludedTools,
756
+ availableTools: toolFilterOptions.availableTools,
757
+ excludedTools: toolFilterOptions.excludedTools,
758
+ toolFilterPrecedence: toolFilterOptions.toolFilterPrecedence,
615
759
  provider: config.provider,
616
760
  enableSessionTelemetry: config.enableSessionTelemetry,
617
761
  modelCapabilities: config.modelCapabilities,
@@ -642,6 +786,7 @@ class CopilotClient {
642
786
  const { workspacePath, capabilities } = response;
643
787
  session["_workspacePath"] = workspacePath;
644
788
  session.setCapabilities(capabilities);
789
+ await this.updateSessionOptionsForMode(session, config);
645
790
  } catch (e) {
646
791
  this.sessions.delete(sessionId);
647
792
  throw e;
@@ -701,6 +846,8 @@ class CopilotClient {
701
846
  if (config.hooks) {
702
847
  session.registerHooks(config.hooks);
703
848
  }
849
+ config = { ...this.configDefaultsForMode(), ...config };
850
+ config.systemMessage = this.getSystemMessageConfigForMode(config.systemMessage);
704
851
  const { wirePayload: wireSystemMessage, transformCallbacks } = extractTransformCallbacks(
705
852
  config.systemMessage
706
853
  );
@@ -712,6 +859,7 @@ class CopilotClient {
712
859
  }
713
860
  this.sessions.set(sessionId, session);
714
861
  this.setupSessionFs(session, config);
862
+ const toolFilterOptions = this.resolveToolFilterOptions(config);
715
863
  try {
716
864
  const response = await this.connection.sendRequest("session.resume", {
717
865
  ...await getTraceContext(this.onGetTraceContext),
@@ -720,8 +868,9 @@ class CopilotClient {
720
868
  model: config.model,
721
869
  reasoningEffort: config.reasoningEffort,
722
870
  systemMessage: wireSystemMessage,
723
- availableTools: config.availableTools,
724
- excludedTools: config.excludedTools,
871
+ availableTools: toolFilterOptions.availableTools,
872
+ excludedTools: toolFilterOptions.excludedTools,
873
+ toolFilterPrecedence: toolFilterOptions.toolFilterPrecedence,
725
874
  enableSessionTelemetry: config.enableSessionTelemetry,
726
875
  tools: config.tools?.map((tool) => ({
727
876
  name: tool.name,
@@ -770,6 +919,7 @@ class CopilotClient {
770
919
  session["_workspacePath"] = workspacePath;
771
920
  session.setCapabilities(capabilities);
772
921
  session.setOpenCanvases(openCanvases ?? []);
922
+ await this.updateSessionOptionsForMode(session, config);
773
923
  } catch (e) {
774
924
  this.sessions.delete(sessionId);
775
925
  throw e;
@@ -1148,6 +1298,9 @@ class CopilotClient {
1148
1298
  if (this.options.baseDirectory) {
1149
1299
  envWithoutNodeDebug.COPILOT_HOME = this.options.baseDirectory;
1150
1300
  }
1301
+ if (this.options.mode === "empty") {
1302
+ envWithoutNodeDebug.COPILOT_DISABLE_KEYTAR = "1";
1303
+ }
1151
1304
  if (!this.resolvedCliPath) {
1152
1305
  throw new Error(
1153
1306
  "Path to Copilot CLI is required. Please supply it via `RuntimeConnection.forStdio({ path })` or `RuntimeConnection.forTcp({ path })`, set the COPILOT_CLI_PATH environment variable, or use `RuntimeConnection.forUri(...)` to connect to an already-running runtime."