@gitterm/sdk 0.2.1 → 0.3.0

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
@@ -23,10 +23,12 @@ Create a workspace, run a prompt, and get the final result:
23
23
  import { createGittermClient } from "@gitterm/sdk";
24
24
 
25
25
  const client = createGittermClient({ token: process.env.GITTERM_API_TOKEN });
26
+ const [github] = await client.integrations.github.list();
27
+ if (!github) throw new Error("Connect GitHub in the Gitterm dashboard first");
26
28
 
27
29
  const { workspace } = await client.workspaces.create({
28
30
  repo: "https://github.com/acme/product",
29
- repositoryCredentials: { token: process.env.GITHUB_TOKEN! },
31
+ gitIntegrationId: github.id,
30
32
  autoTerminateAfterMs: 2 * 60 * 60 * 1000,
31
33
  });
32
34
 
@@ -43,6 +45,22 @@ try {
43
45
  }
44
46
  ```
45
47
 
48
+ Connected cloud identities are discoverable without returning credentials:
49
+
50
+ ```ts
51
+ const [github] = await client.integrations.github.list();
52
+ const [googleCloud] = await client.integrations.googleCloud.list();
53
+
54
+ await client.workspaces.create({
55
+ repo: "https://github.com/acme/product",
56
+ gitIntegrationId: github.id,
57
+ googleCloudIntegrationId: googleCloud.id,
58
+ });
59
+ ```
60
+
61
+ GitHub tokens and Google workload credentials are issued inside the workspace; the SDK caller does
62
+ not receive or forward them.
63
+
46
64
  `result()` returns only on successful completion. If the agent asks something and no handler is
47
65
  registered, it throws `AgentRunError` with code `INPUT_REQUIRED` and the current `error.run`.
48
66
  Register `onPermission` / `onQuestion` handlers for interactive runs, or use the event relay below.
@@ -194,6 +212,44 @@ as pull and push. Without inline credentials, `gitIntegrationId` continues to us
194
212
  dashboard integration. Omitting `models` likewise continues to
195
213
  use dashboard-managed model credentials.
196
214
 
215
+ #### GitHub CLI authentication
216
+
217
+ For GitHub repositories, GitTerm configures both Git and `gh` from the same workspace
218
+ credentials. Agents can run commands such as `gh pr list` or `gh pr create` without
219
+ running `gh auth login`:
220
+
221
+ - `repositoryCredentials.token` takes precedence over `gitIntegrationId`. The supplied
222
+ token is retained in a permission-restricted file on the workspace machine and is
223
+ not automatically renewed. This also works with the standalone SDK's direct providers.
224
+ - `gitIntegrationId` uses the GitHub App installation token. Git and `gh` share a
225
+ cached token and refresh it through GitTerm before expiry. Concurrent commands share
226
+ the refresh, and a failed refresh stops the command rather than using an expired token.
227
+ - Without repository credentials or an integration, GitTerm leaves CLI authentication
228
+ to the environment or the CLI's existing configuration.
229
+
230
+ GitTerm supplies `GH_TOKEN` only to the invoked CLI process, so there is no stale
231
+ installation token exported into the agent's long-running environment. Explicit
232
+ `GH_TOKEN` or `GITHUB_TOKEN` environment variables override this CLI authentication;
233
+ they do not change the credentials used by Git. Managed credentials are for `github.com`.
234
+
235
+ The generated `AGENTS.md` tells agents that `$HOME/.gitterm/bin/gh` indicates GitTerm
236
+ authentication was provisioned. Agents install the real CLI separately when needed, keep
237
+ the GitTerm launcher first on `PATH`, and use the normal `gh` command. `$GH_TOKEN` is
238
+ intentionally not exported globally, and agents are instructed not to pipe it into
239
+ `gh auth login`.
240
+ CLI installation is left to the image or the agent as needed for the task; there is no SDK
241
+ installation option. The guidance explains checking `gh --version` and installing the CLI
242
+ if needed. The launcher discovers the
243
+ actual binary on each invocation, including installations made after startup.
244
+ It lives in `~/.gitterm/bin`, which GitTerm adds to the agent's PATH and standard shell
245
+ profiles. Install the actual CLI elsewhere and keep `~/.gitterm/bin` first on PATH so it
246
+ does not overwrite or bypass the launcher. Node.js and Git are required by the
247
+ authentication runtime.
248
+
249
+ Available commands depend on the token's repository access and permissions. Installation
250
+ tokens act as the GitHub App bot; user-scoped commands may require user authentication.
251
+ Updating an integration's token does not grant permissions absent from its installation.
252
+
197
253
  GitTerm does not save inline PATs in its application database. Inline PATs must be delivered to the
198
254
  selected compute provider and retained on the workspace machine for runtime Git operations, so
199
255
  provider infrastructure and processes running in that workspace may be able to access them. Prefer
@@ -520,6 +576,10 @@ remain. The exported types are `AgentPermissionRequest`, `AgentQuestionRequest`,
520
576
  `custom` defaults to true, matching OpenCode; false only when the agent sets it.
521
577
  Questions with `options: []` accept free-text answers when `custom` is true.
522
578
 
579
+ Question options are normalised at the runtime boundary: labels and descriptions are trimmed,
580
+ OpenCode's copied "Type your own answer" entry is removed and enables `custom: true`, and
581
+ duplicate and blank labels are dropped. Hosted users need the API redeployed for this to take effect.
582
+
523
583
  The quick start above shows a complete relay. Rejecting a permission or dismissing a question
524
584
  ends the turn: OpenCode records a failed tool call and the run finishes. A run left
525
585
  `awaiting_input` does not keep its workspace awake; if nobody answers before the workspace's
@@ -622,11 +682,10 @@ would otherwise change them for other runs on the same runtime.
622
682
 
623
683
  ## OpenCode API versions
624
684
 
625
- Managed workspaces continue to run `opencode-ai@latest` (`opencodeApi: "v1"`). OpenCode 2
626
- changes the server API; callers testing it must supply their own compatible image and create the
627
- workspace with `opencode: { api: "v2" }`. Runs, questions, permissions, and events behave the
628
- same from the SDK's point of view; the flag only tells GitTerm which protocol to speak to the
629
- workspace. `v2` is experimental until OpenCode 2 ships.
685
+ Managed workspaces run OpenCode V2 from `@opencode/cli@2` and use `opencodeApi: "v2"`. V2 stores
686
+ credentials in SQLite; GitTerm writes a short-lived V1-shaped `auth.json` seed before first startup,
687
+ triggers V2's supported credential import, then deletes the seed. Runs, questions, permissions, and
688
+ events use GitTerm's V2 runtime adapter.
630
689
 
631
690
  ## Errors
632
691
 
package/dist/client.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { AgentType, AgentRun, AgentRunCreateInput, AgentRunListOptions, AgentRunListResult, AgentRunMessage, AgentRunReply, AuthStatus, CloudProvider, RunRef, RunWaitOptions, RunWatchOptions, WaitOptions, Workspace, WorkspaceCreateInput, WorkspaceCreateResult, WorkspaceEnsureRunningResult, WorkspaceListOptions, WorkspaceListResult, WorkspaceRef, WorkspaceRestartResult, WorkspaceRuntimeAccess, WorkspacePauseResult, WorkspaceTerminateResult, WorkspaceCatalog, WorkspaceSetupStatus, ModelCredential, ModelProviderInfo } from "./types.js";
1
+ import type { AgentType, AgentRun, AgentRunCreateInput, AgentRunListOptions, AgentRunListResult, AgentRunMessage, AgentRunReply, AuthStatus, CloudProvider, RunRef, RunWaitOptions, RunWatchOptions, WaitOptions, Workspace, WorkspaceCreateInput, WorkspaceCreateResult, WorkspaceEnsureRunningResult, WorkspaceListOptions, WorkspaceListResult, WorkspaceRef, WorkspaceRestartResult, WorkspaceRuntimeAccess, WorkspacePauseResult, WorkspaceTerminateResult, WorkspaceCatalog, WorkspaceSetupStatus, ModelCredential, ModelProviderInfo, GitHubIntegration, GoogleCloudIntegration } from "./types.js";
2
2
  import type { AgentRunEvent, AgentRunResult, RunResultOptions, ModelInfo, WorkspaceModelAccess } from "./types.js";
3
3
  export type GittermClientOptions = {
4
4
  serverUrl?: string;
@@ -69,6 +69,14 @@ export type GittermClient = {
69
69
  list(): Promise<ModelCredential[]>;
70
70
  listProviders(): Promise<ModelProviderInfo[]>;
71
71
  };
72
+ integrations: {
73
+ github: {
74
+ list(): Promise<GitHubIntegration[]>;
75
+ };
76
+ googleCloud: {
77
+ list(): Promise<GoogleCloudIntegration[]>;
78
+ };
79
+ };
72
80
  models: {
73
81
  list(options?: {
74
82
  provider?: string;
@@ -2,6 +2,36 @@
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { createOpencodeClient as createOpencodeClient2 } from "@opencode-ai/sdk";
4
4
 
5
+ // ../agent-runtime/src/normalize.ts
6
+ var OPENCODE_CUSTOM_OPTION_LABEL = /^type your own answer\W*$/i;
7
+ function normalizeQuestion(question) {
8
+ const options = [];
9
+ const labels = new Set;
10
+ let changed = false;
11
+ let custom = question.custom;
12
+ for (const option of question.options) {
13
+ const label = option.label.trim();
14
+ const description = option.description.trim();
15
+ if (OPENCODE_CUSTOM_OPTION_LABEL.test(label)) {
16
+ custom = true;
17
+ changed = true;
18
+ continue;
19
+ }
20
+ if (!label || labels.has(label)) {
21
+ changed = true;
22
+ continue;
23
+ }
24
+ labels.add(label);
25
+ if (label !== option.label || description !== option.description) {
26
+ changed = true;
27
+ options.push({ ...option, label, description });
28
+ } else {
29
+ options.push(option);
30
+ }
31
+ }
32
+ return changed ? { ...question, options, custom } : question;
33
+ }
34
+
5
35
  // ../agent-runtime/src/opencode.ts
6
36
  import { createOpencodeClient } from "@opencode-ai/sdk";
7
37
 
@@ -426,7 +456,7 @@ function questionRequest(raw) {
426
456
  kind: "question",
427
457
  createdAt: null,
428
458
  toolCallId: asString(tool.callID),
429
- questions: questions.map((question, index) => ({
459
+ questions: questions.map((question, index) => normalizeQuestion({
430
460
  key: `q${index}`,
431
461
  header: asString(question.header) ?? `Question ${index + 1}`,
432
462
  question: asString(question.question) ?? "",
@@ -714,7 +744,7 @@ function formRequest(raw) {
714
744
  toolCallId: asString(tool.id) ?? asString(tool.callID),
715
745
  questions: fields.map((field, index) => {
716
746
  const key = asString(field.key) ?? `q${index}`;
717
- return {
747
+ return normalizeQuestion({
718
748
  key,
719
749
  header: asString(field.title) ?? key,
720
750
  question: asString(field.description) ?? asString(field.title) ?? key,
@@ -729,7 +759,7 @@ function formRequest(raw) {
729
759
  }),
730
760
  multiple: field.type === "multiselect",
731
761
  custom: field.custom !== false
732
- };
762
+ });
733
763
  })
734
764
  };
735
765
  }
@@ -793,6 +823,12 @@ function parseV2Signal(raw) {
793
823
  return null;
794
824
  }
795
825
  }
826
+ // ../agent-runtime/src/opencode-credentials.ts
827
+ var OPENCODE_V2_IMPORT_CREDENTIALS = String.raw`AUTH_FILE="\${OPENCODE_DATA_DIR:-\${XDG_DATA_HOME:-$HOME/.local/share}/opencode}/auth.json"
828
+ if [ -f "$AUTH_FILE" ]; then
829
+ opencode auth list >/dev/null
830
+ rm -f "$AUTH_FILE"
831
+ fi`;
796
832
  // ../agent-runtime/src/derive.ts
797
833
  var MISSING_ASSISTANT_GRACE_MS = 1e4;
798
834
  function deriveRunState(snapshot, context) {
@@ -1243,6 +1279,243 @@ import {
1243
1279
  // src/direct/ascii.ts
1244
1280
  import { BoxApi, Configuration, waitUntilReady } from "@asciidev/box-sdk";
1245
1281
 
1282
+ // ../agent-runtime/src/github-auth.ts
1283
+ var GITHUB_AUTH_PATH = 'export PATH="$HOME/.gitterm/bin:$PATH"';
1284
+ var GITHUB_CLI_INSTRUCTIONS = `GitHub CLI authentication:
1285
+ - The user may have supplied repository credentials or selected a GitHub integration for this workspace. GitTerm authentication is configured only when $HOME/.gitterm/bin/gh exists. Check for that executable, but never read or print files under $HOME/.gitterm/github.
1286
+ - The image may not include the real GitHub CLI. Run gh --version. If the GitTerm launcher says the CLI is missing, install the official gh binary using the image's package manager or in another directory.
1287
+ - Keep $HOME/.gitterm/bin first on PATH after installation. For a downloaded CLI, use: export PATH="$HOME/.gitterm/bin:/path/to/installed/gh/bin:$PATH". Never prepend the real CLI ahead of $HOME/.gitterm/bin and never overwrite $HOME/.gitterm/bin/gh.
1288
+ - With the launcher first, use the normal gh command (for example, gh pr create). The launcher supplies the user-provided token or a fresh integration token only to that operation.
1289
+ - Never run gh auth login, pipe $GH_TOKEN, copy tokens, or use gh auth status for this workspace authentication. $GH_TOKEN is intentionally not exported to the agent shell.
1290
+ - If $HOME/.gitterm/bin/gh is absent, tell the user that this workspace has no GitTerm-provided GitHub credentials. If an authenticated operation is denied, report the denied operation and ask for repository access or the required GitHub App/token permission.`;
1291
+ function githubAuthCommand(command) {
1292
+ return `env PATH="$HOME/.gitterm/bin:$PATH" sh -c '${command.replaceAll("'", `'"'"'`)}'`;
1293
+ }
1294
+ function hasGithubAuth(repo) {
1295
+ return Boolean(repo?.authToken && /^(https:\/\/github\.com\/|git@github\.com:|ssh:\/\/git@github\.com\/)/i.test(repo.url));
1296
+ }
1297
+ function githubAuthProvisioning(repo) {
1298
+ if (!hasGithubAuth(repo)) {
1299
+ return;
1300
+ }
1301
+ const config = {
1302
+ username: repo.authUsername ?? "x-access-token",
1303
+ renewable: !repo.inlineAuth,
1304
+ expiresAt: repo.authExpiresAt
1305
+ };
1306
+ return {
1307
+ files: [
1308
+ {
1309
+ path: "~/.gitterm/github/runtime.cjs",
1310
+ contentBase64: Buffer.from(GITHUB_AUTH_RUNTIME).toString("base64"),
1311
+ mode: 384
1312
+ },
1313
+ {
1314
+ path: "~/.gitterm/github/config.json",
1315
+ contentBase64: Buffer.from(JSON.stringify(config)).toString("base64"),
1316
+ mode: 384
1317
+ },
1318
+ {
1319
+ path: "/run/gitterm/github/credential.json",
1320
+ contentBase64: Buffer.from(JSON.stringify({
1321
+ token: repo.authToken,
1322
+ username: repo.authUsername ?? "x-access-token",
1323
+ expiresAt: repo.authExpiresAt
1324
+ })).toString("base64"),
1325
+ mode: 384
1326
+ }
1327
+ ],
1328
+ setup: `node "$HOME/.gitterm/github/runtime.cjs" setup || exit $?
1329
+ ${GITHUB_AUTH_PATH}`
1330
+ };
1331
+ }
1332
+ var GITHUB_AUTH_RUNTIME = String.raw`
1333
+ const fs = require("node:fs");
1334
+ const path = require("node:path");
1335
+ const os = require("node:os");
1336
+ const cp = require("node:child_process");
1337
+ const { randomUUID } = require("node:crypto");
1338
+ const directory = path.join(os.homedir(), ".gitterm/github");
1339
+ const runtimeDirectory = path.join(process.env.GITTERM_RUNTIME_DIR || "/run/gitterm", "github");
1340
+ const bin = path.join(os.homedir(), ".gitterm/bin");
1341
+ const configFile = path.join(directory, "config.json");
1342
+ const credentialFile = path.join(runtimeDirectory, "credential.json");
1343
+ const cacheFile = path.join(runtimeDirectory, "cache.json");
1344
+ const read = (file) => JSON.parse(fs.readFileSync(file, "utf8"));
1345
+ const valid = (value) => typeof value === "string" && value.length > 0 && !/[\r\n\0]/.test(value);
1346
+ const fresh = (value) => valid(value?.token) && Date.parse(value.expiresAt) > Date.now() + 300000;
1347
+ const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
1348
+
1349
+ function atomicWrite(file, value) {
1350
+ const temporary = file + "." + randomUUID();
1351
+ try {
1352
+ fs.writeFileSync(temporary, JSON.stringify(value), { mode: 0o600, flag: "wx" });
1353
+ fs.renameSync(temporary, file);
1354
+ } finally {
1355
+ fs.rmSync(temporary, { force: true });
1356
+ }
1357
+ }
1358
+
1359
+ async function credential() {
1360
+ const config = read(configFile);
1361
+ const initial = read(credentialFile);
1362
+ if (!config.renewable) {
1363
+ if (!valid(initial.token) || !valid(initial.username)) throw new Error("invalid credential");
1364
+ return initial;
1365
+ }
1366
+ const cached = () => {
1367
+ try { const value = read(cacheFile); if (fresh(value)) return value; } catch {}
1368
+ if (fresh(initial)) return initial;
1369
+ };
1370
+ let value = cached();
1371
+ if (value) return value;
1372
+ const lock = path.join(runtimeDirectory, "refresh.lock");
1373
+ const deadline = Date.now() + 15000;
1374
+ while (true) {
1375
+ try { fs.mkdirSync(lock, { mode: 0o700 }); break; }
1376
+ catch (error) {
1377
+ if (error.code !== "EEXIST") throw error;
1378
+ try {
1379
+ if (Date.now() - fs.statSync(lock).mtimeMs > 30000) fs.rmdirSync(lock);
1380
+ } catch {}
1381
+ if (Date.now() > deadline) throw new Error("refresh busy");
1382
+ await delay(100);
1383
+ value = cached();
1384
+ if (value) return value;
1385
+ }
1386
+ }
1387
+ try {
1388
+ value = cached();
1389
+ if (value) return value;
1390
+ const url = process.env.WORKSPACE_API_URL;
1391
+ const auth = process.env.WORKSPACE_AGENT_AUTH_TOKEN;
1392
+ if (!url || !auth) throw new Error("refresh unavailable");
1393
+ const response = await fetch(url.replace(/\/$/, "") + "/workspaceOps.gitCredential", {
1394
+ method: "POST",
1395
+ headers: { Authorization: "Bearer " + auth, "Content-Type": "application/json" },
1396
+ body: "{}",
1397
+ redirect: "error",
1398
+ signal: AbortSignal.timeout(10000),
1399
+ });
1400
+ if (!response.ok) throw new Error("refresh failed");
1401
+ const body = await response.json();
1402
+ const data = body.result?.data?.json ?? body.result?.data;
1403
+ if (!fresh(data)) throw new Error("invalid credential");
1404
+ value = { token: data.token, expiresAt: data.expiresAt, username: "x-access-token" };
1405
+ atomicWrite(cacheFile, value);
1406
+ return value;
1407
+ } finally { fs.rmdirSync(lock); }
1408
+ }
1409
+
1410
+ function setup() {
1411
+ fs.mkdirSync(bin, { recursive: true, mode: 0o700 });
1412
+ fs.mkdirSync(runtimeDirectory, { recursive: true, mode: 0o700 });
1413
+ fs.chmodSync(directory, 0o700);
1414
+ fs.chmodSync(runtimeDirectory, 0o700);
1415
+ fs.chmodSync(configFile, 0o600);
1416
+ fs.chmodSync(credentialFile, 0o600);
1417
+ for (const [name, mode] of [["gh", "gh"], ["git-credential-gitterm", "credential"]]) {
1418
+ const file = path.join(bin, name);
1419
+ fs.writeFileSync(file, '#!/bin/sh\nexec node "$HOME/.gitterm/github/runtime.cjs" ' + mode + ' "$@"\n', { mode: 0o700 });
1420
+ fs.chmodSync(file, 0o700);
1421
+ }
1422
+ // Reset only GitHub's helper chain. Other hosts retain their own credentials.
1423
+ // Retire our clone-time helpers, which predate host-scoped runtime auth.
1424
+ let helpers = [];
1425
+ try { helpers = cp.execFileSync("git", ["config", "--global", "--get-all", "credential.helper"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim().split("\n"); } catch {}
1426
+ for (const helper of helpers) {
1427
+ if (["/usr/local/bin/gitterm-git-credential", "/run/gitterm/git-credential-helper.sh", "/tmp/gitterm-repository-auth/git-credential-helper.sh", "/workspace/.git-credential-helper.sh"].includes(helper) || helper.includes('password=$GITHUB_APP_TOKEN')) {
1428
+ cp.execFileSync("git", ["config", "--global", "--fixed-value", "--unset-all", "credential.helper", helper]);
1429
+ }
1430
+ }
1431
+ const key = "credential.https://github.com.helper";
1432
+ cp.execFileSync("git", ["config", "--global", "--replace-all", key, ""]);
1433
+ cp.execFileSync("git", ["config", "--global", "--add", key, '!node "$HOME/.gitterm/github/runtime.cjs" credential']);
1434
+ // Login shells and interactive agent shells may reset PATH. No secrets in profiles.
1435
+ const line = 'export PATH="$HOME/.gitterm/bin:$PATH" # gitterm-github-cli';
1436
+ for (const name of [".profile", ".bashrc", ".zshrc"]) {
1437
+ const file = path.join(os.homedir(), name);
1438
+ const text = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
1439
+ if (!text.includes(line)) fs.appendFileSync(file, "\n" + line + "\n", { mode: 0o600 });
1440
+ }
1441
+ }
1442
+
1443
+ function githubTarget(args) {
1444
+ let host = process.env.GH_HOST;
1445
+ let repo = process.env.GH_REPO;
1446
+ for (let i = 0; i < args.length; i++) {
1447
+ if (args[i] === "--hostname") host = args[++i];
1448
+ else if (args[i].startsWith("--hostname=")) host = args[i].slice(11);
1449
+ else if (args[i] === "--repo" || args[i] === "-R") repo = args[++i];
1450
+ else if (args[i].startsWith("--repo=")) repo = args[i].slice(7);
1451
+ else if (args[i].startsWith("-R") && args[i].length > 2) repo = args[i].slice(2);
1452
+ }
1453
+ if (host && host.toLowerCase() !== "github.com") return false;
1454
+ if (repo && !/^(https:\/\/github\.com\/|github\.com\/)?[^/]+\/[^/]+\/?$/.test(repo)) return false;
1455
+ if (args.some((arg) => /^https?:\/\//.test(arg) && !/^https:\/\/(api\.)?github\.com\//.test(arg))) return false;
1456
+ if (!host && !repo) {
1457
+ try {
1458
+ const origin = cp.execFileSync("git", ["remote", "get-url", "origin"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
1459
+ if (!/^(https:\/\/github\.com\/|git@github\.com:|ssh:\/\/git@github\.com\/)/i.test(origin)) return false;
1460
+ } catch {}
1461
+ }
1462
+ return true;
1463
+ }
1464
+
1465
+ async function main() {
1466
+ const [mode, ...args] = process.argv.slice(2);
1467
+ if (mode === "setup") return setup();
1468
+ if (mode === "credential") {
1469
+ if (args[0] !== "get") return;
1470
+ const input = fs.readFileSync(0, "utf8");
1471
+ const fields = Object.fromEntries(input.trim().split("\n").map((line) => { const i = line.indexOf("="); return [line.slice(0, i), line.slice(i + 1)]; }));
1472
+ if (fields.protocol !== "https" || fields.host !== "github.com") return;
1473
+ const value = await credential();
1474
+ process.stdout.write("username=" + (value.username ?? "x-access-token") + "\npassword=" + value.token + "\n");
1475
+ return;
1476
+ }
1477
+ if (mode !== "gh") throw new Error("invalid mode");
1478
+ // Find the actual binary on every invocation, including installations made later.
1479
+ const wrapper = fs.realpathSync(path.join(bin, "gh"));
1480
+ let executable;
1481
+ for (const entry of (process.env.PATH ?? "").split(path.delimiter)) {
1482
+ if (!entry) continue;
1483
+ const candidate = path.resolve(entry, "gh");
1484
+ try {
1485
+ if (fs.statSync(candidate).isFile() && fs.realpathSync(candidate) !== wrapper) {
1486
+ fs.accessSync(candidate, fs.constants.X_OK); executable = candidate; break;
1487
+ }
1488
+ } catch {}
1489
+ }
1490
+ if (!executable) {
1491
+ console.error("GitHub CLI is not installed in this image. Install gh, then retry.");
1492
+ process.exitCode = 127;
1493
+ return;
1494
+ }
1495
+ const env = { ...process.env };
1496
+ if (!env.GH_TOKEN && !env.GITHUB_TOKEN && args.length && githubTarget(args) && !args.some((arg) => ["--help", "--version"].includes(arg)) && args[0] !== "help") {
1497
+ env.GH_TOKEN = (await credential()).token;
1498
+ }
1499
+ const child = cp.spawn(executable, args, { env, stdio: "inherit" });
1500
+ const handlers = new Map();
1501
+ for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
1502
+ const handler = () => child.kill(signal);
1503
+ handlers.set(signal, handler);
1504
+ process.on(signal, handler);
1505
+ }
1506
+ child.on("error", () => { console.error("Could not start GitHub CLI."); process.exitCode = 127; });
1507
+ child.on("exit", (code, signal) => {
1508
+ for (const [name, handler] of handlers) process.removeListener(name, handler);
1509
+ if (signal) process.kill(process.pid, signal);
1510
+ else process.exitCode = code ?? 1;
1511
+ });
1512
+ }
1513
+ main().catch(() => {
1514
+ console.error("GitTerm GitHub authentication failed; check the workspace credentials or integration.");
1515
+ process.exitCode = 1;
1516
+ });
1517
+ `;
1518
+
1246
1519
  // src/direct/provisioning.ts
