@gitterm/sdk 0.2.2 → 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 +61 -6
- package/dist/client.d.ts +9 -1
- package/dist/direct/index.js +262 -6
- package/dist/direct/provisioning.d.ts +1 -1
- package/dist/direct/types.d.ts +2 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +28 -0
- package/dist/types.d.ts +35 -3
- package/package.json +1 -1
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
|
-
|
|
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
|
|
@@ -626,11 +682,10 @@ would otherwise change them for other runs on the same runtime.
|
|
|
626
682
|
|
|
627
683
|
## OpenCode API versions
|
|
628
684
|
|
|
629
|
-
Managed workspaces
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
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.
|
|
634
689
|
|
|
635
690
|
## Errors
|
|
636
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;
|
package/dist/direct/index.js
CHANGED
|
@@ -823,6 +823,12 @@ function parseV2Signal(raw) {
|
|
|
823
823
|
return null;
|
|
824
824
|
}
|
|
825
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`;
|
|
826
832
|
// ../agent-runtime/src/derive.ts
|
|
827
833
|
var MISSING_ASSISTANT_GRACE_MS = 1e4;
|
|
828
834
|
function deriveRunState(snapshot, context) {
|
|
@@ -1273,6 +1279,243 @@ import {
|
|
|
1273
1279
|
// src/direct/ascii.ts
|
|
1274
1280
|
import { BoxApi, Configuration, waitUntilReady } from "@asciidev/box-sdk";
|
|
1275
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
|
+
|
|
1276
1519
|
// src/direct/provisioning.ts
|
|
1277
1520
|
var DIRECT_OPENCODE_PORT = 4096;
|
|
1278
1521
|
var DIRECT_OPENCODE_COMMAND = `opencode serve --hostname 0.0.0.0 --port ${DIRECT_OPENCODE_PORT}`;
|
|
@@ -1281,7 +1524,9 @@ var DIRECT_E2B_TEMPLATES = {
|
|
|
1281
1524
|
standard: "gitterm-opencode-server",
|
|
1282
1525
|
large: "gitterm-opencode-server-lg"
|
|
1283
1526
|
};
|
|
1284
|
-
var DIRECT_GITTERM_INSTRUCTIONS =
|
|
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}`;
|
|
1285
1530
|
var MAX_ADDITIONAL_AGENT_INSTRUCTIONS = 50000;
|
|
1286
1531
|
function resolveDirectImage(image) {
|
|
1287
1532
|
return image?.trim() || DIRECT_OPENCODE_SERVER_IMAGE;
|
|
@@ -1398,7 +1643,14 @@ function buildDirectProvisioningPlan(input) {
|
|
|
1398
1643
|
},
|
|
1399
1644
|
...plugins.length ? { plugin: plugins } : {}
|
|
1400
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);
|
|
1401
1652
|
const files = [
|
|
1653
|
+
...github?.files ?? [],
|
|
1402
1654
|
{
|
|
1403
1655
|
path: "~/.local/share/opencode/auth.json",
|
|
1404
1656
|
contentBase64: base64(JSON.stringify(Object.fromEntries(credentials)))
|
|
@@ -1442,11 +1694,15 @@ function buildDirectProvisioningPlan(input) {
|
|
|
1442
1694
|
...environmentVariables,
|
|
1443
1695
|
OPENCODE_SERVER_PASSWORD: input.password
|
|
1444
1696
|
},
|
|
1445
|
-
command: DIRECT_OPENCODE_COMMAND,
|
|
1697
|
+
command: github ? githubAuthCommand(DIRECT_OPENCODE_COMMAND) : DIRECT_OPENCODE_COMMAND,
|
|
1446
1698
|
port: DIRECT_OPENCODE_PORT
|
|
1447
1699
|
},
|
|
1448
1700
|
setup: {
|
|
1449
|
-
beforeAgent:
|
|
1701
|
+
beforeAgent: [
|
|
1702
|
+
...(input.opencode?.api ?? "v2") === "v2" ? [OPENCODE_V2_IMPORT_CREDENTIALS] : [],
|
|
1703
|
+
...github ? [github.setup] : [],
|
|
1704
|
+
...input.setup?.beforeAgent ?? []
|
|
1705
|
+
],
|
|
1450
1706
|
afterAgent: input.setup?.afterAgent ?? []
|
|
1451
1707
|
}
|
|
1452
1708
|
};
|
|
@@ -1641,7 +1897,7 @@ function createAsciiDirectProvider(config) {
|
|
|
1641
1897
|
});
|
|
1642
1898
|
await runCommand(handle.boxId, `mkdir -p ${shellQuote(directory)}`);
|
|
1643
1899
|
for (const command of config.runtimeSetupCommands ?? [
|
|
1644
|
-
"npm install -g opencode
|
|
1900
|
+
"npm install -g @opencode/cli@2 --no-audit --fund=false"
|
|
1645
1901
|
]) {
|
|
1646
1902
|
await runCommand(handle.boxId, command, undefined, 600);
|
|
1647
1903
|
}
|
|
@@ -2633,7 +2889,7 @@ function createVercelDirectProvider(config) {
|
|
|
2633
2889
|
});
|
|
2634
2890
|
try {
|
|
2635
2891
|
await run(sandbox, `mkdir -p ${shellQuote(handle.directory)}`);
|
|
2636
|
-
for (const command of config.runtimeSetupCommands ?? (config.image ? [] : ["npm install -g opencode
|
|
2892
|
+
for (const command of config.runtimeSetupCommands ?? (config.image ? [] : ["npm install -g @opencode/cli@2 --no-audit --fund=false"])) {
|
|
2637
2893
|
await run(sandbox, command);
|
|
2638
2894
|
}
|
|
2639
2895
|
if (plan.repository) {
|
|
@@ -2793,7 +3049,7 @@ function runtimeClient(workspace, fetchImpl) {
|
|
|
2793
3049
|
}
|
|
2794
3050
|
function createAuthClient(workspace, fetchImpl) {
|
|
2795
3051
|
if (workspace.opencodeApi !== "v2")
|
|
2796
|
-
throw new GittermError("BAD_REQUEST", "Runtime OAuth connection management requires OpenCode v2
|
|
3052
|
+
throw new GittermError("BAD_REQUEST", "Runtime OAuth connection management requires OpenCode v2");
|
|
2797
3053
|
const authorization = workspace.runtime.password ? `Basic ${Buffer.from(`opencode:${workspace.runtime.password}`).toString("base64")}` : undefined;
|
|
2798
3054
|
return createOpencodeV2Client({
|
|
2799
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;
|
package/dist/direct/types.d.ts
CHANGED
|
@@ -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
|
|
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/*`)
|
|
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 `
|
|
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
|
};
|
|
@@ -594,6 +614,12 @@ export type AgentType = {
|
|
|
594
614
|
export type CloudProvider = {
|
|
595
615
|
id: string;
|
|
596
616
|
name: string;
|
|
617
|
+
awsAccessProfiles?: Array<{
|
|
618
|
+
id: string;
|
|
619
|
+
name: string;
|
|
620
|
+
description: string;
|
|
621
|
+
roleArn: string;
|
|
622
|
+
}>;
|
|
597
623
|
providerKey: ProviderKey | string;
|
|
598
624
|
regions?: Array<{
|
|
599
625
|
id: string;
|
|
@@ -623,6 +649,12 @@ export type WorkspaceCatalog = {
|
|
|
623
649
|
name: string;
|
|
624
650
|
location: string;
|
|
625
651
|
}>;
|
|
652
|
+
accessProfiles?: Array<{
|
|
653
|
+
id: string;
|
|
654
|
+
name: string;
|
|
655
|
+
description: string;
|
|
656
|
+
roleArn: string;
|
|
657
|
+
}>;
|
|
626
658
|
machines: Array<{
|
|
627
659
|
id: string;
|
|
628
660
|
key: string;
|