@gitterm/sdk 0.1.0 → 0.1.2

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
@@ -10,14 +10,14 @@ Direct mode runs an agent using your cloud-provider account without a Gitterm se
10
10
 
11
11
  All built-in compute providers use the same provisioning plan and workspace/run API:
12
12
 
13
- | Provider | Direct prerequisite | Persistent pause | Keep-alive |
14
- | -------- | --------------------------------------------------------------------------------------- | ---------------- | ---------- |
15
- | E2B | OpenCode-compatible template | Yes | Yes |
16
- | Daytona | Public Gitterm OpenCode server image by default | Yes | Yes |
17
- | Vercel | Vercel Sandbox project | Yes | Yes |
18
- | Ascii | Box API key | Yes | Yes |
19
- | exe.dev | Token with `new,ls,ssh,share,ssh-key,pause,resume,rm`; public OpenCode image by default | Yes | No |
20
- | Railway | Project/environment and public service domains | With a volume | No |
13
+ | Provider | Direct prerequisite | Persistent pause | Keep-alive |
14
+ | -------- | -------------------------------------------------------------- | ---------------- | ---------- |
15
+ | E2B | OpenCode-compatible template | Yes | Yes |
16
+ | Daytona | Public Gitterm OpenCode server image by default | Yes | Yes |
17
+ | Vercel | Vercel Sandbox project | Yes | Yes |
18
+ | Ascii | Box API key | Yes | Yes |
19
+ | exe.dev | Lifecycle token, or an existing VM with `ls,ssh,share,ssh-key` | Yes | No |
20
+ | Railway | Project/environment and public service domains | With a volume | No |
21
21
 
22
22
  AWS remains available through `createGittermClient()` and the Gitterm control plane; it is intentionally not exposed in direct mode.
23
23
 