1247
1520
  var DIRECT_OPENCODE_PORT = 4096;
1248
1521
  var DIRECT_OPENCODE_COMMAND = `opencode serve --hostname 0.0.0.0 --port ${DIRECT_OPENCODE_PORT}`;
@@ -1251,7 +1524,9 @@ var DIRECT_E2B_TEMPLATES = {
1251
1524
  standard: "gitterm-opencode-server",
1252
1525
  large: "gitterm-opencode-server-lg"
1253
1526
  };
1254
- var DIRECT_GITTERM_INSTRUCTIONS = "You are running in a direct Gitterm workspace. Follow the user's instructions and verify outcomes before reporting success.";
1527
+ var DIRECT_GITTERM_INSTRUCTIONS = `You are running in a direct Gitterm workspace. Follow the user's instructions and verify outcomes before reporting success.
1528
+
1529
+ ${GITHUB_CLI_INSTRUCTIONS}`;
1255
1530
  var MAX_ADDITIONAL_AGENT_INSTRUCTIONS = 50000;
1256
1531
  function resolveDirectImage(image) {
1257
1532
  return image?.trim() || DIRECT_OPENCODE_SERVER_IMAGE;
@@ -1368,7 +1643,14 @@ function buildDirectProvisioningPlan(input) {
1368
1643
  },
1369
1644
  ...plugins.length ? { plugin: plugins } : {}
1370
1645
  };
