@gitterm/sdk 0.2.2 → 0.4.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 +72 -9
- package/dist/client.d.ts +9 -1
- package/dist/direct/index.js +257 -6
- package/dist/direct/provisioning.d.ts +1 -1
- package/dist/direct/types.d.ts +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +28 -0
- package/dist/types.d.ts +35 -2
- 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
|
|
@@ -394,7 +450,10 @@ Three things have to be true before a prompt can be delivered, and the SDK handl
|
|
|
394
450
|
The shortest correct sequence is therefore:
|
|
395
451
|
|
|
396
452
|
```ts
|
|
397
|
-
const { workspace } = await client.workspaces.create({
|
|
453
|
+
const { workspace } = await client.workspaces.create({
|
|
454
|
+
repo,
|
|
455
|
+
setup: { afterAgent: ["npm ci"] },
|
|
456
|
+
});
|
|
398
457
|
const run = await client.runs.create({
|
|
399
458
|
workspace,
|
|
400
459
|
idempotencyKey: `review-${sha}`,
|
|
@@ -626,11 +685,10 @@ would otherwise change them for other runs on the same runtime.
|
|
|
626
685
|
|
|
627
686
|
## OpenCode API versions
|
|
628
687
|
|
|
629
|
-
Managed workspaces
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
workspace. `v2` is experimental until OpenCode 2 ships.
|
|
688
|
+
Managed workspaces default to OpenCode V1 from `opencode-ai` and use `opencodeApi: "v1"`.
|
|
689
|
+
Credentials are written to OpenCode's `auth.json` before the runtime starts. Runs, questions,
|
|
690
|
+
permissions, and events use GitTerm's V1 runtime adapter. V2 remains selectable only for explicitly
|
|
691
|
+
compatible images.
|
|
634
692
|
|
|
635
693
|
## Errors
|
|
636
694
|
|
|
@@ -729,12 +787,17 @@ let workspace = await direct.workspaces.create({
|
|
|
729
787
|
repo: "https://github.com/acme/project",
|
|
730
788
|
lifecycle: "ephemeral",
|
|
731
789
|
models: {
|
|
732
|
-
providers: {
|
|
790
|
+
providers: {
|
|
791
|
+
anthropic: { source: "apiKey", apiKey: process.env.ANTHROPIC_API_KEY! },
|
|
792
|
+
},
|
|
733
793
|
},
|
|
734
794
|
});
|
|
735
795
|
|
|
736
796
|
try {
|
|
737
|
-
const run = await direct.runs.create({
|
|
797
|
+
const run = await direct.runs.create({
|
|
798
|
+
workspace,
|
|
799
|
+
prompt: "Review the open pull request",
|
|
800
|
+
});
|
|
738
801
|
const completed = await direct.runs.result(run);
|
|
739
802
|
console.log(completed.finalText);
|
|
740
803
|
} finally {
|
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
|
@@ -1273,6 +1273,243 @@ import {
|
|
|
1273
1273
|
// src/direct/ascii.ts
|
|
1274
1274
|
import { BoxApi, Configuration, waitUntilReady } from "@asciidev/box-sdk";
|
|
1275
1275
|
|
|
1276
|
+
// ../agent-runtime/src/github-auth.ts
|
|
1277
|
+
var GITHUB_AUTH_PATH = 'export PATH="$HOME/.gitterm/bin:$PATH"';
|
|
1278
|
+
var GITHUB_CLI_INSTRUCTIONS = `GitHub CLI authentication:
|
|
1279
|
+
- 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.
|
|
1280
|
+
- 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.
|
|
1281
|
+
- 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.
|
|
1282
|
+
- 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.
|
|
1283
|
+
- 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.
|
|
1284
|
+
- 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.`;
|
|
1285
|
+
function githubAuthCommand(command) {
|
|
1286
|
+
return `env PATH="$HOME/.gitterm/bin:$PATH" sh -c '${command.replaceAll("'", `'"'"'`)}'`;
|
|
1287
|
+
}
|
|
1288
|
+
function hasGithubAuth(repo) {
|
|
1289
|
+
return Boolean(repo?.authToken && /^(https:\/\/github\.com\/|git@github\.com:|ssh:\/\/git@github\.com\/)/i.test(repo.url));
|
|
1290
|
+
}
|
|
1291
|
+
function githubAuthProvisioning(repo) {
|
|
1292
|
+
if (!hasGithubAuth(repo)) {
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
const config = {
|
|
1296
|
+
username: repo.authUsername ?? "x-access-token",
|
|
1297
|
+
renewable: !repo.inlineAuth,
|
|
1298
|
+
expiresAt: repo.authExpiresAt
|
|
1299
|
+
};
|
|
1300
|
+
return {
|
|
1301
|
+
files: [
|
|
1302
|
+
{
|
|
1303
|
+
path: "~/.gitterm/github/runtime.cjs",
|
|
1304
|
+
contentBase64: Buffer.from(GITHUB_AUTH_RUNTIME).toString("base64"),
|
|
1305
|
+
mode: 384
|
|
1306
|
+
},
|
|
1307
|
+
{
|
|
1308
|
+
path: "~/.gitterm/github/config.json",
|
|
1309
|
+
contentBase64: Buffer.from(JSON.stringify(config)).toString("base64"),
|
|
1310
|
+
mode: 384
|
|
1311
|
+
},
|
|
1312
|
+
{
|
|
1313
|
+
path: "/run/gitterm/github/credential.json",
|
|
1314
|
+
contentBase64: Buffer.from(JSON.stringify({
|
|
1315
|
+
token: repo.authToken,
|
|
1316
|
+
username: repo.authUsername ?? "x-access-token",
|
|
1317
|
+
expiresAt: repo.authExpiresAt
|
|
1318
|
+
})).toString("base64"),
|
|
1319
|
+
mode: 384
|
|
1320
|
+
}
|
|
1321
|
+
],
|
|
1322
|
+
setup: `node "$HOME/.gitterm/github/runtime.cjs" setup || exit $?
|
|
1323
|
+
${GITHUB_AUTH_PATH}`
|
|
1324
|
+
};
|
|
1325
|
+
}
|
|
1326
|
+
var GITHUB_AUTH_RUNTIME = String.raw`
|
|
1327
|
+
const fs = require("node:fs");
|
|
1328
|
+
const path = require("node:path");
|
|
1329
|
+
const os = require("node:os");
|
|
1330
|
+
const cp = require("node:child_process");
|
|
1331
|
+
const { randomUUID } = require("node:crypto");
|
|
1332
|
+
const directory = path.join(os.homedir(), ".gitterm/github");
|
|
1333
|
+
const runtimeDirectory = path.join(process.env.GITTERM_RUNTIME_DIR || "/run/gitterm", "github");
|
|
1334
|
+
const bin = path.join(os.homedir(), ".gitterm/bin");
|
|
1335
|
+
const configFile = path.join(directory, "config.json");
|
|
1336
|
+
const credentialFile = path.join(runtimeDirectory, "credential.json");
|
|
1337
|
+
const cacheFile = path.join(runtimeDirectory, "cache.json");
|
|
1338
|
+
const read = (file) => JSON.parse(fs.readFileSync(file, "utf8"));
|
|
1339
|
+
const valid = (value) => typeof value === "string" && value.length > 0 && !/[\r\n\0]/.test(value);
|
|
1340
|
+
const fresh = (value) => valid(value?.token) && Date.parse(value.expiresAt) > Date.now() + 300000;
|
|
1341
|
+
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
1342
|
+
|
|
1343
|
+
function atomicWrite(file, value) {
|
|
1344
|
+
const temporary = file + "." + randomUUID();
|
|
1345
|
+
try {
|
|
1346
|
+
fs.writeFileSync(temporary, JSON.stringify(value), { mode: 0o600, flag: "wx" });
|
|
1347
|
+
fs.renameSync(temporary, file);
|
|
1348
|
+
} finally {
|
|
1349
|
+
fs.rmSync(temporary, { force: true });
|
|
1350
|
+
}
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
async function credential() {
|
|
1354
|
+
const config = read(configFile);
|
|
1355
|
+
const initial = read(credentialFile);
|
|
1356
|
+
if (!config.renewable) {
|
|
1357
|
+
if (!valid(initial.token) || !valid(initial.username)) throw new Error("invalid credential");
|
|
1358
|
+
return initial;
|
|
1359
|
+
}
|
|
1360
|
+
const cached = () => {
|
|
1361
|
+
try { const value = read(cacheFile); if (fresh(value)) return value; } catch {}
|
|
1362
|
+
if (fresh(initial)) return initial;
|
|
1363
|
+
};
|
|
1364
|
+
let value = cached();
|
|
1365
|
+
if (value) return value;
|
|
1366
|
+
const lock = path.join(runtimeDirectory, "refresh.lock");
|
|
1367
|
+
const deadline = Date.now() + 15000;
|
|
1368
|
+
while (true) {
|
|
1369
|
+
try { fs.mkdirSync(lock, { mode: 0o700 }); break; }
|
|
1370
|
+
catch (error) {
|
|
1371
|
+
if (error.code !== "EEXIST") throw error;
|
|
1372
|
+
try {
|
|
1373
|
+
if (Date.now() - fs.statSync(lock).mtimeMs > 30000) fs.rmdirSync(lock);
|
|
1374
|
+
} catch {}
|
|
1375
|
+
if (Date.now() > deadline) throw new Error("refresh busy");
|
|
1376
|
+
await delay(100);
|
|
1377
|
+
value = cached();
|
|
1378
|
+
if (value) return value;
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
try {
|
|
1382
|
+
value = cached();
|
|
1383
|
+
if (value) return value;
|
|
1384
|
+
const url = process.env.WORKSPACE_API_URL;
|
|
1385
|
+
const auth = process.env.WORKSPACE_AGENT_AUTH_TOKEN;
|
|
1386
|
+
if (!url || !auth) throw new Error("refresh unavailable");
|
|
1387
|
+
const response = await fetch(url.replace(/\/$/, "") + "/workspaceOps.gitCredential", {
|
|
1388
|
+
method: "POST",
|
|
1389
|
+
headers: { Authorization: "Bearer " + auth, "Content-Type": "application/json" },
|
|
1390
|
+
body: "{}",
|
|
1391
|
+
redirect: "error",
|
|
1392
|
+
signal: AbortSignal.timeout(10000),
|
|
1393
|
+
});
|
|
1394
|
+
if (!response.ok) throw new Error("refresh failed");
|
|
1395
|
+
const body = await response.json();
|
|
1396
|
+
const data = body.result?.data?.json ?? body.result?.data;
|
|
1397
|
+
if (!fresh(data)) throw new Error("invalid credential");
|
|
1398
|
+
value = { token: data.token, expiresAt: data.expiresAt, username: "x-access-token" };
|
|
1399
|
+
atomicWrite(cacheFile, value);
|
|
1400
|
+
return value;
|
|
1401
|
+
} finally { fs.rmdirSync(lock); }
|
|
1402
|
+
}
|
|
1403
|
+
|
|
1404
|
+
function setup() {
|
|
1405
|
+
fs.mkdirSync(bin, { recursive: true, mode: 0o700 });
|
|
1406
|
+
fs.mkdirSync(runtimeDirectory, { recursive: true, mode: 0o700 });
|
|
1407
|
+
fs.chmodSync(directory, 0o700);
|
|
1408
|
+
fs.chmodSync(runtimeDirectory, 0o700);
|
|
1409
|
+
fs.chmodSync(configFile, 0o600);
|
|
1410
|
+
fs.chmodSync(credentialFile, 0o600);
|
|
1411
|
+
for (const [name, mode] of [["gh", "gh"], ["git-credential-gitterm", "credential"]]) {
|
|
1412
|
+
const file = path.join(bin, name);
|
|
1413
|
+
fs.writeFileSync(file, '#!/bin/sh\nexec node "$HOME/.gitterm/github/runtime.cjs" ' + mode + ' "$@"\n', { mode: 0o700 });
|
|
1414
|
+
fs.chmodSync(file, 0o700);
|
|
1415
|
+
}
|
|
1416
|
+
// Reset only GitHub's helper chain. Other hosts retain their own credentials.
|
|
1417
|
+
// Retire our clone-time helpers, which predate host-scoped runtime auth.
|
|
1418
|
+
let helpers = [];
|
|
1419
|
+
try { helpers = cp.execFileSync("git", ["config", "--global", "--get-all", "credential.helper"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim().split("\n"); } catch {}
|
|
1420
|
+
for (const helper of helpers) {
|
|
1421
|
+
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')) {
|
|
1422
|
+
cp.execFileSync("git", ["config", "--global", "--fixed-value", "--unset-all", "credential.helper", helper]);
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
const key = "credential.https://github.com.helper";
|
|
1426
|
+
cp.execFileSync("git", ["config", "--global", "--replace-all", key, ""]);
|
|
1427
|
+
cp.execFileSync("git", ["config", "--global", "--add", key, '!node "$HOME/.gitterm/github/runtime.cjs" credential']);
|
|
1428
|
+
// Login shells and interactive agent shells may reset PATH. No secrets in profiles.
|
|
1429
|
+
const line = 'export PATH="$HOME/.gitterm/bin:$PATH" # gitterm-github-cli';
|
|
1430
|
+
for (const name of [".profile", ".bashrc", ".zshrc"]) {
|
|
1431
|
+
const file = path.join(os.homedir(), name);
|
|
1432
|
+
const text = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
|
|
1433
|
+
if (!text.includes(line)) fs.appendFileSync(file, "\n" + line + "\n", { mode: 0o600 });
|
|
1434
|
+
}
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
function githubTarget(args) {
|
|
1438
|
+
let host = process.env.GH_HOST;
|
|
1439
|
+
let repo = process.env.GH_REPO;
|
|
1440
|
+
for (let i = 0; i < args.length; i++) {
|
|
1441
|
+
if (args[i] === "--hostname") host = args[++i];
|
|
1442
|
+
else if (args[i].startsWith("--hostname=")) host = args[i].slice(11);
|
|
1443
|
+
else if (args[i] === "--repo" || args[i] === "-R") repo = args[++i];
|
|
1444
|
+
else if (args[i].startsWith("--repo=")) repo = args[i].slice(7);
|
|
1445
|
+
else if (args[i].startsWith("-R") && args[i].length > 2) repo = args[i].slice(2);
|
|
1446
|
+
}
|
|
1447
|
+
if (host && host.toLowerCase() !== "github.com") return false;
|
|
1448
|
+
if (repo && !/^(https:\/\/github\.com\/|github\.com\/)?[^/]+\/[^/]+\/?$/.test(repo)) return false;
|
|
1449
|
+
if (args.some((arg) => /^https?:\/\//.test(arg) && !/^https:\/\/(api\.)?github\.com\//.test(arg))) return false;
|
|
1450
|
+
if (!host && !repo) {
|
|
1451
|
+
try {
|
|
1452
|
+
const origin = cp.execFileSync("git", ["remote", "get-url", "origin"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
1453
|
+
if (!/^(https:\/\/github\.com\/|git@github\.com:|ssh:\/\/git@github\.com\/)/i.test(origin)) return false;
|
|
1454
|
+
} catch {}
|
|
1455
|
+
}
|
|
1456
|
+
return true;
|
|
1457
|
+
}
|
|
1458
|
+
|
|
1459
|
+
async function main() {
|
|
1460
|
+
const [mode, ...args] = process.argv.slice(2);
|
|
1461
|
+
if (mode === "setup") return setup();
|
|
1462
|
+
if (mode === "credential") {
|
|
1463
|
+
if (args[0] !== "get") return;
|
|
1464
|
+
const input = fs.readFileSync(0, "utf8");
|
|
1465
|
+
const fields = Object.fromEntries(input.trim().split("\n").map((line) => { const i = line.indexOf("="); return [line.slice(0, i), line.slice(i + 1)]; }));
|
|
1466
|
+
if (fields.protocol !== "https" || fields.host !== "github.com") return;
|
|
1467
|
+
const value = await credential();
|
|
1468
|
+
process.stdout.write("username=" + (value.username ?? "x-access-token") + "\npassword=" + value.token + "\n");
|
|
1469
|
+
return;
|
|
1470
|
+
}
|
|
1471
|
+
if (mode !== "gh") throw new Error("invalid mode");
|
|
1472
|
+
// Find the actual binary on every invocation, including installations made later.
|
|
1473
|
+
const wrapper = fs.realpathSync(path.join(bin, "gh"));
|
|
1474
|
+
let executable;
|
|
1475
|
+
for (const entry of (process.env.PATH ?? "").split(path.delimiter)) {
|
|
1476
|
+
if (!entry) continue;
|
|
1477
|
+
const candidate = path.resolve(entry, "gh");
|
|
1478
|
+
try {
|
|
1479
|
+
if (fs.statSync(candidate).isFile() && fs.realpathSync(candidate) !== wrapper) {
|
|
1480
|
+
fs.accessSync(candidate, fs.constants.X_OK); executable = candidate; break;
|
|
1481
|
+
}
|
|
1482
|
+
} catch {}
|
|
1483
|
+
}
|
|
1484
|
+
if (!executable) {
|
|
1485
|
+
console.error("GitHub CLI is not installed in this image. Install gh, then retry.");
|
|
1486
|
+
process.exitCode = 127;
|
|
1487
|
+
return;
|
|
1488
|
+
}
|
|
1489
|
+
const env = { ...process.env };
|
|
1490
|
+
if (!env.GH_TOKEN && !env.GITHUB_TOKEN && args.length && githubTarget(args) && !args.some((arg) => ["--help", "--version"].includes(arg)) && args[0] !== "help") {
|
|
1491
|
+
env.GH_TOKEN = (await credential()).token;
|
|
1492
|
+
}
|
|
1493
|
+
const child = cp.spawn(executable, args, { env, stdio: "inherit" });
|
|
1494
|
+
const handlers = new Map();
|
|
1495
|
+
for (const signal of ["SIGINT", "SIGTERM", "SIGHUP"]) {
|
|
1496
|
+
const handler = () => child.kill(signal);
|
|
1497
|
+
handlers.set(signal, handler);
|
|
1498
|
+
process.on(signal, handler);
|
|
1499
|
+
}
|
|
1500
|
+
child.on("error", () => { console.error("Could not start GitHub CLI."); process.exitCode = 127; });
|
|
1501
|
+
child.on("exit", (code, signal) => {
|
|
1502
|
+
for (const [name, handler] of handlers) process.removeListener(name, handler);
|
|
1503
|
+
if (signal) process.kill(process.pid, signal);
|
|
1504
|
+
else process.exitCode = code ?? 1;
|
|
1505
|
+
});
|
|
1506
|
+
}
|
|
1507
|
+
main().catch(() => {
|
|
1508
|
+
console.error("GitTerm GitHub authentication failed; check the workspace credentials or integration.");
|
|
1509
|
+
process.exitCode = 1;
|
|
1510
|
+
});
|
|
1511
|
+
`;
|
|
1512
|
+
|
|
1276
1513
|
// src/direct/provisioning.ts
|
|
1277
1514
|
var DIRECT_OPENCODE_PORT = 4096;
|
|
1278
1515
|
var DIRECT_OPENCODE_COMMAND = `opencode serve --hostname 0.0.0.0 --port ${DIRECT_OPENCODE_PORT}`;
|
|
@@ -1281,7 +1518,9 @@ var DIRECT_E2B_TEMPLATES = {
|
|
|
1281
1518
|
standard: "gitterm-opencode-server",
|
|
1282
1519
|
large: "gitterm-opencode-server-lg"
|
|
1283
1520
|
};
|
|
1284
|
-
var DIRECT_GITTERM_INSTRUCTIONS =
|
|
1521
|
+
var DIRECT_GITTERM_INSTRUCTIONS = `You are running in a direct Gitterm workspace. Follow the user's instructions and verify outcomes before reporting success.
|
|
1522
|
+
|
|
1523
|
+
${GITHUB_CLI_INSTRUCTIONS}`;
|
|
1285
1524
|
var MAX_ADDITIONAL_AGENT_INSTRUCTIONS = 50000;
|
|
1286
1525
|
function resolveDirectImage(image) {
|
|
1287
1526
|
return image?.trim() || DIRECT_OPENCODE_SERVER_IMAGE;
|
|
@@ -1372,7 +1611,10 @@ function buildDirectProvisioningPlan(input) {
|
|
|
1372
1611
|
throw new GittermError("MODEL_CREDENTIAL_INVALID", "Direct mode cannot inherit dashboard credentials");
|
|
1373
1612
|
}
|
|
1374
1613
|
for (const [key, source] of Object.entries(input.models?.providers ?? {})) {
|
|
1375
|
-
const credential = {
|
|
1614
|
+
const credential = {
|
|
1615
|
+
...source,
|
|
1616
|
+
providerName: key
|
|
1617
|
+
};
|
|
1376
1618
|
const providerName = key.trim();
|
|
1377
1619
|
if (!providerName)
|
|
1378
1620
|
throw new Error("Model credential providerName is required");
|
|
@@ -1398,7 +1640,14 @@ function buildDirectProvisioningPlan(input) {
|
|
|
1398
1640
|
},
|
|
1399
1641
|
...plugins.length ? { plugin: plugins } : {}
|
|
1400
1642
|
};
|
|
1643
|
+
const github = githubAuthProvisioning(input.repo ? {
|
|
1644
|
+
url: input.repo,
|
|
1645
|
+
authToken: input.repositoryCredentials?.token,
|
|
1646
|
+
authUsername: input.repositoryCredentials?.username,
|
|
1647
|
+
inlineAuth: true
|
|
1648
|
+
} : undefined);
|
|
1401
1649
|
const files = [
|
|
1650
|
+
...github?.files ?? [],
|
|
1402
1651
|
{
|
|
1403
1652
|
path: "~/.local/share/opencode/auth.json",
|
|
1404
1653
|
contentBase64: base64(JSON.stringify(Object.fromEntries(credentials)))
|
|
@@ -1442,11 +1691,11 @@ function buildDirectProvisioningPlan(input) {
|
|
|
1442
1691
|
...environmentVariables,
|
|
1443
1692
|
OPENCODE_SERVER_PASSWORD: input.password
|
|
1444
1693
|
},
|
|
1445
|
-
command: DIRECT_OPENCODE_COMMAND,
|
|
1694
|
+
command: github ? githubAuthCommand(DIRECT_OPENCODE_COMMAND) : DIRECT_OPENCODE_COMMAND,
|
|
1446
1695
|
port: DIRECT_OPENCODE_PORT
|
|
1447
1696
|
},
|
|
1448
1697
|
setup: {
|
|
1449
|
-
beforeAgent: input.setup?.beforeAgent ?? [],
|
|
1698
|
+
beforeAgent: [...github ? [github.setup] : [], ...input.setup?.beforeAgent ?? []],
|
|
1450
1699
|
afterAgent: input.setup?.afterAgent ?? []
|
|
1451
1700
|
}
|
|
1452
1701
|
};
|
|
@@ -1471,7 +1720,9 @@ function railwayContainerEnvironment(plan) {
|
|
|
1471
1720
|
} : {}
|
|
1472
1721
|
} : {},
|
|
1473
1722
|
AGENT_FILES_BASE64: base64(JSON.stringify(plan.agent.files)),
|
|
1474
|
-
...beforeAgent.length ? {
|
|
1723
|
+
...beforeAgent.length ? {
|
|
1724
|
+
WORKSPACE_SETUP_COMMAND_BASE64: base64(setupCommandScript(beforeAgent))
|
|
1725
|
+
} : {},
|
|
1475
1726
|
GITTERM_DIRECT_PROVIDER: "railway"
|
|
1476
1727
|
};
|
|
1477
1728
|
}
|
|
@@ -2793,7 +3044,7 @@ function runtimeClient(workspace, fetchImpl) {
|
|
|
2793
3044
|
}
|
|
2794
3045
|
function createAuthClient(workspace, fetchImpl) {
|
|
2795
3046
|
if (workspace.opencodeApi !== "v2")
|
|
2796
|
-
throw new GittermError("BAD_REQUEST", "Runtime OAuth connection management requires OpenCode v2
|
|
3047
|
+
throw new GittermError("BAD_REQUEST", "Runtime OAuth connection management requires OpenCode v2");
|
|
2797
3048
|
const authorization = workspace.runtime.password ? `Basic ${Buffer.from(`opencode:${workspace.runtime.password}`).toString("base64")}` : undefined;
|
|
2798
3049
|
return createOpencodeV2Client({
|
|
2799
3050
|
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;
|
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/*`) and the default; `v2` = OpenCode 2 (`/api/*`). */
|
|
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";
|
|
@@ -268,11 +273,27 @@ export type WorkspaceCreateInput = {
|
|
|
268
273
|
config?: Record<string, unknown>;
|
|
269
274
|
/**
|
|
270
275
|
* OpenCode API generation served by the image. Defaults to `v1`. Use `v2`
|
|
271
|
-
* with an
|
|
276
|
+
* only with an explicitly compatible image.
|
|
272
277
|
*/
|
|
273
278
|
api?: OpencodeApi;
|
|
274
279
|
};
|
|
275
280
|
};
|
|
281
|
+
export type GitHubIntegration = {
|
|
282
|
+
id: string;
|
|
283
|
+
accountLogin: string;
|
|
284
|
+
accountType: string;
|
|
285
|
+
repositorySelection: string;
|
|
286
|
+
suspended: boolean;
|
|
287
|
+
connectedAt: string;
|
|
288
|
+
};
|
|
289
|
+
export type GoogleCloudIntegration = {
|
|
290
|
+
id: string;
|
|
291
|
+
name: string;
|
|
292
|
+
projectId: string;
|
|
293
|
+
workloadIdentityProvider: string;
|
|
294
|
+
serviceAccountEmail: string;
|
|
295
|
+
connectedAt: string;
|
|
296
|
+
};
|
|
276
297
|
export type WorkspaceRestartResult = {
|
|
277
298
|
status: WorkspaceStatus;
|
|
278
299
|
};
|
|
@@ -594,6 +615,12 @@ export type AgentType = {
|
|
|
594
615
|
export type CloudProvider = {
|
|
595
616
|
id: string;
|
|
596
617
|
name: string;
|
|
618
|
+
awsAccessProfiles?: Array<{
|
|
619
|
+
id: string;
|
|
620
|
+
name: string;
|
|
621
|
+
description: string;
|
|
622
|
+
roleArn: string;
|
|
623
|
+
}>;
|
|
597
624
|
providerKey: ProviderKey | string;
|
|
598
625
|
regions?: Array<{
|
|
599
626
|
id: string;
|
|
@@ -623,6 +650,12 @@ export type WorkspaceCatalog = {
|
|
|
623
650
|
name: string;
|
|
624
651
|
location: string;
|
|
625
652
|
}>;
|
|
653
|
+
accessProfiles?: Array<{
|
|
654
|
+
id: string;
|
|
655
|
+
name: string;
|
|
656
|
+
description: string;
|
|
657
|
+
roleArn: string;
|
|
658
|
+
}>;
|
|
626
659
|
machines: Array<{
|
|
627
660
|
id: string;
|
|
628
661
|
key: string;
|