@@ -53,6 +53,43 @@ try {
53
53
 
54
54
  Every adapter receives the same normalized plan: repository/ref and optional Git credentials, agent files, model credentials, environment, setup commands, serve command, and port. Provider-specific configuration only describes how to allocate and expose compute.
55
55
 
56
+ Direct setup has explicit phases. `beforeAgent` blocks workspace creation, while
57
+ `afterAgent` runs in the background and can be observed with `setupStatus()` or
58
+ `waitForSetup()`:
59
+
60
+ ```ts
61
+ const workspace = await direct.workspaces.create({
62
+ repo: "https://github.com/acme/project",
63
+ setup: {
64
+ beforeAgent: ["npm install"],
65
+ afterAgent: ["npm run generate"],
66
+ },
67
+ secretFiles: [
68
+ {
69
+ path: "~/.config/gcloud/service-account.json",
70
+ content: process.env.GCP_SERVICE_ACCOUNT_JSON!,
71
+ mode: 0o600,
72
+ },
73
+ ],
74
+ });
75
+
76
+ await direct.workspaces.waitForSetup(workspace);
77
+ ```
78
+
79
+ To attach to an existing exe.dev VM without giving Gitterm ownership of that VM, pass
80
+ `exedev: { existingVmName: "acme-agent-machine" }` to `workspaces.create()`. Terminating
81
+ that workspace stops only its tracked agent process and does not remove the VM.
82
+
83
+ Trusted integration context can be appended to the generated global `AGENTS.md` without changing the model system prompt:
84
+
85
+ ```ts
86
+ await direct.workspaces.create({
87
+ repo: "https://github.com/acme/project",
88
+ additionalAgentInstructions:
89
+ "You are running as a Slack bot. Keep responses concise and suitable for a thread.",
90
+ });
91
+ ```
92
+
56
93
  ### Provider authentication
57
94
 
58
95
  Direct workspaces can start OpenCode provider authentication without shell access. Discover the provider's methods and select a headless or device-code OAuth method when OpenCode is running remotely:
@@ -187,6 +224,49 @@ const client = createGittermClient({
187
224
  const { workspaces } = await client.workspaces.list();
188
225
  ```
189
226
 
227
+ ### Managed private repositories
228
+
229
+ For renewable, short-lived repository authentication, connect the GitHub App in the GitTerm
230
+ dashboard and copy its **SDK integration ID** from the Integrations page:
231
+
232
+ ```ts
233
+ const { workspace, runtime } = await client.workspaces.create({
234
+ repo: "https://github.com/acme/private-repo",
235
+ branch: "main",
236
+ gitIntegrationId: "your-dashboard-integration-id",
237
+ });
238
+ ```
239
+
240
+ Managed workspaces can also use dashboard-managed model subscriptions while accepting an
241
+ application-owned GitHub PAT inline:
242
+
243
+ ```ts
244
+ const client = createGittermClient({
245
+ token: process.env.GITTERM_API_TOKEN,
246
+ });
247
+
248
+ const { workspace, runtime } = await client.workspaces.create({
249
+ repo: "https://github.com/acme/private-repo",
250
+ branch: "main",
251
+ repositoryCredentials: {
252
+ username: "x-access-token",
253
+ token: process.env.GITHUB_TOKEN!,
254
+ },
255
+ });
256
+ ```
257
+
258
+ The username defaults to `x-access-token`. Inline repository credentials take precedence over
259
+ `gitIntegrationId` and authenticate repository validation, cloning, and runtime Git operations such
260
+ as pull and push. Without inline credentials, `gitIntegrationId` continues to use the connected
261
+ dashboard integration. Omitting `modelCredentialIds` and `modelCredentials` likewise continues to
262
+ use dashboard-managed model credentials.
263
+
264
+ GitTerm does not save inline PATs in its application database. Inline PATs must be delivered to the
265
+ selected compute provider and retained on the workspace machine for runtime Git operations, so
266
+ provider infrastructure and processes running in that workspace may be able to access them. Prefer
267
+ `gitIntegrationId` for durable managed workspaces and use narrowly scoped, short-lived PATs when
268
+ inline credentials are necessary.
269
+
190
270
  The SDK deliberately exposes two clients. `createGittermClient()` uses a user API token and
191
271
  can manage the user's workspaces. `createGittermWorkspaceClient()` uses the scoped identity
192
272
  injected into a GitTerm workspace and can inspect only that workspace and its ports:
@@ -237,7 +317,10 @@ Override only the placement decisions your integration cares about:
237
317
  await client.workspaces.create({
238
318
  repo: "https://github.com/acme/product",
239
319
  agent: "opencode",
240
- setupCommands: ["npm install", "npm run generate"],
320
+ setup: {
321
+ beforeAgent: ["npm install"],
322
+ afterAgent: ["npm run generate"],
323
+ },
241
324
  opencode: {
242
325
  skills: [
243
326
  {
@@ -259,12 +342,38 @@ Follow the repository's release-demo workflow.`,
259
342
  });
260
343
  ```
261
344
 
262
- Setup commands run in order from the checked-out repository after the agent server is
263
- ready. They do not delay workspace creation or stop the agent if they fail. Provider and
264
- agent defaults configured by an administrator run first. Use
265
- `client.workspaces.setupStatus(workspaceId)` or `waitForSetup(workspaceId)` to inspect them.
266
- GitTerm persists the reported state and bounded log; a recovery copy also lives in the
267
- repository's git-excluded `.gitterm/setup/` directory.
345
+ Setup commands run in order from the checked-out repository. `beforeAgent` blocks agent
346
+ startup; when it fails, `create()` rejects with the tail of its log. `afterAgent` starts
347
+ after the agent is reachable and reports status independently. Provider and agent defaults
348
+ configured by an administrator run first. Use `client.workspaces.setupStatus(workspaceId)`
349
+ or `waitForSetup(workspaceId)` to inspect the `afterAgent` phase. GitTerm persists bounded
350
+ logs and a recovery copy in the repository's git-excluded `.gitterm/setup/` directory.
351
+ Setup commands can reference the checkout with `$WORKSPACE_REPO_DIR`, which is the same on
352
+ every provider even though the underlying path differs.
353
+
354
+ Secret files are created relative to the repository with restrictive permissions and are
355
+ added to `.git/info/exclude` so the agent cannot commit them. GitTerm does not retain their
356
+ contents; to rotate a secret, recreate the workspace. Like model credentials, they are
357
+ delivered to the sandbox through its launch environment, so anyone who can read the
358
+ provider's task or container definition can read them:
359
+
360
+ ```ts
361
+ await client.workspaces.create({
362
+ repo: "https://github.com/acme/product",
363
+ secretFiles: [
364
+ {
365
+ path: ".secrets/gcp.json",
366
+ content: process.env.GCP_SERVICE_ACCOUNT_JSON!,
367
+ mode: "0600",
368
+ },
369
+ ],
370
+ setup: {
371
+ beforeAgent: [
372
+ 'gcloud auth activate-service-account --key-file "$WORKSPACE_REPO_DIR/.secrets/gcp.json"',
373
+ ],
374
+ },
375
+ });
376
+ ```
268
377
 
269
378
  `provider` is a discriminated union, so TypeScript only offers `region` for providers
270
379
  where GitTerm supports caller-selected placement. Machine keys are configured by admins
@@ -286,7 +395,7 @@ it does not claim that a pull request, upload, or other product outcome succeede
286
395
  ```ts
287
396
  const { workspace } = await client.workspaces.create({
288
397
  repo: "https://github.com/acme/product",
289
- setupCommands: ["npm install", "npm run db:seed"],
398
+ setup: { afterAgent: ["npm install", "npm run db:seed"] },
290
399
  });
291
400
 
292
401
  const run = await client.runs.create({
@@ -1,4 +1,4 @@
1
- import type { DirectProviderAdapter, DirectProviderConfig, DirectAuthAttempt, DirectAuthAttemptStatus, DirectAuthIntegration, DirectAuthWaitOptions, DirectModelCredential, DirectRun, DirectRunCreateInput, DirectRunMessage, DirectRunWaitOptions, DirectWorkspace, DirectWorkspaceCreateInput } from "./types.js";
1
+ import type { DirectProviderAdapter, DirectProviderConfig, DirectAuthAttempt, DirectAuthAttemptStatus, DirectAuthIntegration, DirectAuthWaitOptions, DirectModelCredential, DirectRun, DirectRunCreateInput, DirectRunMessage, DirectRunWaitOptions, DirectWorkspace, DirectWorkspaceCreateInput, DirectWorkspaceSetupStatus, DirectWorkspaceSetupWaitOptions } from "./types.js";
2
2
  export type DirectGittermClientOptions = {
3
3
  provider: DirectProviderAdapter | DirectProviderConfig;
4
4
  };
@@ -36,6 +36,8 @@ export declare function createDirectGittermClient(options: DirectGittermClientOp
36
36
  resume(workspace: DirectWorkspace): Promise<DirectWorkspace>;
37
37
  terminate(workspace: DirectWorkspace): Promise<DirectWorkspace>;
38
38
  keepAlive(workspace: DirectWorkspace, timeoutMs: number): Promise<void>;
39
+ setupStatus: (workspace: DirectWorkspace) => Promise<DirectWorkspaceSetupStatus>;
40
+ waitForSetup(workspace: DirectWorkspace, wait?: DirectWorkspaceSetupWaitOptions): Promise<DirectWorkspaceSetupStatus>;
39
41
  };
40
42
  runs: {
41
43
  create(input: DirectRunCreateInput): Promise<DirectRun>;
@@ -5,4 +5,4 @@ export { createDaytonaDirectProvider } from "./daytona.js";
5
5
  export { createExeDevDirectProvider } from "./exedev.js";
6
6
  export { createRailwayDirectProvider } from "./railway.js";
7
7
  export { createVercelDirectProvider } from "./vercel.js";
8
- export type { AsciiDirectProviderConfig, DirectAuthAttempt, DirectAuthAttemptStatus, DirectAuthIntegration, DirectAuthMethod, DirectAuthPrompt, DirectAuthWaitOptions, DirectApiModelCredential, DaytonaDirectProviderConfig, DirectModelCredential, DirectOAuthModelCredential, DirectProviderAdapter, DirectProviderCapabilities, DirectProviderConfig, DirectProviderWorkspaceInput, DirectRun, DirectRunCreateInput, DirectRunMessage, DirectRunWaitOptions, DirectWorkspace, DirectWorkspaceCreateInput, DirectWorkspaceLifecycle, DirectWorkspaceRuntime, E2BDirectProviderConfig, ExeDevDirectProviderConfig, RailwayDirectProviderConfig, VercelDirectProviderConfig, } from "./types.js";
8
+ export type { AsciiDirectProviderConfig, DirectAuthAttempt, DirectAuthAttemptStatus, DirectAuthIntegration, DirectAuthMethod, DirectAuthPrompt, DirectAuthWaitOptions, DirectApiModelCredential, DaytonaDirectProviderConfig, DirectModelCredential, DirectOAuthModelCredential, DirectProviderAdapter, DirectProviderCapabilities, DirectProviderConfig, DirectProviderWorkspaceInput, DirectRun, DirectRunCreateInput, DirectRunMessage, DirectRunWaitOptions, DirectSecretFile, DirectWorkspace, DirectWorkspaceCreateInput, DirectWorkspaceLifecycle, DirectWorkspaceRuntime, DirectWorkspaceSetup, DirectWorkspaceSetupStatus, DirectWorkspaceSetupWaitOptions, E2BDirectProviderConfig, ExeDevDirectProviderConfig, RailwayDirectProviderConfig, VercelDirectProviderConfig, } from "./types.js";
@@ -16,6 +16,8 @@ var DIRECT_E2B_TEMPLATES = {
16
16
  standard: "gitterm-opencode-server",
17
17
  large: "gitterm-opencode-server-lg"
18
18
  };
19
+ var DIRECT_GITTERM_INSTRUCTIONS = "You are running in a direct Gitterm workspace. Follow the user's instructions and verify outcomes before reporting success.";
20
+ var MAX_ADDITIONAL_AGENT_INSTRUCTIONS = 50000;
19
21
  function resolveDirectImage(image) {
20
22
  return image?.trim() || DIRECT_OPENCODE_SERVER_IMAGE;
21
23
  }
@@ -46,6 +48,28 @@ function directModelAuth(credential) {
46
48
  function base64(value) {
47
49
  return Buffer.from(value).toString("base64");
48
50
  }
51
+ function validateDirectFilePath(path) {
52
+ if (path.includes("\x00") || !path.startsWith("/") && !path.startsWith("~/") || path.split("/").some((part) => part === ".." || part === ".") || path === "/" || path === "~/") {
53
+ throw new Error(`Invalid secret file path: ${path}`);
54
+ }
55
+ return path;
56
+ }
57
+ function validateDirectFileMode(mode = 384) {
58
+ if (!Number.isInteger(mode) || mode < 0 || mode > 511) {
59
+ throw new Error("Secret file mode must be an integer between 0000 and 0777");
60
+ }
61
+ return mode;
62
+ }
63
+ function buildDirectGittermInstructions(additional) {
64
+ const trimmed = additional?.trim();
65
+ if (trimmed && trimmed.length > MAX_ADDITIONAL_AGENT_INSTRUCTIONS) {
66
+ throw new Error("additionalAgentInstructions is too large");
67
+ }
68
+ return trimmed ? `${DIRECT_GITTERM_INSTRUCTIONS}
69
+
70
+ ${trimmed}
71
+ ` : DIRECT_GITTERM_INSTRUCTIONS;
72
+ }
49
73
  function validateName(value, kind) {
50
74
  if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(value) || value === "." || value === "..") {
51
75
  throw new Error(`Invalid ${kind}: ${value}`);
@@ -89,10 +113,15 @@ function buildDirectProvisioningPlan(input) {
89
113
  const environmentVariables = { ...input.environmentVariables };
90
114
  delete environmentVariables.OPENCODE_SERVER_USERNAME;
91
115
  delete environmentVariables.GITTERM_DIRECT_PROVIDER;
116
+ const userPermission = input.opencode?.config?.permission && typeof input.opencode.config.permission === "object" && !Array.isArray(input.opencode.config.permission) ? input.opencode.config.permission : {};
92
117
  const config = {
93
118
  $schema: "https://opencode.ai/config.json",
94
119
  ...input.opencode?.config,
95
120
  username: "Gitterm direct",
121
+ permission: {
122
+ external_directory: "allow",
123
+ ...userPermission
124
+ },
96
125
  ...plugins.length ? { plugin: plugins } : {}
97
126
  };
98
127
  const files = [
@@ -106,13 +135,21 @@ function buildDirectProvisioningPlan(input) {
106
135
  },
107
136
  {
108
137
  path: "~/.config/opencode/AGENTS.md",
109
- contentBase64: base64("You are running in a direct Gitterm workspace. Follow the user's instructions and verify outcomes before reporting success.")
138
+ contentBase64: base64(buildDirectGittermInstructions(input.additionalAgentInstructions))
110
139
  },
111
140
  ...(input.opencode?.skills ?? []).map((skill) => ({
112
141
  path: `~/.config/opencode/skills/${validateName(skill.name, "skill name")}/SKILL.md`,
113
142
  contentBase64: base64(skill.content)
143
+ })),
144
+ ...(input.secretFiles ?? []).map((file) => ({
145
+ path: validateDirectFilePath(file.path),
146
+ contentBase64: base64(file.content),
147
+ mode: validateDirectFileMode(file.mode)
114
148
  }))
115
149
  ];
150
+ const duplicatePath = files.find((file, index) => files.findIndex((candidate) => candidate.path === file.path) !== index);
151
+ if (duplicatePath)
152
+ throw new Error(`Duplicate provisioned file path: ${duplicatePath.path}`);
116
153
  return {
117
154
  workspaceId: input.id,
118
155
  lifecycle: input.lifecycle,
@@ -134,11 +171,18 @@ function buildDirectProvisioningPlan(input) {
134
171
  command: DIRECT_OPENCODE_COMMAND,
135
172
  port: DIRECT_OPENCODE_PORT
136
173
  },
137
- setupCommands: input.setupCommands ?? []
174
+ setup: {
175
+ beforeAgent: input.setup?.beforeAgent ?? [],
176
+ afterAgent: input.setup?.afterAgent ?? []
177
+ }
138
178
  };
139
179
  }
140
180
  function railwayContainerEnvironment(plan) {
141
181
  const repository = plan.repository;
182
+ const beforeAgent = [
183
+ ...plan.agent.files.flatMap((file) => file.mode == null ? [] : [`chmod ${file.mode.toString(8)} ${shellPath(file.path)}`]),
184
+ ...plan.setup.beforeAgent
185
+ ];
142
186
  return {
143
187
  ...plan.agent.environmentVariables,
144
188
  ...repository ? {
@@ -153,7 +197,7 @@ function railwayContainerEnvironment(plan) {
153
197
  } : {}
154
198
  } : {},
155
199
  AGENT_FILES_BASE64: base64(JSON.stringify(plan.agent.files)),
156
- ...plan.setupCommands.length ? { WORKSPACE_SETUP_COMMAND_BASE64: base64(setupCommandScript(plan.setupCommands)) } : {},
200
+ ...beforeAgent.length ? { WORKSPACE_SETUP_COMMAND_BASE64: base64(setupCommandScript(beforeAgent)) } : {},
157
201
  GITTERM_DIRECT_PROVIDER: "railway"
158
202
  };
159
203
  }
@@ -164,6 +208,9 @@ function setupCommandScript(commands) {
164
208
  function shellQuote(value) {
165
209
  return `'${value.replaceAll("'", `'"'"'`)}'`;
166
210
  }
211
+ function shellPath(path) {
212
+ return path.startsWith("~/") ? `"$HOME"/${shellQuote(path.slice(2))}` : shellQuote(path);
213
+ }
167
214
  function cloneRepositoryScript(repository, directory) {
168
215
  const ref = repository.checkoutRef ?? repository.branch;
169
216
  const commands = [
@@ -333,9 +380,12 @@ function createAsciiDirectProvider(config) {
333
380
  boxId: handle.boxId,
334
381
  fileWriteRequest: { path, content: file.contentBase64, encoding: "base64" }
335
382
  });
383
+ if (file.mode != null) {
384
+ await runCommand(handle.boxId, `chmod ${file.mode.toString(8)} ${shellQuote(`${HOME}/${path}`)}`);
385
+ }
336
386
  }
337
- if (plan.setupCommands.length) {
338
- await runCommand(handle.boxId, setupCommandScript(plan.setupCommands), directory, 600);
387
+ if (plan.setup.beforeAgent.length) {
388
+ await runCommand(handle.boxId, setupCommandScript(plan.setup.beforeAgent), directory, 600);
339
389
  }
340
390
  await startRuntime(handle);
341
391
  const runtime = {
@@ -499,10 +549,10 @@ function createDaytonaDirectProvider(config) {
499
549
  for (const file of plan.agent.files) {
500
550
  const target = file.path.replace(/^~/, home);
501
551
  const parent = target.slice(0, target.lastIndexOf("/"));
502
- await execute(sandbox, `mkdir -p ${shellQuote(parent)} && printf %s ${shellQuote(file.contentBase64)} | base64 -d > ${shellQuote(target)}`);
552
+ await execute(sandbox, `mkdir -p ${shellQuote(parent)} && printf %s ${shellQuote(file.contentBase64)} | base64 -d > ${shellQuote(target)}${file.mode == null ? "" : ` && chmod ${file.mode.toString(8)} ${shellQuote(target)}`}`);
503
553
  }
504
- if (plan.setupCommands.length) {
505
- await execute(sandbox, setupCommandScript(plan.setupCommands), directory);
554
+ if (plan.setup.beforeAgent.length) {
555
+ await execute(sandbox, setupCommandScript(plan.setup.beforeAgent), directory);
506
556
  }
507
557
  await startAgent(sandbox, handle);
508
558
  const runtime = await runtimeFor(sandbox, handle, input.password);
@@ -625,10 +675,12 @@ function createE2BDirectProvider(config) {
625
675
  for (const file of plan.agent.files) {
626
676
  const path = file.path.replace(/^~/, "/home/user");
627
677
  const parent = path.slice(0, path.lastIndexOf("/"));
628
- await sandbox.commands.run(`mkdir -p ${shellQuote(parent)} && printf %s ${shellQuote(file.contentBase64)} | base64 -d > ${shellQuote(path)}`);
678
+ await sandbox.commands.run(`mkdir -p ${shellQuote(parent)} && printf %s ${shellQuote(file.contentBase64)} | base64 -d > ${shellQuote(path)}${file.mode == null ? "" : ` && chmod ${file.mode.toString(8)} ${shellQuote(path)}`}`);
629
679
  }
630
- if (plan.setupCommands.length) {
631
- await sandbox.commands.run(setupCommandScript(plan.setupCommands), { cwd: directory });
680
+ if (plan.setup.beforeAgent.length) {
681
+ await sandbox.commands.run(setupCommandScript(plan.setup.beforeAgent), {
682
+ cwd: directory
683
+ });
632
684
  }
633
685
  await sandbox.commands.run(plan.agent.command, {
634
686
  cwd: directory,
@@ -697,7 +749,7 @@ function serializeHandle2(handle) {
697
749
  function parseHandle3(value) {
698
750
  try {
699
751
  const handle = JSON.parse(value);
700
- if (!handle.vmName || !handle.repoDir || !handle.serve?.command || !handle.serve.port) {
752
+ if (!handle.vmName || !handle.repoDir || !handle.serve?.command || !handle.serve.port || !handle.pidFile || typeof handle.owned !== "boolean") {
701
753
  throw new Error("missing required fields");
702
754
  }
703
755
  return handle;
@@ -748,8 +800,9 @@ function createExeDevDirectProvider(config) {
748
800
  }
749
801
  const runVmCommand = (handle, command) => execute(`ssh ${handle.vmName} -- bash -lc ${shellQuote(command)}`);
750
802
  async function startRuntime(handle) {
751
- await runVmCommand(handle, `cd ${shellQuote(handle.repoDir)} && nohup setsid bash -lc ${shellQuote(handle.serve.command)} > /tmp/opencode-server.log 2>&1 </dev/null &`);
803
+ await runVmCommand(handle, `cd ${shellQuote(handle.repoDir)} && nohup setsid bash -lc ${shellQuote(handle.serve.command)} > /tmp/opencode-server.log 2>&1 </dev/null & printf %s "$!" > ${shellQuote(handle.pidFile)}`);
752
804
  }
805
+ const stopRuntime = (handle) => runVmCommand(handle, `if [ -f ${shellQuote(handle.pidFile)} ]; then kill "$(cat ${shellQuote(handle.pidFile)})" 2>/dev/null || true; rm -f ${shellQuote(handle.pidFile)}; fi`);
753
806
  async function accessToken(vmName) {
754
807
  const token = findToken(await execute(`ssh-key generate-api-key --vm=${vmName} --label=gitterm-direct --exp=never`));
755
808
  if (!token)
@@ -787,11 +840,19 @@ function createExeDevDirectProvider(config) {
787
840
  async create(input) {
788
841
  const plan = input.provisioning;
789
842
  const vmSuffix = input.id.replace(/[^a-zA-Z0-9]/g, "").slice(0, 20).toLowerCase();
790
- const vmName = `gitterm-${vmSuffix}`;
843
+ const attachedVmName = input.exedev?.existingVmName.trim();
844
+ if (input.exedev && !attachedVmName)
845
+ throw new Error("exe.dev existingVmName is required");
846
+ if (attachedVmName && !/^[a-zA-Z0-9][a-zA-Z0-9-]*$/.test(attachedVmName)) {
847
+ throw new Error("Invalid exe.dev existingVmName");
848
+ }
849
+ const vmName = attachedVmName ?? `gitterm-${vmSuffix}`;
791
850
  const handle = {
792
851
  vmName,
793
852
  repoDir: `${HOME2}/${plan.repository?.name ?? "workspace"}`,
794
- serve: { command: plan.agent.command, port: plan.agent.port }
853
+ serve: { command: plan.agent.command, port: plan.agent.port },
854
+ pidFile: `/tmp/gitterm-${vmSuffix}.pid`,
855
+ owned: !attachedVmName
795
856
  };
796
857
  const createArgs = [
797
858
  `new --name=${vmName}`,
@@ -803,7 +864,8 @@ function createExeDevDirectProvider(config) {
803
864
  config.disk ? `--disk=${shellQuote(config.disk)}` : "",
804
865
  ...Object.entries(plan.agent.environmentVariables).map(([key, value]) => `--env=${shellQuote(`${key}=${value}`)}`)
805
866
  ].filter(Boolean).join(" ");
806
- await execute(createArgs);
867
+ if (handle.owned)
868
+ await execute(createArgs);
807
869
  try {
808
870
  await waitUntilRunning(vmName);
809
871
  await runVmCommand(handle, `mkdir -p ${shellQuote(handle.repoDir)}`);
@@ -815,10 +877,10 @@ function createExeDevDirectProvider(config) {
815
877
  }
816
878
  for (const file of plan.agent.files) {
817
879
  const path = file.path.startsWith("~/") ? `${HOME2}/${file.path.slice(2)}` : file.path;
818
- await runVmCommand(handle, `mkdir -p ${shellQuote(path.slice(0, path.lastIndexOf("/")))} && printf %s ${shellQuote(file.contentBase64)} | base64 -d > ${shellQuote(path)}`);
880
+ await runVmCommand(handle, `mkdir -p ${shellQuote(path.slice(0, path.lastIndexOf("/")))} && printf %s ${shellQuote(file.contentBase64)} | base64 -d > ${shellQuote(path)}${file.mode == null ? "" : ` && chmod ${file.mode.toString(8)} ${shellQuote(path)}`}`);
819
881
  }
820
- if (plan.setupCommands.length) {
821
- await runVmCommand(handle, `cd ${shellQuote(handle.repoDir)} && ${setupCommandScript(plan.setupCommands)}`);
882
+ if (plan.setup.beforeAgent.length) {
883
+ await runVmCommand(handle, `cd ${shellQuote(handle.repoDir)} && ${setupCommandScript(plan.setup.beforeAgent)}`);
822
884
  }
823
885
  await startRuntime(handle);
824
886
  await execute(`share port ${vmName} ${handle.serve.port}`);
@@ -827,9 +889,15 @@ function createExeDevDirectProvider(config) {
827
889
  await waitForDirectRuntime(directRuntime);
828
890
  return { externalId: serializeHandle2(handle), runtime: directRuntime };
829
891
  } catch (error) {
830
- await execute(`rm ${vmName}`).catch(() => {
831
- return;
832
- });
892
+ if (handle.owned) {
893
+ await execute(`rm ${vmName}`).catch(() => {
894
+ return;
895
+ });
896
+ } else {
897
+ await stopRuntime(handle).catch(() => {
898
+ return;
899
+ });
900
+ }
833
901
  throw error;
834
902
  }
835
903
  },
@@ -859,7 +927,12 @@ function createExeDevDirectProvider(config) {
859
927
  return directRuntime;
860
928
  },
861
929
  async terminate(workspace) {
862
- await execute(`rm ${parseHandle3(workspace.externalId).vmName}`);
930
+ const handle = parseHandle3(workspace.externalId);
931
+ if (handle.owned) {
932
+ await execute(`rm ${handle.vmName}`);
933
+ } else {
934
+ await stopRuntime(handle);
935
+ }
863
936
  }
864
937
  };
865
938
  }
@@ -908,6 +981,7 @@ var SERVICE_DEPLOY = `
908
981
  `;
909
982
  var LATEST_DEPLOYMENT = `
910
983
  query DirectLatestDeployment($environmentId: String!, $serviceId: String!) {
984
+ service(id: $serviceId) { id deletedAt }
911
985
  serviceInstance(environmentId: $environmentId, serviceId: $serviceId) {
912
986
  latestDeployment { id status }
913
987
  }
@@ -1000,10 +1074,31 @@ function createRailwayDirectProvider(config) {
1000
1074
  throw new Error("Railway GraphQL response did not include data");
1001
1075
  return result.data;
1002
1076
  };
1003
- const latestDeployment = async (serviceId) => {
1004
- const result = await request(LATEST_DEPLOYMENT, { environmentId: config.environmentId, serviceId });
1005
- return result.serviceInstance?.latestDeployment ?? undefined;
1077
+ const missingService = (error) => error instanceof Error && /not found|does not exist|deleted|no service/i.test(error.message);
1078
+ const serviceSnapshot = async (serviceId) => {
1079
+ const response = await fetch(apiUrl, {
1080
+ method: "POST",
1081
+ headers: {
1082
+ "Content-Type": "application/json",
1083
+ Authorization: `Bearer ${config.apiToken}`
1084
+ },
1085
+ body: JSON.stringify({
1086
+ query: LATEST_DEPLOYMENT,
1087
+ variables: { environmentId: config.environmentId, serviceId }
1088
+ })
1089
+ });
1090
+ if (!response.ok) {
1091
+ throw new Error(`Railway API request failed (${response.status} ${response.statusText})`);
1092
+ }
1093
+ const result = await response.json();
1094
+ if (result.data)
1095
+ return result.data;
1096
+ if (result.errors?.length) {
1097
+ throw new Error(`Railway GraphQL error: ${result.errors.map((error) => error.message).join(", ")}`);
1098
+ }
1099
+ throw new Error("Railway GraphQL response did not include data");
1006
1100
  };
1101
+ const latestDeployment = async (serviceId) => (await serviceSnapshot(serviceId)).serviceInstance?.latestDeployment ?? undefined;
1007
1102
  const waitForDeployment = async (serviceId, previousDeploymentId) => {
1008
1103
  const deadline = Date.now() + DEPLOYMENT_TIMEOUT_MS;
1009
1104
  while (Date.now() < deadline) {
@@ -1149,12 +1244,16 @@ function createRailwayDirectProvider(config) {
1149
1244
  async status(workspace) {
1150
1245
  const handle = parseHandle4(workspace.externalId);
1151
1246
  try {
1152
- const deployment = await latestDeployment(handle.serviceId);
1153
- return deployment ? workspaceStatus(deployment.status) : "terminated";
1247
+ const snapshot = await serviceSnapshot(handle.serviceId);
1248
+ if (!snapshot.service || snapshot.service.deletedAt)
1249
+ return "terminated";
1250
+ const deployment = snapshot.serviceInstance?.latestDeployment;
1251
+ if (!deployment)
1252
+ return "paused";
1253
+ return workspaceStatus(deployment.status);
1154
1254
  } catch (error) {
1155
- if (error instanceof Error && /not found|does not exist/i.test(error.message)) {
1255
+ if (missingService(error))
1156
1256
  return "terminated";
1157
- }
1158
1257
  throw error;
1159
1258
  }
1160
1259
  },
@@ -1307,11 +1406,15 @@ esac
1307
1406
  const parent = target.slice(0, target.lastIndexOf("/"));
1308
1407
  await run(sandbox, `mkdir -p ${shellQuote(parent)}`);
1309
1408
  await sandbox.writeFiles([
1310
- { path: target, content: Buffer.from(file.contentBase64, "base64") }
1409
+ {
1410
+ path: target,
1411
+ content: Buffer.from(file.contentBase64, "base64"),
1412
+ ...file.mode == null ? {} : { mode: file.mode }
1413
+ }
1311
1414
  ]);
1312
1415
  }
1313
- if (plan.setupCommands.length) {
1314
- await run(sandbox, setupCommandScript(plan.setupCommands), handle.directory);
1416
+ if (plan.setup.beforeAgent.length) {
1417
+ await run(sandbox, setupCommandScript(plan.setup.beforeAgent), handle.directory);
1315
1418
  }
1316
1419
  await startAgent(sandbox, handle);
1317
1420
  const runtime = {
@@ -1383,6 +1486,7 @@ esac
1383
1486
  }
1384
1487
 
1385
1488
  // src/direct/client.ts
1489
+ var SETUP_DIR = ".gitterm/setup";
1386
1490
  function resolveProvider(provider) {
1387
1491
  if ("create" in provider)
1388
1492
  return provider;
@@ -1480,6 +1584,38 @@ function modelParts(model) {
1480
1584
  }
1481
1585
  return { providerID: model.slice(0, separator), modelID: model.slice(separator + 1) };
1482
1586
  }
1587
+ function validateEnvironmentVariables(values) {
1588
+ for (const key of Object.keys(values)) {
1589
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key)) {
1590
+ throw new Error(`Invalid environment variable name: ${key}`);
1591
+ }
1592
+ if (key === "OPENCODE_SERVER_PASSWORD" || key === "OPENCODE_SERVER_USERNAME" || key === "GITTERM_DIRECT_PROVIDER") {
1593
+ throw new Error(`Environment variable ${key} is managed by Gitterm`);
1594
+ }
1595
+ }
1596
+ return values;
1597
+ }
1598
+ function setupRunner(commands) {
1599
+ const body = Buffer.from(setupCommandScript(commands)).toString("base64");
1600
+ return [
1601
+ "set -u",
1602
+ `SETUP_DIR=${shellQuote(SETUP_DIR)}`,
1603
+ 'mkdir -p "$SETUP_DIR"',
1604
+ 'if [ -d .git/info ]; then grep -qxF "/.gitterm/" .git/info/exclude 2>/dev/null || printf "/.gitterm/\\n" >> .git/info/exclude; fi',
1605
+ 'printf "waiting\\n" > "$SETUP_DIR/state"',
1606
+ `printf %s ${shellQuote(body)} | base64 -d > "$SETUP_DIR/script.sh"`,
1607
+ 'chmod 700 "$SETUP_DIR/script.sh"',
1608
+ 'date -u +%Y-%m-%dT%H:%M:%SZ > "$SETUP_DIR/started-at"',
1609
+ 'printf "running\\n" > "$SETUP_DIR/state"',
1610
+ 'bash -e "$SETUP_DIR/script.sh" > "$SETUP_DIR/setup.log" 2>&1',
1611
+ "code=$?",
1612
+ 'printf "%s\\n" "$code" > "$SETUP_DIR/exit-code"',
1613
+ 'date -u +%Y-%m-%dT%H:%M:%SZ > "$SETUP_DIR/finished-at"',
1614
+ 'if [ "$code" -eq 0 ]; then printf "succeeded\\n" > "$SETUP_DIR/state"; else printf "failed\\n" > "$SETUP_DIR/state"; fi',
1615
+ "exit $code"
1616
+ ].join(`
1617
+ `);
1618
+ }
1483
1619
  function createDirectGittermClient(options) {
1484
1620
  const provider = resolveProvider(options.provider);
1485
1621
  function assertWorkspace(workspace) {
@@ -1537,6 +1673,63 @@ function createDirectGittermClient(options) {
1537
1673
  finalText: finalText || null
1538
1674
  };
1539
1675
  }
1676
+ async function startPty(workspace, command, title) {
1677
+ const result = await authClient(workspace).pty.create({
1678
+ command: "bash",
1679
+ args: ["-lc", command],
1680
+ cwd: workspace.runtime.directory,
1681
+ directory: workspace.runtime.directory,
1682
+ title
1683
+ });
1684
+ if (result.error || !result.data)
1685
+ throw new Error(errorMessage(result.error));
1686
+ return result.data;
1687
+ }
1688
+ async function setupFile(workspace, name) {
1689
+ const result = await authClient(workspace).file.read({
1690
+ directory: workspace.runtime.directory,
1691
+ path: `${SETUP_DIR}/${name}`
1692
+ });
1693
+ if (result.error || !result.data || result.data.type !== "text")
1694
+ return null;
1695
+ return result.data.content.trim();
1696
+ }
1697
+ async function getSetupStatus(workspace) {
1698
+ assertWorkspace(workspace);
1699
+ if (workspace.setup === "not_requested") {
1700
+ return {
1701
+ status: "not_requested",
1702
+ exitCode: null,
1703
+ startedAt: null,
1704
+ finishedAt: null,
1705
+ log: null
1706
+ };
1707
+ }
1708
+ if (workspace.setup === "before_agent_complete") {
1709
+ return {
1710
+ status: "succeeded",
1711
+ exitCode: 0,
1712
+ startedAt: null,
1713
+ finishedAt: null,
1714
+ log: null
1715
+ };
1716
+ }
1717
+ const [state, exitCode, startedAt, finishedAt, log] = await Promise.all([
1718
+ setupFile(workspace, "state"),
1719
+ setupFile(workspace, "exit-code"),
1720
+ setupFile(workspace, "started-at"),
1721
+ setupFile(workspace, "finished-at"),
1722
+ setupFile(workspace, "setup.log")
1723
+ ]);
1724
+ const status = ["waiting", "running", "succeeded", "failed"].includes(state ?? "") ? state : "waiting";
1725
+ return {
1726
+ status,
1727
+ exitCode: exitCode != null && Number.isInteger(Number(exitCode)) ? Number(exitCode) : null,
1728
+ startedAt,
1729
+ finishedAt,
1730
+ log: log?.slice(-50000) ?? null
1731
+ };
1732
+ }
1540
1733
  return {
1541
1734
  provider: { name: provider.name, capabilities: provider.capabilities },
1542
1735
  auth: {
@@ -1654,17 +1847,30 @@ function createDirectGittermClient(options) {
1654
1847
  }
1655
1848
  const id = input.id ?? randomUUID();
1656
1849
  const password = randomUUID();
1850
+ validateEnvironmentVariables(input.environmentVariables ?? {});
1657
1851
  const provisioning = buildDirectProvisioningPlan({ ...input, id, lifecycle, password });
1658
1852
  const created = await provider.create({ ...input, id, lifecycle, password, provisioning });
1659
- return {
1853
+ const workspace = {
1660
1854
  id,
1661
1855
  provider: provider.name,
1662
1856
  externalId: created.externalId,
1663
1857
  status: "running",
1664
1858
  lifecycle,
1665
1859
  runtime: created.runtime,
1860
+ setup: provisioning.setup.afterAgent.length ? "after_agent" : provisioning.setup.beforeAgent.length ? "before_agent_complete" : "not_requested",
1666
1861
  createdAt: new Date().toISOString()
1667
1862
  };
1863
+ if (provisioning.setup.afterAgent.length) {
1864
+ try {
1865
+ await startPty(workspace, setupRunner(provisioning.setup.afterAgent), "Gitterm setup");
1866
+ } catch (error) {
1867
+ await provider.terminate(workspace).catch(() => {
1868
+ return;
1869
+ });
1870
+ throw error;
1871
+ }
1872
+ }
1873
+ return workspace;
1668
1874
  },
1669
1875
  async status(workspace) {
1670
1876
  assertWorkspace(workspace);
@@ -1701,6 +1907,24 @@ function createDirectGittermClient(options) {
1701
1907
  if (!provider.keepAlive)
1702
1908
  throw new Error(`${provider.name} does not support keep-alive`);
1703
1909
  await provider.keepAlive(workspace, timeoutMs);
1910
+ },
1911
+ setupStatus: getSetupStatus,
1912
+ async waitForSetup(workspace, wait = {}) {
1913
+ const { timeoutMs, pollIntervalMs } = pollTiming(wait, 10 * 60000);
1914
+ const deadline = Date.now() + timeoutMs;
1915
+ while (true) {
1916
+ const status = await getSetupStatus(workspace);
1917
+ if (status.status === "not_requested" || status.status === "succeeded")
1918
+ return status;
1919
+ if (status.status === "failed") {
1920
+ throw new Error(`Workspace setup failed${status.exitCode == null ? "" : ` with exit code ${status.exitCode}`}${status.log ? `
1921
+ ${status.log}` : ""}`);
1922
+ }
1923
+ if (Date.now() >= deadline) {
1924
+ throw new Error(`Workspace setup timed out after ${timeoutMs}ms`);
1925
+ }
1926
+ await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
1927
+ }
1704
1928
  }
1705
1929
  },
1706
1930
  runs: {
@@ -1792,11 +2016,11 @@ function createDirectGittermClient(options) {
1792
2016
  };
1793
2017
  }
1794
2018
  export {
1795
- createVercelDirectProvider,
1796
- createRailwayDirectProvider,
1797
- createExeDevDirectProvider,
1798
- createE2BDirectProvider,
1799
- createDirectGittermClient,
2019
+ createAsciiDirectProvider,
1800
2020
  createDaytonaDirectProvider,
1801
- createAsciiDirectProvider
2021
+ createDirectGittermClient,
2022
+ createE2BDirectProvider,
2023
+ createExeDevDirectProvider,
2024
+ createRailwayDirectProvider,
2025
+ createVercelDirectProvider
1802
2026
  };
@@ -6,6 +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
10
  export declare function resolveDirectImage(image?: string): string;
10
11
  export declare function directModelAuth(credential: DirectModelCredential): {
11
12
  enterpriseUrl?: string | undefined;
@@ -19,6 +20,9 @@ export declare function directModelAuth(credential: DirectModelCredential): {
19
20
  type: "api";
20
21
  key: string;
21
22
  };
23
+ export declare function validateDirectFilePath(path: string): string;
24
+ export declare function validateDirectFileMode(mode?: number): number;
25
+ export declare function buildDirectGittermInstructions(additional?: string): string;
22
26
  export declare function repositoryName(url: string): string;
23
27
  export declare function buildDirectProvisioningPlan(input: DirectWorkspaceCreateInput & {
24
28
  id: string;
@@ -28,6 +32,7 @@ export declare function buildDirectProvisioningPlan(input: DirectWorkspaceCreate
28
32
  export declare function railwayContainerEnvironment(plan: DirectProvisioningPlan): Record<string, string>;
29
33
  export declare function setupCommandScript(commands: string[]): string;
30
34
  export declare function shellQuote(value: string): string;
35
+ export declare function shellPath(path: string): string;
31
36
  export declare function cloneRepositoryScript(repository: NonNullable<DirectProvisioningPlan["repository"]>, directory: string): string;
32
37
  export declare function pinFloatingDockerImage(image: string): Promise<string>;
33
38
  export declare function basicAuthHeader(password: string): string;
@@ -108,6 +108,17 @@ export type DirectAuthWaitOptions = {
108
108
  timeoutMs?: number;
109
109
  pollIntervalMs?: number;
110
110
  };
111
+ export type DirectSecretFile = {
112
+ /** Absolute path or a path below the workspace user's home (`~/...`). */
113
+ path: string;
114
+ content: string;
115
+ /** Unix permission bits. Defaults to owner read/write (0600). */
116
+ mode?: number;
117
+ };
118
+ export type DirectWorkspaceSetup = {
119
+ beforeAgent?: string[];
120
+ afterAgent?: string[];
121
+ };
111
122
  export type DirectWorkspaceCreateInput = {
112
123
  id?: string;
113
124
  repo?: string;
@@ -121,7 +132,14 @@ export type DirectWorkspaceCreateInput = {
121
132
  lifecycle?: DirectWorkspaceLifecycle;
122
133
  environmentVariables?: Record<string, string>;
123
134
  modelCredentials?: DirectModelCredential[];
124
- setupCommands?: string[];
135
+ setup?: DirectWorkspaceSetup;
136
+ secretFiles?: DirectSecretFile[];
137
+ /** Provider-specific attachment settings. */
138
+ exedev?: {
139
+ existingVmName: string;
140
+ };
141
+ /** Trusted integration context appended to the generated global AGENTS.md. */
142
+ additionalAgentInstructions?: string;
125
143
  opencode?: {
126
144
  config?: Record<string, unknown>;
127
145
  plugins?: string[];
@@ -144,8 +162,20 @@ export type DirectWorkspace = {
144
162
  status: DirectWorkspaceStatus;
145
163
  lifecycle: DirectWorkspaceLifecycle;
146
164
  runtime: DirectWorkspaceRuntime;
165
+ setup: "not_requested" | "before_agent_complete" | "after_agent";
147
166
  createdAt: string;
148
167
  };
168
+ export type DirectWorkspaceSetupStatus = {
169
+ status: "not_requested" | "waiting" | "running" | "succeeded" | "failed";
170
+ exitCode: number | null;
171
+ startedAt: string | null;
172
+ finishedAt: string | null;
173
+ log: string | null;
174
+ };
175
+ export type DirectWorkspaceSetupWaitOptions = {
176
+ timeoutMs?: number;
177
+ pollIntervalMs?: number;
178
+ };
149
179
  export type DirectProviderWorkspaceInput = DirectWorkspaceCreateInput & {
150
180
  id: string;
151
181
  lifecycle: DirectWorkspaceLifecycle;
@@ -155,6 +185,7 @@ export type DirectProviderWorkspaceInput = DirectWorkspaceCreateInput & {
155
185
  export type DirectAgentFile = {
156
186
  path: string;
157
187
  contentBase64: string;
188
+ mode?: number;
158
189
  };
159
190
  export type DirectProvisioningPlan = {
160
191
  workspaceId: string;
@@ -174,7 +205,10 @@ export type DirectProvisioningPlan = {
174
205
  command: string;
175
206
  port: number;
176
207
  };
177
- setupCommands: string[];
208
+ setup: {
209
+ beforeAgent: string[];
210
+ afterAgent: string[];
211
+ };
178
212
  };
179
213
  export interface DirectProviderAdapter {
180
214
  readonly name: string;
package/dist/index.js CHANGED
@@ -236,7 +236,7 @@ async function runWithServer(serverUrl, operation) {
236
236
  cause: error
237
237
  });
238
238
  }
239
- throw new GittermError(code, code === "UNAUTHORIZED" ? "Not logged in or token expired. Run: gitterm login" : error.message, { cause: error });
239
+ throw new GittermError(code, code === "UNAUTHORIZED" ? `Authentication failed: ${error.message}. Check that the API token is valid and has not expired.` : error.message, { cause: error });
240
240
  }
241
241
  throw new GittermError("NETWORK", error instanceof Error ? error.message : "Network request failed", { cause: error });
242
242
  }
@@ -525,16 +525,16 @@ function createGittermWorkspaceClient(options = {}) {
525
525
  };
526
526
  }
527
527
  export {
528
- saveConfig,
529
- loginWithDeviceCode,
530
- loadConfigSync,
531
- loadConfig,
532
- getWorkspaceEnvironment,
533
- getConfigPath,
534
- deleteConfig,
535
- createGittermWorkspaceClient,
536
- createGittermClient,
537
- WorkspaceLifecycleError,
528
+ DEFAULT_GITTERM_SERVER_URL,
538
529
  GittermError,
539
- DEFAULT_GITTERM_SERVER_URL
530
+ WorkspaceLifecycleError,
531
+ createGittermClient,
532
+ createGittermWorkspaceClient,
533
+ deleteConfig,
534
+ getConfigPath,
535
+ getWorkspaceEnvironment,
536
+ loadConfig,
537
+ loadConfigSync,
538
+ loginWithDeviceCode,
539
+ saveConfig
540
540
  };
package/dist/types.d.ts CHANGED
@@ -167,6 +167,11 @@ export type WorkspaceCreateInput = {
167
167
  agent?: AgentKey;
168
168
  /** Provider intent. Defaults to the user's or deployment's preferred provider. */
169
169
  provider?: WorkspaceProviderSelection;
170
+ /** Inline Git credentials for repository validation, cloning, and runtime pull/push. */
171
+ repositoryCredentials?: {
172
+ username?: string;
173
+ token: string;
174
+ };
170
175
  gitIntegrationId?: string;
171
176
  /** Defaults from the selected provider. */
172
177
  persistent?: boolean;
@@ -183,11 +188,22 @@ export type WorkspaceCreateInput = {
183
188
  /** Ephemeral environment variables injected into this workspace only. */
184
189
  environmentVariables?: Record<string, string>;
185
190
  /**
186
- * Ordered commands launched in the repository after the agent server starts.
187
- * They do not block workspace readiness; inspect ~/.gitterm/setup for status
188
- * and logs through workspaces.setupStatus()/waitForSetup().
191
+ * Setup phases run in order. `beforeAgent` blocks agent startup and fails
192
+ * create() when it exits non-zero; `afterAgent` starts after the agent is
193
+ * reachable and is observable with setupStatus()/waitForSetup().
189
194
  */
190
- setupCommands?: string[];
195
+ setup?: {
196
+ beforeAgent?: string[];
197
+ afterAgent?: string[];
198
+ };
199
+ /** Secret files written relative to the repository and excluded from git. Rotate by recreating the workspace. */
200
+ secretFiles?: Array<{
201
+ path: string;
202
+ content: string;
203
+ mode?: "0400" | "0600";
204
+ }>;
205
+ /** Trusted integration context appended to the workspace's global AGENTS.md. */
206
+ additionalAgentInstructions?: string;
191
207
  /** OpenCode capabilities materialized only in this workspace. */
192
208
  opencode?: {
193
209
  skills?: Array<{
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gitterm/sdk",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "files": [
5
5
  "dist"
6
6
  ],