1646
+ const github = githubAuthProvisioning(input.repo ? {
1647
+ url: input.repo,
1648
+ authToken: input.repositoryCredentials?.token,
1649
+ authUsername: input.repositoryCredentials?.username,
1650
+ inlineAuth: true
1651
+ } : undefined);
1371
1652
  const files = [
1653
+ ...github?.files ?? [],
1372
1654
  {
1373
1655
  path: "~/.local/share/opencode/auth.json",
1374
1656
  contentBase64: base64(JSON.stringify(Object.fromEntries(credentials)))
@@ -1412,11 +1694,15 @@ function buildDirectProvisioningPlan(input) {
1412
1694
  ...environmentVariables,
1413
1695
  OPENCODE_SERVER_PASSWORD: input.password
1414
1696
  },
1415
- command: DIRECT_OPENCODE_COMMAND,
1697
+ command: github ? githubAuthCommand(DIRECT_OPENCODE_COMMAND) : DIRECT_OPENCODE_COMMAND,
1416
1698
  port: DIRECT_OPENCODE_PORT
1417
1699
  },
1418
1700
  setup: {
1419
- beforeAgent: input.setup?.beforeAgent ?? [],
1701
+ beforeAgent: [
1702
+ ...(input.opencode?.api ?? "v2") === "v2" ? [OPENCODE_V2_IMPORT_CREDENTIALS] : [],
1703
+ ...github ? [github.setup] : [],
1704
+ ...input.setup?.beforeAgent ?? []
1705
+ ],
1420
1706
  afterAgent: input.setup?.afterAgent ?? []
1421
1707
  }
1422
1708
  };
