suspend() {
+ return caller.invoke("session.suspend", java.util.Map.of("sessionId", this.sessionId), Void.class);
+ }
+
/**
* Invokes {@code session.log}.
*
diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSuspendParams.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSuspendParams.java
new file mode 100644
index 000000000..300b1e4cb
--- /dev/null
+++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionSuspendParams.java
@@ -0,0 +1,27 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+// AUTO-GENERATED FILE - DO NOT EDIT
+// Generated from: api.schema.json
+
+package com.github.copilot.sdk.generated.rpc;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import javax.annotation.processing.Generated;
+
+/**
+ * Request parameters for the {@code session.suspend} RPC method.
+ *
+ * @since 1.0.0
+ */
+@javax.annotation.processing.Generated("copilot-sdk-codegen")
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public record SessionSuspendParams(
+ /** Target session identifier */
+ @JsonProperty("sessionId") String sessionId
+) {
+}
diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUsageGetMetricsResult.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUsageGetMetricsResult.java
index 9cfdbcfc8..cb1897d27 100644
--- a/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUsageGetMetricsResult.java
+++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/SessionUsageGetMetricsResult.java
@@ -26,6 +26,10 @@ public record SessionUsageGetMetricsResult(
@JsonProperty("totalPremiumRequestCost") Double totalPremiumRequestCost,
/** Raw count of user-initiated API requests */
@JsonProperty("totalUserRequests") Long totalUserRequests,
+ /** Session-wide accumulated nano-AI units cost */
+ @JsonProperty("totalNanoAiu") Long totalNanoAiu,
+ /** Session-wide per-token-type accumulated token counts */
+ @JsonProperty("tokenDetails") Map tokenDetails,
/** Total time spent in model API calls (milliseconds) */
@JsonProperty("totalApiDurationMs") Double totalApiDurationMs,
/** Session start timestamp (epoch milliseconds) */
diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsModelMetric.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsModelMetric.java
index 8334872cb..844d4a1bf 100644
--- a/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsModelMetric.java
+++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsModelMetric.java
@@ -10,6 +10,7 @@
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonProperty;
+import java.util.Map;
import javax.annotation.processing.Generated;
@javax.annotation.processing.Generated("copilot-sdk-codegen")
@@ -19,6 +20,10 @@ public record UsageMetricsModelMetric(
/** Request count and cost metrics for this model */
@JsonProperty("requests") UsageMetricsModelMetricRequests requests,
/** Token usage metrics for this model */
- @JsonProperty("usage") UsageMetricsModelMetricUsage usage
+ @JsonProperty("usage") UsageMetricsModelMetricUsage usage,
+ /** Accumulated nano-AI units cost for this model */
+ @JsonProperty("totalNanoAiu") Long totalNanoAiu,
+ /** Token count details per type */
+ @JsonProperty("tokenDetails") Map tokenDetails
) {
}
diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsModelMetricTokenDetail.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsModelMetricTokenDetail.java
new file mode 100644
index 000000000..51ef4f3ee
--- /dev/null
+++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsModelMetricTokenDetail.java
@@ -0,0 +1,22 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+// AUTO-GENERATED FILE - DO NOT EDIT
+// Generated from: api.schema.json
+
+package com.github.copilot.sdk.generated.rpc;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import javax.annotation.processing.Generated;
+
+@javax.annotation.processing.Generated("copilot-sdk-codegen")
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public record UsageMetricsModelMetricTokenDetail(
+ /** Accumulated token count for this token type */
+ @JsonProperty("tokenCount") Long tokenCount
+) {
+}
diff --git a/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsTokenDetail.java b/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsTokenDetail.java
new file mode 100644
index 000000000..1f763f028
--- /dev/null
+++ b/src/generated/java/com/github/copilot/sdk/generated/rpc/UsageMetricsTokenDetail.java
@@ -0,0 +1,22 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+// AUTO-GENERATED FILE - DO NOT EDIT
+// Generated from: api.schema.json
+
+package com.github.copilot.sdk.generated.rpc;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.annotation.JsonInclude;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import javax.annotation.processing.Generated;
+
+@javax.annotation.processing.Generated("copilot-sdk-codegen")
+@JsonInclude(JsonInclude.Include.NON_NULL)
+@JsonIgnoreProperties(ignoreUnknown = true)
+public record UsageMetricsTokenDetail(
+ /** Accumulated token count for this token type */
+ @JsonProperty("tokenCount") Long tokenCount
+) {
+}
diff --git a/src/main/java/com/github/copilot/sdk/CliServerManager.java b/src/main/java/com/github/copilot/sdk/CliServerManager.java
index 07b5eefec..b6009e839 100644
--- a/src/main/java/com/github/copilot/sdk/CliServerManager.java
+++ b/src/main/java/com/github/copilot/sdk/CliServerManager.java
@@ -33,11 +33,23 @@ final class CliServerManager {
private final CopilotClientOptions options;
private final StringBuilder stderrBuffer = new StringBuilder();
+ private String connectionToken;
CliServerManager(CopilotClientOptions options) {
this.options = options;
}
+ /**
+ * Sets the connection token to pass to the CLI process via environment
+ * variable.
+ *
+ * @param connectionToken
+ * the token, or {@code null} if not applicable
+ */
+ void setConnectionToken(String connectionToken) {
+ this.connectionToken = connectionToken;
+ }
+
/**
* Starts the CLI server process.
*
@@ -115,6 +127,16 @@ ProcessInfo startCliServer() throws IOException, InterruptedException {
pb.environment().put("COPILOT_SDK_AUTH_TOKEN", options.getGitHubToken());
}
+ // Set Copilot home directory if configured
+ if (options.getCopilotHome() != null && !options.getCopilotHome().isEmpty()) {
+ pb.environment().put("COPILOT_HOME", options.getCopilotHome());
+ }
+
+ // Set connection token for TCP mode
+ if (connectionToken != null && !connectionToken.isEmpty()) {
+ pb.environment().put("COPILOT_CONNECTION_TOKEN", connectionToken);
+ }
+
// Set telemetry environment variables if configured
if (options.getTelemetry() != null) {
var telemetry = options.getTelemetry();
diff --git a/src/main/java/com/github/copilot/sdk/CopilotClient.java b/src/main/java/com/github/copilot/sdk/CopilotClient.java
index ebe995bd7..555e88861 100644
--- a/src/main/java/com/github/copilot/sdk/CopilotClient.java
+++ b/src/main/java/com/github/copilot/sdk/CopilotClient.java
@@ -21,6 +21,7 @@
import com.github.copilot.sdk.json.CopilotClientOptions;
import com.github.copilot.sdk.json.CreateSessionResponse;
+import com.github.copilot.sdk.generated.rpc.ConnectParams;
import com.github.copilot.sdk.generated.rpc.ServerRpc;
import com.github.copilot.sdk.json.DeleteSessionResponse;
import com.github.copilot.sdk.json.GetAuthStatusResponse;
@@ -83,6 +84,7 @@ public final class CopilotClient implements AutoCloseable {
private volatile boolean disposed = false;
private final String optionsHost;
private final Integer optionsPort;
+ private final String effectiveConnectionToken;
private volatile List modelsCache;
private final Object modelsCacheLock = new Object();
@@ -122,6 +124,24 @@ public CopilotClient(CopilotClientOptions options) {
"GitHubToken and UseLoggedInUser cannot be used with CliUrl (external server manages its own auth)");
}
+ // Validate tcpConnectionToken
+ if (this.options.getTcpConnectionToken() != null) {
+ if (this.options.getTcpConnectionToken().isEmpty()) {
+ throw new IllegalArgumentException("TcpConnectionToken must be a non-empty string");
+ }
+ if (this.options.isUseStdio()) {
+ throw new IllegalArgumentException("TcpConnectionToken cannot be used with UseStdio = true");
+ }
+ }
+
+ // Compute effective connection token: use provided, or auto-generate for
+ // SDK-spawned TCP mode, or null for stdio/external server
+ boolean sdkSpawnsCli = !this.options.isUseStdio()
+ && (this.options.getCliUrl() == null || this.options.getCliUrl().isEmpty());
+ this.effectiveConnectionToken = this.options.getTcpConnectionToken() != null
+ ? this.options.getTcpConnectionToken()
+ : (sdkSpawnsCli ? java.util.UUID.randomUUID().toString() : null);
+
// Parse CliUrl if provided
if (this.options.getCliUrl() != null && !this.options.getCliUrl().isEmpty()) {
URI uri = CliServerManager.parseCliUrl(this.options.getCliUrl());
@@ -133,6 +153,7 @@ public CopilotClient(CopilotClientOptions options) {
}
this.serverManager = new CliServerManager(this.options);
+ this.serverManager.setConnectionToken(this.effectiveConnectionToken);
}
/**
@@ -202,23 +223,49 @@ private Connection startCoreBody() {
}
private static final int MIN_PROTOCOL_VERSION = 2;
+ private static final int METHOD_NOT_FOUND_ERROR_CODE = -32601;
private void verifyProtocolVersion(Connection connection) throws Exception {
int expectedVersion = SdkProtocolVersion.get();
- var params = new HashMap();
- params.put("message", null);
- PingResponse pingResponse = connection.rpc.invoke("ping", params, PingResponse.class).get(30, TimeUnit.SECONDS);
+ Integer serverVersion;
+
+ try {
+ // Try the new 'connect' RPC which supports connection tokens
+ var connectParams = new ConnectParams(effectiveConnectionToken);
+ var connectResponse = connection.rpc
+ .invoke("connect", connectParams, com.github.copilot.sdk.generated.rpc.ConnectResult.class)
+ .get(30, TimeUnit.SECONDS);
+ serverVersion = connectResponse.protocolVersion() != null
+ ? connectResponse.protocolVersion().intValue()
+ : null;
+ } catch (Exception e) {
+ // Unwrap CompletionException/ExecutionException to check inner cause
+ Throwable cause = e;
+ while (cause instanceof java.util.concurrent.ExecutionException || cause instanceof CompletionException) {
+ cause = cause.getCause();
+ }
+ if (cause instanceof JsonRpcException rpcEx && rpcEx.getCode() == METHOD_NOT_FOUND_ERROR_CODE) {
+ // Legacy server without 'connect'; fall back to 'ping'.
+ // A token, if any, is silently dropped — the legacy server can't enforce one.
+ var params = new HashMap();
+ params.put("message", null);
+ PingResponse pingResponse = connection.rpc.invoke("ping", params, PingResponse.class).get(30,
+ TimeUnit.SECONDS);
+ serverVersion = pingResponse.protocolVersion();
+ } else {
+ throw e;
+ }
+ }
- if (pingResponse.protocolVersion() == null) {
- throw new RuntimeException("SDK protocol version mismatch: SDK expects version " + expectedVersion
- + ", but server does not report a protocol version. "
+ if (serverVersion == null) {
+ throw new RuntimeException("SDK protocol version mismatch: SDK supports versions " + MIN_PROTOCOL_VERSION
+ + "-" + expectedVersion + ", but server does not report a protocol version. "
+ "Please update your server to ensure compatibility.");
}
- int serverVersion = pingResponse.protocolVersion();
if (serverVersion < MIN_PROTOCOL_VERSION || serverVersion > expectedVersion) {
- throw new RuntimeException("SDK protocol version mismatch: SDK expects version " + expectedVersion
- + " (minimum " + MIN_PROTOCOL_VERSION + "), but server reports version " + serverVersion + ". "
+ throw new RuntimeException("SDK protocol version mismatch: SDK supports versions " + MIN_PROTOCOL_VERSION
+ + "-" + expectedVersion + ", but server reports version " + serverVersion + ". "
+ "Please update your SDK or server to ensure compatibility.");
}
}
diff --git a/src/main/java/com/github/copilot/sdk/SessionRequestBuilder.java b/src/main/java/com/github/copilot/sdk/SessionRequestBuilder.java
index de6977ea8..91dbd4642 100644
--- a/src/main/java/com/github/copilot/sdk/SessionRequestBuilder.java
+++ b/src/main/java/com/github/copilot/sdk/SessionRequestBuilder.java
@@ -122,6 +122,7 @@ static CreateSessionRequest buildCreateRequest(SessionConfig config, String sess
request.setAgent(config.getAgent());
request.setInfiniteSessions(config.getInfiniteSessions());
request.setSkillDirectories(config.getSkillDirectories());
+ request.setInstructionDirectories(config.getInstructionDirectories());
request.setDisabledSkills(config.getDisabledSkills());
request.setConfigDir(config.getConfigDir());
request.setEnableConfigDiscovery(config.getEnableConfigDiscovery());
@@ -199,6 +200,7 @@ static ResumeSessionRequest buildResumeRequest(String sessionId, ResumeSessionCo
request.setDefaultAgent(config.getDefaultAgent());
request.setAgent(config.getAgent());
request.setSkillDirectories(config.getSkillDirectories());
+ request.setInstructionDirectories(config.getInstructionDirectories());
request.setDisabledSkills(config.getDisabledSkills());
request.setInfiniteSessions(config.getInfiniteSessions());
request.setModelCapabilities(config.getModelCapabilities());
diff --git a/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java b/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java
index 3a14d1733..dff72581a 100644
--- a/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java
+++ b/src/main/java/com/github/copilot/sdk/json/CopilotClientOptions.java
@@ -44,6 +44,7 @@ public class CopilotClientOptions {
private String[] cliArgs;
private String cliPath;
private String cliUrl;
+ private String copilotHome;
private String cwd;
private Map environment;
private Executor executor;
@@ -53,6 +54,7 @@ public class CopilotClientOptions {
private int port;
private TelemetryConfig telemetry;
private Integer sessionIdleTimeoutSeconds;
+ private String tcpConnectionToken;
private Boolean useLoggedInUser;
private boolean useStdio = true;
@@ -191,6 +193,37 @@ public CopilotClientOptions setCliUrl(String cliUrl) {
return this;
}
+ /**
+ * Gets the base directory for Copilot data (session state, config, etc.).
+ *
+ * @return the Copilot home directory path, or {@code null} to use the CLI
+ * default ({@code ~/.copilot})
+ */
+ public String getCopilotHome() {
+ return copilotHome;
+ }
+
+ /**
+ * Sets the base directory for Copilot data (session state, config, etc.).
+ *
+ * Sets the {@code COPILOT_HOME} environment variable on the spawned CLI
+ * process. When {@code null}, the {@code COPILOT_HOME} env var is not set on
+ * the spawned process, so the CLI falls back to its default
+ * ({@code ~/.copilot}).
+ *
+ * This option is only used when the SDK spawns the CLI process; it is ignored
+ * when connecting to an external server via {@link #setCliUrl(String)}.
+ *
+ * @param copilotHome
+ * the Copilot home directory path, or {@code null} to use the CLI
+ * default
+ * @return this options instance for method chaining
+ */
+ public CopilotClientOptions setCopilotHome(String copilotHome) {
+ this.copilotHome = copilotHome;
+ return this;
+ }
+
/**
* Gets the working directory for the CLI process.
*
@@ -462,6 +495,31 @@ public CopilotClientOptions setSessionIdleTimeoutSeconds(Integer sessionIdleTime
return this;
}
+ /**
+ * Gets the connection token for the headless CLI server (TCP only).
+ *
+ * @return the connection token, or {@code null} if not set
+ */
+ public String getTcpConnectionToken() {
+ return tcpConnectionToken;
+ }
+
+ /**
+ * Sets the connection token for the headless CLI server (TCP only).
+ *
+ * When the SDK spawns its own CLI in TCP mode and this is omitted, a UUID is
+ * generated automatically so the loopback listener is safe by default. Cannot
+ * be combined with {@link #setUseStdio(boolean)} = {@code true}.
+ *
+ * @param tcpConnectionToken
+ * the connection token (must not be {@code null} or empty)
+ * @return this options instance for method chaining
+ */
+ public CopilotClientOptions setTcpConnectionToken(String tcpConnectionToken) {
+ this.tcpConnectionToken = Objects.requireNonNull(tcpConnectionToken, "tcpConnectionToken must not be null");
+ return this;
+ }
+
/**
* Returns whether to use the logged-in user for authentication.
*
@@ -533,6 +591,7 @@ public CopilotClientOptions clone() {
copy.cliArgs = this.cliArgs != null ? this.cliArgs.clone() : null;
copy.cliPath = this.cliPath;
copy.cliUrl = this.cliUrl;
+ copy.copilotHome = this.copilotHome;
copy.cwd = this.cwd;
copy.environment = this.environment != null ? new java.util.HashMap<>(this.environment) : null;
copy.executor = this.executor;
@@ -541,6 +600,7 @@ public CopilotClientOptions clone() {
copy.onListModels = this.onListModels;
copy.port = this.port;
copy.sessionIdleTimeoutSeconds = this.sessionIdleTimeoutSeconds;
+ copy.tcpConnectionToken = this.tcpConnectionToken;
copy.telemetry = this.telemetry;
copy.useLoggedInUser = this.useLoggedInUser;
copy.useStdio = this.useStdio;
diff --git a/src/main/java/com/github/copilot/sdk/json/CreateSessionRequest.java b/src/main/java/com/github/copilot/sdk/json/CreateSessionRequest.java
index ef8d5fda2..5243f99ec 100644
--- a/src/main/java/com/github/copilot/sdk/json/CreateSessionRequest.java
+++ b/src/main/java/com/github/copilot/sdk/json/CreateSessionRequest.java
@@ -91,6 +91,9 @@ public final class CreateSessionRequest {
@JsonProperty("skillDirectories")
private List skillDirectories;
+ @JsonProperty("instructionDirectories")
+ private List instructionDirectories;
+
@JsonProperty("disabledSkills")
private List disabledSkills;
@@ -326,6 +329,18 @@ public void setSkillDirectories(List skillDirectories) {
this.skillDirectories = skillDirectories;
}
+ /** Gets instruction directories. @return the instruction directories */
+ public List getInstructionDirectories() {
+ return instructionDirectories == null ? null : Collections.unmodifiableList(instructionDirectories);
+ }
+
+ /**
+ * Sets instruction directories. @param instructionDirectories the directories
+ */
+ public void setInstructionDirectories(List instructionDirectories) {
+ this.instructionDirectories = instructionDirectories;
+ }
+
/** Gets disabled skills. @return the disabled skill names */
public List getDisabledSkills() {
return disabledSkills == null ? null : Collections.unmodifiableList(disabledSkills);
diff --git a/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java b/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java
index b4dacf370..3f0a0706d 100644
--- a/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java
+++ b/src/main/java/com/github/copilot/sdk/json/ResumeSessionConfig.java
@@ -59,6 +59,7 @@ public class ResumeSessionConfig {
private DefaultAgentConfig defaultAgent;
private String agent;
private List skillDirectories;
+ private List instructionDirectories;
private List disabledSkills;
private InfiniteSessionConfig infiniteSessions;
private Consumer onEvent;
@@ -591,6 +592,27 @@ public ResumeSessionConfig setSkillDirectories(List skillDirectories) {
return this;
}
+ /**
+ * Gets the additional directories to search for custom instruction files.
+ *
+ * @return the list of instruction directory paths
+ */
+ public List getInstructionDirectories() {
+ return instructionDirectories == null ? null : Collections.unmodifiableList(instructionDirectories);
+ }
+
+ /**
+ * Sets additional directories to search for custom instruction files.
+ *
+ * @param instructionDirectories
+ * the list of instruction directory paths
+ * @return this config for method chaining
+ */
+ public ResumeSessionConfig setInstructionDirectories(List instructionDirectories) {
+ this.instructionDirectories = instructionDirectories;
+ return this;
+ }
+
/**
* Gets the disabled skills.
*
@@ -775,6 +797,9 @@ public ResumeSessionConfig clone() {
copy.defaultAgent = this.defaultAgent;
copy.agent = this.agent;
copy.skillDirectories = this.skillDirectories != null ? new ArrayList<>(this.skillDirectories) : null;
+ copy.instructionDirectories = this.instructionDirectories != null
+ ? new ArrayList<>(this.instructionDirectories)
+ : null;
copy.disabledSkills = this.disabledSkills != null ? new ArrayList<>(this.disabledSkills) : null;
copy.infiniteSessions = this.infiniteSessions;
copy.onEvent = this.onEvent;
diff --git a/src/main/java/com/github/copilot/sdk/json/ResumeSessionRequest.java b/src/main/java/com/github/copilot/sdk/json/ResumeSessionRequest.java
index e36e90b67..c5931c275 100644
--- a/src/main/java/com/github/copilot/sdk/json/ResumeSessionRequest.java
+++ b/src/main/java/com/github/copilot/sdk/json/ResumeSessionRequest.java
@@ -98,6 +98,9 @@ public final class ResumeSessionRequest {
@JsonProperty("skillDirectories")
private List skillDirectories;
+ @JsonProperty("instructionDirectories")
+ private List instructionDirectories;
+
@JsonProperty("disabledSkills")
private List disabledSkills;
@@ -366,6 +369,18 @@ public void setSkillDirectories(List skillDirectories) {
this.skillDirectories = skillDirectories;
}
+ /** Gets instruction directories. @return the instruction directories */
+ public List getInstructionDirectories() {
+ return instructionDirectories == null ? null : Collections.unmodifiableList(instructionDirectories);
+ }
+
+ /**
+ * Sets instruction directories. @param instructionDirectories the directories
+ */
+ public void setInstructionDirectories(List instructionDirectories) {
+ this.instructionDirectories = instructionDirectories;
+ }
+
/** Gets disabled skills. @return the disabled skill names */
public List getDisabledSkills() {
return disabledSkills == null ? null : Collections.unmodifiableList(disabledSkills);
diff --git a/src/main/java/com/github/copilot/sdk/json/SessionConfig.java b/src/main/java/com/github/copilot/sdk/json/SessionConfig.java
index 09661346a..4d571990c 100644
--- a/src/main/java/com/github/copilot/sdk/json/SessionConfig.java
+++ b/src/main/java/com/github/copilot/sdk/json/SessionConfig.java
@@ -57,6 +57,7 @@ public class SessionConfig {
private String agent;
private InfiniteSessionConfig infiniteSessions;
private List skillDirectories;
+ private List instructionDirectories;
private List disabledSkills;
private String configDir;
private Boolean enableConfigDiscovery;
@@ -550,6 +551,27 @@ public SessionConfig setSkillDirectories(List skillDirectories) {
return this;
}
+ /**
+ * Gets the additional directories to search for custom instruction files.
+ *
+ * @return the list of instruction directory paths
+ */
+ public List getInstructionDirectories() {
+ return instructionDirectories == null ? null : Collections.unmodifiableList(instructionDirectories);
+ }
+
+ /**
+ * Sets additional directories to search for custom instruction files.
+ *
+ * @param instructionDirectories
+ * the list of instruction directory paths
+ * @return this config instance for method chaining
+ */
+ public SessionConfig setInstructionDirectories(List instructionDirectories) {
+ this.instructionDirectories = instructionDirectories;
+ return this;
+ }
+
/**
* Gets the disabled skill names.
*
@@ -825,6 +847,9 @@ public SessionConfig clone() {
copy.agent = this.agent;
copy.infiniteSessions = this.infiniteSessions;
copy.skillDirectories = this.skillDirectories != null ? new ArrayList<>(this.skillDirectories) : null;
+ copy.instructionDirectories = this.instructionDirectories != null
+ ? new ArrayList<>(this.instructionDirectories)
+ : null;
copy.disabledSkills = this.disabledSkills != null ? new ArrayList<>(this.disabledSkills) : null;
copy.configDir = this.configDir;
copy.enableConfigDiscovery = this.enableConfigDiscovery;
diff --git a/src/site/markdown/advanced.md b/src/site/markdown/advanced.md
index a4c4d830a..e1e08a275 100644
--- a/src/site/markdown/advanced.md
+++ b/src/site/markdown/advanced.md
@@ -626,6 +626,31 @@ var session = client.createSession(
---
+## Instruction Directories
+
+Provide additional directories containing custom instruction files. These instructions are automatically included in the system message for all conversations in the session.
+
+```java
+var session = client.createSession(
+ new SessionConfig().setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
+ .setInstructionDirectories(List.of("/path/to/instructions"))
+).get();
+```
+
+Instruction files are discovered from `.github/instructions/` subdirectories within each specified path and should use the `.instructions.md` extension.
+
+This is also supported on session resume:
+
+```java
+var session = client.resumeSession(sessionId,
+ new ResumeSessionConfig()
+ .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)
+ .setInstructionDirectories(List.of("/path/to/instructions"))
+);
+```
+
+---
+
## Custom Configuration Directory
Use a custom configuration directory for session settings:
diff --git a/src/site/markdown/setup.md b/src/site/markdown/setup.md
index 10c1cb3d8..224fc1948 100644
--- a/src/site/markdown/setup.md
+++ b/src/site/markdown/setup.md
@@ -157,12 +157,28 @@ try (var client = new CopilotClient(options)) {
Multiple application instances can share a single CLI server:
```java
-// In different parts of your application or different containers
-var client1 = new CopilotClient(new CopilotClientOptions().setCliUrl("cli-server:4321"));
-var client2 = new CopilotClient(new CopilotClientOptions().setCliUrl("cli-server:4321"));
+// Use an explicit connection token so all clients can authenticate
+var token = "my-shared-secret";
+var client1 = new CopilotClient(new CopilotClientOptions()
+ .setCliUrl("cli-server:4321").setTcpConnectionToken(token));
+var client2 = new CopilotClient(new CopilotClientOptions()
+ .setCliUrl("cli-server:4321").setTcpConnectionToken(token));
// Both connect to the same CLI server
```
+### Connection Token (TCP Security)
+
+When the SDK spawns the CLI in TCP mode, a random connection token is generated automatically
+to protect the loopback listener. You can also provide an explicit token:
+
+```java
+var options = new CopilotClientOptions()
+ .setUseStdio(false)
+ .setTcpConnectionToken("my-secret-token");
+```
+
+> **Note:** `tcpConnectionToken` cannot be used with `useStdio = true`.
+
### Deployment Patterns
**Container deployment:**
@@ -340,10 +356,12 @@ Complete list of `CopilotClientOptions` settings:
| `cliPath` | String | Path to CLI executable | `"copilot"` from PATH |
| `cliUrl` | String | External CLI server URL | `null` (spawn process) |
| `cliArgs` | String[] | Extra CLI arguments | `null` |
+| `copilotHome` | String | Base directory for Copilot data | `null` (~/.copilot) |
| `gitHubToken` | String | GitHub OAuth token | `null` |
| `useLoggedInUser` | Boolean | Use system credentials | `true` |
| `useStdio` | boolean | Use stdio transport | `true` |
| `port` | int | TCP port for CLI | `0` (random) |
+| `tcpConnectionToken` | String | Connection token for TCP mode | `null` (auto-generated) |
| `autoStart` | boolean | Auto-start server | `true` |
| `autoRestart` | boolean | Auto-restart on crash | `true` |
| `logLevel` | String | CLI log level | `"info"` |
diff --git a/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java b/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java
index f4f702f9e..74bee7b50 100644
--- a/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java
+++ b/src/test/java/com/github/copilot/sdk/ConfigCloneTest.java
@@ -36,6 +36,9 @@ void copilotClientOptionsCloneBasic() {
original.setPort(9000);
original.setGitHubToken("ghp_test");
original.setUseLoggedInUser(false);
+ original.setCopilotHome("/custom/copilot/home");
+ original.setUseStdio(false);
+ original.setTcpConnectionToken("my-token-123");
CopilotClientOptions cloned = original.clone();
@@ -44,6 +47,8 @@ void copilotClientOptionsCloneBasic() {
assertEquals(original.getPort(), cloned.getPort());
assertEquals(original.getGitHubToken(), cloned.getGitHubToken());
assertEquals(original.getUseLoggedInUser(), cloned.getUseLoggedInUser());
+ assertEquals(original.getCopilotHome(), cloned.getCopilotHome());
+ assertEquals(original.getTcpConnectionToken(), cloned.getTcpConnectionToken());
}
@Test
@@ -120,6 +125,7 @@ void sessionConfigListIndependence() {
toolList.add("grep");
toolList.add("bash");
original.setAvailableTools(toolList);
+ original.setInstructionDirectories(new ArrayList<>(List.of("/path/a", "/path/b")));
SessionConfig cloned = original.clone();
@@ -129,6 +135,7 @@ void sessionConfigListIndependence() {
// The cloned config should be unaffected by mutations to the original list
assertEquals(2, cloned.getAvailableTools().size());
assertEquals(3, original.getAvailableTools().size());
+ assertEquals(List.of("/path/a", "/path/b"), cloned.getInstructionDirectories());
}
@Test
diff --git a/src/test/java/com/github/copilot/sdk/CopilotClientTest.java b/src/test/java/com/github/copilot/sdk/CopilotClientTest.java
index 1cc067587..b0430959b 100644
--- a/src/test/java/com/github/copilot/sdk/CopilotClientTest.java
+++ b/src/test/java/com/github/copilot/sdk/CopilotClientTest.java
@@ -199,6 +199,30 @@ void testSessionIdleTimeoutSecondsOptionAccepted() {
assertEquals(600, options.getSessionIdleTimeoutSeconds());
}
+ @Test
+ void testTcpConnectionTokenWithUseStdioThrows() {
+ var options = new CopilotClientOptions().setUseStdio(true).setTcpConnectionToken("my-token");
+
+ assertThrows(IllegalArgumentException.class, () -> new CopilotClient(options));
+ }
+
+ @Test
+ void testTcpConnectionTokenAcceptedInTcpMode() {
+ var options = new CopilotClientOptions().setUseStdio(false).setTcpConnectionToken("my-token");
+
+ // Should not throw
+ try (var client = new CopilotClient(options)) {
+ assertNotNull(client);
+ }
+ }
+
+ @Test
+ void testCopilotHomeOptionSetOnOptions() {
+ var options = new CopilotClientOptions().setCopilotHome("/custom/home");
+
+ assertEquals("/custom/home", options.getCopilotHome());
+ }
+
// ===== onLifecycle tests =====
/**
diff --git a/src/test/java/com/github/copilot/sdk/SessionConfigE2ETest.java b/src/test/java/com/github/copilot/sdk/SessionConfigE2ETest.java
new file mode 100644
index 000000000..5dcb36604
--- /dev/null
+++ b/src/test/java/com/github/copilot/sdk/SessionConfigE2ETest.java
@@ -0,0 +1,130 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ *--------------------------------------------------------------------------------------------*/
+
+package com.github.copilot.sdk;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import org.junit.jupiter.api.AfterAll;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import com.github.copilot.sdk.json.MessageOptions;
+import com.github.copilot.sdk.json.PermissionHandler;
+import com.github.copilot.sdk.json.ResumeSessionConfig;
+import com.github.copilot.sdk.json.SessionConfig;
+
+/**
+ * E2E tests for session configuration features.
+ */
+public class SessionConfigE2ETest {
+
+ private static E2ETestContext ctx;
+
+ @BeforeAll
+ static void setup() throws Exception {
+ ctx = E2ETestContext.create();
+ }
+
+ @AfterAll
+ static void teardown() throws Exception {
+ if (ctx != null) {
+ ctx.close();
+ }
+ }
+
+ @Test
+ void testShouldApplyInstructionDirectoriesOnCreate() throws Exception {
+ ctx.configureForTest("session_config", "should_apply_instructiondirectories_on_create");
+
+ // Set up instruction directory with a custom instruction file
+ Path projectDir = ctx.getWorkDir().resolve("instruction-create-project");
+ Path instructionDir = ctx.getWorkDir().resolve("extra-create-instructions");
+ Path instructionFilesDir = instructionDir.resolve(".github").resolve("instructions");
+ String sentinel = "JAVA_CREATE_INSTRUCTION_DIRECTORIES_SENTINEL";
+ Files.createDirectories(projectDir);
+ Files.createDirectories(instructionFilesDir);
+ Files.writeString(instructionFilesDir.resolve("extra.instructions.md"), "Always include " + sentinel + ".");
+
+ try (CopilotClient client = ctx.createClient()) {
+ CopilotSession session = client.createSession(new SessionConfig().setWorkingDirectory(projectDir.toString())
+ .setInstructionDirectories(List.of(instructionDir.toString()))
+ .setOnPermissionRequest(PermissionHandler.APPROVE_ALL)).get();
+
+ session.sendAndWait(new MessageOptions().setPrompt("What is 1+1?")).get(60, TimeUnit.SECONDS);
+
+ List