@@ -1611,7 +1897,7 @@ function createAsciiDirectProvider(config) {
1611
1897
  });
1612
1898
  await runCommand(handle.boxId, `mkdir -p ${shellQuote(directory)}`);
1613
1899
  for (const command of config.runtimeSetupCommands ?? [
1614
- "npm install -g opencode-ai --no-audit --fund=false"
1900
+ "npm install -g @opencode/cli@2 --no-audit --fund=false"
1615
1901
  ]) {
1616
1902
  await runCommand(handle.boxId, command, undefined, 600);
1617
1903
  }
@@ -2603,7 +2889,7 @@ function createVercelDirectProvider(config) {
2603
2889
  });
2604
2890
  try {
2605
2891
  await run(sandbox, `mkdir -p ${shellQuote(handle.directory)}`);
2606
- for (const command of config.runtimeSetupCommands ?? (config.image ? [] : ["npm install -g opencode-ai --no-audit --fund=false"])) {
2892
+ for (const command of config.runtimeSetupCommands ?? (config.image ? [] : ["npm install -g @opencode/cli@2 --no-audit --fund=false"])) {
2607
2893
  await run(sandbox, command);
2608
2894
  }
2609
2895
  if (plan.repository) {
@@ -2763,7 +3049,7 @@ function runtimeClient(workspace, fetchImpl) {
2763
3049
  }
2764
3050
  function createAuthClient(workspace, fetchImpl) {
2765
3051
  if (workspace.opencodeApi !== "v2")
2766
- throw new GittermError("BAD_REQUEST", "Runtime OAuth connection management requires OpenCode v2; use inline credentials or a managed workspace with v1");
3052
+ throw new GittermError("BAD_REQUEST", "Runtime OAuth connection management requires OpenCode v2");
2767
3053
  const authorization = workspace.runtime.password ? `Basic ${Buffer.from(`opencode:${workspace.runtime.password}`).toString("base64")}` : undefined;
2768
3054
  return createOpencodeV2Client({
2769
3055
  fetch: fetchImpl,
@@ -6,7 +6,7 @@ export declare const DIRECT_E2B_TEMPLATES: {
6
6
  readonly standard: "gitterm-opencode-server";
7
7
  readonly large: "gitterm-opencode-server-lg";
8
8
  };
9
- export declare const DIRECT_GITTERM_INSTRUCTIONS = "You are running in a direct Gitterm workspace. Follow the user's instructions and verify outcomes before reporting success.";
9
+ export declare const DIRECT_GITTERM_INSTRUCTIONS = "You are running in a direct Gitterm workspace. Follow the user's instructions and verify outcomes before reporting success.\n\nGitHub CLI authentication:\n- The user may have supplied repository credentials or selected a GitHub integration for this workspace. GitTerm authentication is configured only when $HOME/.gitterm/bin/gh exists. Check for that executable, but never read or print files under $HOME/.gitterm/github.\n- The image may not include the real GitHub CLI. Run gh --version. If the GitTerm launcher says the CLI is missing, install the official gh binary using the image's package manager or in another directory.\n- Keep $HOME/.gitterm/bin first on PATH after installation. For a downloaded CLI, use: export PATH=\"$HOME/.gitterm/bin:/path/to/installed/gh/bin:$PATH\". Never prepend the real CLI ahead of $HOME/.gitterm/bin and never overwrite $HOME/.gitterm/bin/gh.\n- With the launcher first, use the normal gh command (for example, gh pr create). The launcher supplies the user-provided token or a fresh integration token only to that operation.\n- Never run gh auth login, pipe $GH_TOKEN, copy tokens, or use gh auth status for this workspace authentication. $GH_TOKEN is intentionally not exported to the agent shell.\n- If $HOME/.gitterm/bin/gh is absent, tell the user that this workspace has no GitTerm-provided GitHub credentials. If an authenticated operation is denied, report the denied operation and ask for repository access or the required GitHub App/token permission.";
10
10
  export declare function resolveDirectImage(image?: string): string;
11
11
  export declare function directModelAuth(credential: DirectModelCredential): {
12
12
  enterpriseUrl?: string | undefined;
@@ -126,6 +126,7 @@ export type DirectWorkspaceCreateInput = {
126
126
  branch?: string;
127
127
  baseCommit?: string;
128
128
  checkoutRef?: string;
129
+ /** Git credentials; also configure runtime Git/gh authentication for github.com. Not auto-renewed. */
129
130
  repositoryCredentials?: {
130
131
  username?: string;
131
132
  token: string;
@@ -146,7 +147,7 @@ export type DirectWorkspaceCreateInput = {
146
147
  /** Trusted integration context appended to the generated global AGENTS.md. */
147
148
  additionalAgentInstructions?: string;
148
149
  opencode?: {
149
- /** Defaults to v1. v2 requires a compatible provider image/template. */
150
+ /** Defaults to v2. */
150
151
  api?: OpencodeApi;
151
152
  config?: Record<string, unknown>;
152
153
  plugins?: string[];
package/dist/index.d.ts CHANGED
@@ -6,4 +6,4 @@ export type { DeviceCodeInfo, LoginWithDeviceCodeOptions } from "./device-login.
6
6
  export { AgentRunError, GittermError, WorkspaceLifecycleError } from "./errors.js";
7
7
  export { createGittermWorkspaceClient, getWorkspaceEnvironment, type GittermWorkspaceClient, type WorkspaceClientOptions, type WorkspaceEnvironment, type WorkspacePort, type WorkspaceSelf, } from "./workspace-client.js";
8
8
  export type { CredentialErrorCode, GittermErrorCode, WorkspaceLifecycleErrorCode, } from "./errors.js";
9
- export type { AgentPermissionRequest, AgentQuestion, AgentQuestionOption, AgentQuestionRequest, AgentRun, AgentRunCreateInput, AgentRunInputRequest, AgentRunMessage, AgentRunMessagePart, AgentRunReply, AgentRunStatus, AgentRunEvent, AgentRunResult, AgentRunReplyPermission, AgentRunReplyQuestion, RunResultOptions, RunRef, RunWaitOptions, RunWatchOptions, OpencodeApi, AgentType, AgentKey, AuthStatus, BuiltInAgentKey, CloudProvider, ProviderKey, Workspace, WorkspaceCreateInput, WorkspaceCreateResult, WorkspaceCatalog, WorkspaceEnsureRunningResult, WorkspaceHostingType, WorkspaceListOptions, WorkspaceListResult, WorkspaceRestartResult, WorkspaceRuntimeAccess, WorkspaceStatus, WorkspaceSetupStatus, ModelCredential, ModelProviderInfo, ModelInfo, ModelCredentialSource, WorkspaceModelsInput, WorkspaceModelAccess, WorkspacePauseResult, WorkspaceTerminateResult, WorkspaceProviderSelection, } from "./types.js";
9
+ export type { AgentPermissionRequest, AgentQuestion, AgentQuestionOption, AgentQuestionRequest, AgentRun, AgentRunCreateInput, AgentRunInputRequest, AgentRunMessage, AgentRunMessagePart, AgentRunReply, AgentRunStatus, AgentRunEvent, AgentRunResult, AgentRunReplyPermission, AgentRunReplyQuestion, RunResultOptions, RunRef, RunWaitOptions, RunWatchOptions, OpencodeApi, AgentType, AgentKey, AuthStatus, BuiltInAgentKey, CloudProvider, ProviderKey, Workspace, WorkspaceCreateInput, WorkspaceCreateResult, WorkspaceCatalog, WorkspaceEnsureRunningResult, WorkspaceHostingType, WorkspaceListOptions, WorkspaceListResult, WorkspaceRestartResult, WorkspaceRuntimeAccess, WorkspaceStatus, WorkspaceSetupStatus, ModelCredential, ModelProviderInfo, ModelInfo, ModelCredentialSource, WorkspaceModelsInput, WorkspaceModelAccess, WorkspacePauseResult, WorkspaceTerminateResult, WorkspaceProviderSelection, GitHubIntegration, GoogleCloudIntegration, } from "./types.js";
package/dist/index.js CHANGED
@@ -686,6 +686,34 @@ ${log}` : ""}`);
686
686
  })).filter((model) => !listOptions?.provider || model.provider === listOptions.provider);
687
687
  })
688
688
  },
689
+ integrations: {
690
+ github: {
691
+ list: () => run(async () => {
692
+ const result = await trpc.workspace.listUserInstallations.query();
693
+ return result.installations.map(({ git_integration, github_app_installation }) => ({
694
+ id: git_integration.id,
695
+ accountLogin: git_integration.providerAccountLogin,
696
+ accountType: github_app_installation.accountType,
697
+ repositorySelection: github_app_installation.repositorySelection,
698
+ suspended: github_app_installation.suspended,
699
+ connectedAt: toIso(git_integration.connectedAt)
700
+ }));
701
+ })
702
+ },
703
+ googleCloud: {
704
+ list: () => run(async () => {
705
+ const result = await trpc.googleCloud.list.query();
706
+ return result.map((integration) => ({
707
+ id: integration.id,
708
+ name: integration.name,
709
+ projectId: integration.projectId,
710
+ workloadIdentityProvider: integration.workloadIdentityProvider,
711
+ serviceAccountEmail: integration.serviceAccountEmail,
712
+ connectedAt: toIso(integration.connectedAt)
713
+ }));
714
+ })
715
+ }
716
+ },
689
717
  credentials: {
690
718
  list: () => run(async () => {
691
719
  const result = await trpc.modelCredentials.listMyCredentials.query();
package/dist/types.d.ts CHANGED
@@ -63,7 +63,7 @@ export type RunRef = {
63
63
  workspaceId: string;
64
64
  id: string;
65
65
  };
66
- /** `v1` = OpenCode 1.x (`/event`, `/session/*`); `v2` = OpenCode 2 (`/api/*`), experimental until 2.0 ships. */
66
+ /** `v1` = OpenCode 1.x (`/event`, `/session/*`); `v2` = OpenCode 2 (`/api/*`) and the default. */
67
67
  export type OpencodeApi = "v1" | "v2";
68
68
  export type WaitOptions = {
69
69
  timeoutMs?: number;
@@ -161,6 +161,7 @@ export type WorkspaceProviderSelection = {
161
161
  } | ({
162
162
  type: "aws";
163
163
  region?: string;
164
+ accessProfile?: string;
164
165
  } & Omit<ProviderSelectionBase, "machine"> & {
165
166
  machine?: FlexibleMachine<AwsResources>;
166
167
  }) | ({
@@ -223,11 +224,15 @@ export type WorkspaceCreateInput = {
223
224
  /** Provider intent. Defaults to the user's or deployment's preferred provider. */
224
225
  provider?: WorkspaceProviderSelection;
225
226
  /** Inline Git credentials for repository validation, cloning, and runtime pull/push. */
227
+ /** Runtime Git/gh authentication. Takes precedence over gitIntegrationId; not auto-renewed. */
226
228
  repositoryCredentials?: {
227
229
  username?: string;
228
230
  token: string;
229
231
  };
232
+ /** Dashboard GitHub App integration; runtime Git/gh credentials are renewed automatically. */
230
233
  gitIntegrationId?: string;
234
+ /** Google Workload Identity Federation integration; injects keyless ADC/gcloud auth. */
235
+ googleCloudIntegrationId?: string;
231
236
  /** Defaults from the selected provider. */
232
237
  persistent?: boolean;
233
238
  workspaceProfile?: "standard" | "ssh-enabled";
@@ -267,12 +272,27 @@ export type WorkspaceCreateInput = {
267
272
  */
268
273
  config?: Record<string, unknown>;
269
274
  /**
270
- * OpenCode API generation served by the image. Defaults to `v1`. Use `v2`
271
- * with an image built on OpenCode 2 (`@opencode-ai/cli`); experimental.
275
+ * OpenCode API generation served by the image. Defaults to `v2`.
272
276
  */
273
277
  api?: OpencodeApi;
274
278
  };
275
279
  };
280
+ export type GitHubIntegration = {
281
+ id: string;
282
+ accountLogin: string;
283
+ accountType: string;
284
+ repositorySelection: string;
285
+ suspended: boolean;
286
+ connectedAt: string;
287
+ };
288
+ export type GoogleCloudIntegration = {
289
+ id: string;
290
+ name: string;
291
+ projectId: string;
292
+ workloadIdentityProvider: string;
293
+ serviceAccountEmail: string;
294
+ connectedAt: string;
295
+ };
276
296
  export type WorkspaceRestartResult = {
277
297
  status: WorkspaceStatus;
278
298
  };
@@ -309,6 +329,7 @@ export type AgentQuestionOption = {
309
329
  label: string;
310
330
  description: string;
311
331
  };
332
+ /** Options are normalised: OpenCode's own type-your-own entry, duplicates and blank labels never appear. */
312
333
  export type AgentQuestion = {
313
334
  /** Identifies this question in `AgentRunReply.answers`. Unique within a request. */
314
335
  key: string;
@@ -593,6 +614,12 @@ export type AgentType = {
593
614
  export type CloudProvider = {
594
615
  id: string;
595
616
  name: string;
617
+ awsAccessProfiles?: Array<{
618
+ id: string;
619
+ name: string;
620
+ description: string;
621
+ roleArn: string;
622
+ }>;
596
623
  providerKey: ProviderKey | string;
597
624
  regions?: Array<{
598
625
  id: string;
@@ -622,6 +649,12 @@ export type WorkspaceCatalog = {
622
649
  name: string;
623
650
  location: string;
624
651
  }>;
652
+ accessProfiles?: Array<{
653
+ id: string;
654
+ name: string;
655
+ description: string;
656
+ roleArn: string;
657
+ }>;
625
658
  machines: Array<{
626
659
  id: string;
627
660
  key: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitterm/sdk",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "files": [
5
5
  "dist"
6
6
  ],