Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh

Commit

Sessions g1t starts on Anthropic work through the g1t CLI, and their Console links open in the key's own Console workspace. The sandbox had no CLI, so the agent reached g1t only through MCP and hand-written git headers, though the work-on-issue skill it follows is written for the CLI (g1t work context, g1t work claim, g1t pr checks --watch). The kickoff now opens with one setup command: install the CLI from the site (into /usr/local/bin, else ~/.local/bin), have git send G1T_TOKEN to the site in a header (the vault swaps the placeholder there), clone the repository, and write the session id to the clone's .git/g1t/session, where the CLI keeps it, so claims and notes are this session's. The system prompt tells the agent to run it first, to use the CLI as the skill does, and MCP for what the CLI has no command for; agents already on accounts take the new prompt at their next session (config_hash).

The Console link was always .../workspaces/default/..., which lands on "Session not found" when the key belongs to another Console workspace, and a session does not say which workspace it is in. An Anthropic AI provider has an optional Console workspace (AiConfig console_workspace, consoleWorkspace in TypeScript): its wrkspc_ id, or any Console address in that workspace, from which the id is taken. Session links use it, else default. Other providers drop it. The AI page asks for it when adding Anthropic and under the provider's Change. API: config.console_workspace on add/update/list AI providers. Docs: the AI guide's Anthropic Console workspace section, and the sessions guide on the setup and the link. No migration (the provider's config is JSON). Deploy integrations, api and web together, and the docs.

Checked:
cargo test -p g1t-integrations --lib (72, new: the Console workspace from an address or an id, Console links by workspace), g1t-contracts and g1t-api (the OpenAPI document regenerated), wasm32 checks of integrations and api, the site's tsc -b --force and tests (934), the docs build.
syntaqxcommitted Parent4fff915Browse files
10 files+151−200/10 viewed
+1−0
525525 "base_url": { "type": "string", "description": "The address of a self-hosted endpoint or an Azure OpenAI resource, over https. An Anthropic-compatible one without /v1; an OpenAI-compatible one up to and including the version." },
526526 "auth_header": { "type": "string", "enum": ["x-api-key", "authorization"], "description": "For an endpoint: send the key as x-api-key, or as authorization: Bearer." },
527527 "model": { "type": "string", "description": "The model the team uses on it; for Azure OpenAI, the deployment." },
528+ "console_workspace": { "type": "string", "description": "For anthropic: the Console workspace the key belongs to (wrkspc_…), or any Console address in it, so links to the sessions it starts open there. Without it, the account's Default workspace." },
528529 },
529530 })
530531 }
+13−1
4747 You can name the model your team uses on each provider. It is optional,
4848 except for Azure OpenAI, where it is the deployment.
4949
50+### Anthropic Console workspace
51+
52+Sessions on issues run on the Anthropic provider's account, and the
53+Anthropic Console shows each one in the Console workspace its key belongs
54+to. A session does not say which workspace that is, so if the key is not
55+in the account's Default workspace, tell g1t: paste any Console address
56+from that workspace (such as
57+`https://platform.claude.com/workspaces/wrkspc_01AbC…/sessions`), or its
58+`wrkspc_` id, into the provider's **Console workspace**. Links to its
59+sessions then open there. Left empty, they open in the Default workspace.
60+
5061 ## Look after a provider
5162
5263 - **Test.** g1t checks a key when it is saved, and again whenever you
5566 provider, until it next works. An endpoint that does not list its
5667 models is reached, but its key cannot be checked this way.
5768 - **Change.** **Change its model, address or key** sets a new model or
58− address, or replaces the key. The old key is gone once the new one is
69+ address, or replaces the key (for Anthropic, its model, Console
70+ workspace or key). The old key is gone once the new one is
5971 saved.
6072 - **Remove.** The **×** on a provider removes it and deletes its key. If
6173 Ask was using it, Ask is turned off until an owner chooses another.
+12−4
276276 under [**AI**](/guides/ai/)), g1t starts a
277277 [Claude Managed Agents](https://platform.claude.com/docs/en/managed-agents/overview)
278278 session on that account, with its key, on the model Ask is set to use
279− (or `claude-opus-5-5`). It runs in a sandbox on Anthropic's side and
280− reaches g1t through g1t's MCP server and git, with a token made for you
279+ (or `claude-opus-5-5`). It runs in a sandbox on Anthropic's side. Its
280+ first step installs the [g1t CLI](/guides/cli/) from g1t, sets git up to
281+ send the token, clones the repository and ties the clone to the session,
282+ so it works the way the work-on-issue skill describes: `g1t work
283+ context`, `g1t work claim`, `g1t pr create`, `g1t pr checks --watch`.
284+ g1t's MCP server is attached too, for anything the CLI has no command
285+ for. Both reach g1t with a token made for you
281286 and that repository only, which lasts the session and is revoked when it
282287 ends. The token is kept in a vault on the Anthropic account: the MCP
283288 server's credential, and `G1T_TOKEN`, which the sandbox holds only as a
293298 The session that starts attaches to the issue.
294299
295300 The answer has the `session` on g1t, its `session_url`, and
296−`provider_session_url`, where the Anthropic Console shows it (its
297−`default` workspace; another workspace's sessions are under its own id).
301+`provider_session_url`, where the Anthropic Console shows it. The Console
302+keeps sessions in the workspace the key belongs to, and a session does not
303+say which that is: if the key is not in the account's Default workspace,
304+set the provider's **Console workspace** under [**AI**](/guides/ai/#anthropic-console-workspace)
305+so the link opens there.
298306 The session is recorded as harness `claude_managed_agents`: its messages,
299307 the tools it called and with what (never what they returned), the skills
300308 it read, what it touched, and each thread as a subagent with its task and
+0−0

Binary or large file; its contents are not shown.

+22−0
8989 <Field label="Model" hint="Optional. The model your team uses on it. g1t lists the key's models when it checks the key.">
9090 <Input name="model" placeholder={look.model} />
9191 </Field>
92+ {provider === "anthropic" && <ConsoleWorkspaceField />}
9293 </>
9394 );
9495 }
96+
97+/**
98+ * The Console workspace an Anthropic key belongs to: links to the sessions
99+ * it starts open there. Sessions don't say which workspace they are in.
100+ */
101+export function ConsoleWorkspaceField({ defaultValue }: { defaultValue?: string }) {
102+ return (
103+ <Field
104+ label="Console workspace"
105+ hint="Optional. Paste any Console address from the workspace the key belongs to, or its wrkspc_ id, so session links open there. Empty means the Default workspace."
106+ >
107+ <Input
108+ name="consoleWorkspace"
109+ defaultValue={defaultValue ?? ""}
110+ placeholder="https://platform.claude.com/workspaces/wrkspc_…"
111+ className="max-w-sm font-mono text-[0.8125rem]"
112+ autoComplete="off"
113+ />
114+ </Field>
115+ );
116+}
+5−2
77 import type { Route } from "./+types/ai";
88 import { page } from "../../lib/meta";
99 import { AiProviderMark } from "../../components/connectors";
10−import { ModelCatalog, ModelProviderFields } from "../../components/model-providers";
10+import { ConsoleWorkspaceField, ModelCatalog, ModelProviderFields } from "../../components/model-providers";
1111 import { AI_PROVIDER_LOOKS, isEndpoint } from "../../lib/ai-providers";
1212 import { ErrorText, Field, Input, SubmitButton, TimeAgo } from "../../components/ui";
1313 import { Card } from "../../components/ui/card";
8080 ...connection.config,
8181 model: text(form, "model"),
8282 ...(form.has("baseUrl") ? { baseUrl: text(form, "baseUrl") } : {}),
83+ ...(form.has("consoleWorkspace") ? { consoleWorkspace: text(form, "consoleWorkspace") } : {}),
8384 },
8485 secret: text(form, "secret"),
8586 });
9495 baseUrl: text(form, "baseUrl"),
9596 authHeader: text(form, "authHeader"),
9697 model: text(form, "model"),
98+ consoleWorkspace: text(form, "consoleWorkspace"),
9799 },
98100 secret: text(form, "secret"),
99101 });
349351 <details className="group mt-3" open={updated || undefined}>
350352 <summary className="inline-flex cursor-pointer list-none items-center gap-1 text-xs text-muted hover:text-fg">
351353 <ChevronRight size={12} className="transition-transform group-open:rotate-90" />
352− Change its model, address or key
354+ {connection.provider === "anthropic" ? "Change its model, Console workspace or key" : "Change its model, address or key"}
353355 </summary>
354356 <Form method="post" className="mt-3 max-w-2xl space-y-3">
355357 <input type="hidden" name="intent" value="provider" />
376378 ))}
377379 </datalist>
378380 )}
381+ {connection.provider === "anthropic" && <ConsoleWorkspaceField defaultValue={connection.config.consoleWorkspace} />}
379382 <Field label="Replace the key" hint="Write-only: g1t never shows it again. Leave it empty to keep the key you have.">
380383 <Input
381384 name="secret"
+5−0
165165 /// The model the team uses on it; for Azure OpenAI, the deployment.
166166 #[serde(default, skip_serializing_if = "Option::is_none")]
167167 pub model: Option<String>,
168+ /// For Anthropic: the Console workspace the key belongs to, as its
169+ /// Console address names it (`wrkspc_…`), so links to the sessions it
170+ /// starts open there. Without it, the account's Default workspace.
171+ #[serde(default, skip_serializing_if = "Option::is_none")]
172+ pub console_workspace: Option<String>,
168173 }
169174
170175 /// One of a workspace's AI providers, as any member sees it. Its key is
+6−0
9898 authHeader?: string;
9999 /** The model the team uses on it; for Azure OpenAI, the deployment. */
100100 model?: string;
101+ /**
102+ * For Anthropic: the Console workspace the key belongs to, as its Console
103+ * address names it (`wrkspc_…`), so links to the sessions it starts open
104+ * there. Without it, the account's Default workspace.
105+ */
106+ consoleWorkspace?: string;
101107 };
102108
103109 /** One of a workspace's AI providers, as any member sees it. */
+47−13
104104 pub fn system_prompt() -> String {
105105 "You work on issues in a g1t repository (g1t is a git forge), for the person who started you, with their own Claude. \
106106 Each session's first message says which repository and issue, what kind of work, and how to reach them.\n\n\
107− - g1t's MCP server (the `g1t` tools) acts as that person on that repository only: read the issue and its work context (`issue` action `work`), \
108− claim it (`issue` action `claim`), see who else is on it (`session` action `list` with the issue), open and update pull requests, read checks and runs.\n\
109− - `G1T_TOKEN` is set for git and the g1t CLI. Use git with it as a header, never in a URL: \
110− `git -c http.extraHeader=\"Authorization: Bearer $G1T_TOKEN\" clone …`, then `git config http.extraHeader \"Authorization: Bearer $G1T_TOKEN\"` in the clone. \
111− Never print it, write it to a file, or put it anywhere else.\n\
112− - Follow the work-on-issue skill. Check get_work and the sessions on the issue before you start, and claim only what you take. \
107+ - Run the setup in your first message before anything else: it installs the g1t CLI, has git send your token to g1t, \
108+ clones the repository and ties the clone to this g1t session.\n\
109+ - Use the g1t CLI from the clone, as the work-on-issue skill does: `g1t work context N`, `g1t work claim N --note …`, \
110+ `g1t pr create --issue N --draft --fill`, `g1t pr checks --watch --fail-fast`, `g1t run logs --failed`. \
111+ Add `--json` when you need fields; `g1t <command> --help` says the rest.\n\
112+ - g1t's MCP server (the `g1t` tools) does the same as that person on that repository only. Use it when the CLI has no command for something.\n\
113+ - `G1T_TOKEN` is set for the CLI and git. In the sandbox it is a placeholder that becomes the token only in a header on the way to g1t, \
114+ never in a URL. Never print it or send it anywhere else.\n\
115+ - Follow the work-on-issue skill. Check the work context and the sessions on the issue before you start, and claim only what you take. \
113116 When work splits into independent parts, you may give a part to a copy of yourself; each is recorded on g1t as a subagent and sees the others.\n\
114117 - Put the trailer `G1t-Session: <the session id in your first message>` on every commit, so g1t ties the commits to this session.\n\
115118 - Work on a branch whose name starts with the issue number, and open a draft pull request early. Never push to the default branch.\n\
213216 body
214217 }
215218
216−/// Where Anthropic's Console shows a session.
217−pub fn console_url(provider_session_id: &str) -> String {
218− format!("https://platform.claude.com/workspaces/default/sessions/{provider_session_id}")
219+/// Where Anthropic's Console shows a session: in the Console workspace the
220+/// key belongs to. Sessions do not say which that is, so the provider's
221+/// settings do; without it, the account's Default workspace.
222+pub fn console_url(console_workspace: Option<&str>, provider_session_id: &str) -> String {
223+ let workspace = console_workspace.filter(|id| !id.is_empty()).unwrap_or("default");
224+ format!("https://platform.claude.com/workspaces/{workspace}/sessions/{provider_session_id}")
219225 }
220226
221227 /// What a session is asked to do.
261267 text.push_str(&format!("\nAlso: {}\n", cut(instruction, MAX_INSTRUCTION_CHARS)));
262268 }
263269 if k.managed {
264− let host = site.trim_start_matches("https://").trim_start_matches("http://");
270+ text.push_str(&format!("\nSet up first, as one command:\n\n```sh\n{}\n```\n", setup_command(site, &repo, &k.repo.name, k.session_id)));
265271 text.push_str(&format!(
266− "\nThe repository: `git -c http.extraHeader=\"Authorization: Bearer $G1T_TOKEN\" clone https://{host}/{repo}.git /workspace/{}`.\n",
272+ "\nIf /usr/local/bin cannot be written, install without G1T_INSTALL_DIR and run ~/.local/bin/g1t. Then work in /workspace/{}.\n",
267273 k.repo.name
268274 ));
269275 }
274280 text
275281 }
276282
283+/// What a sandbox runs before it works: the g1t CLI from the site, git
284+/// sending `G1T_TOKEN` to the site in a header (the vault swaps the
285+/// placeholder there), the clone, and the clone's g1t session where the
286+/// CLI keeps it, so claims and notes are this session's.
287+pub fn setup_command(site: &str, repo: &str, name: &str, session_id: Option<&str>) -> String {
288+ let site = site.trim_end_matches('/');
289+ let mut steps = vec![
290+ format!("curl -fsSL {site}/install | G1T_INSTALL_DIR=/usr/local/bin sh"),
291+ format!("git config --global http.{site}/.extraHeader \"Authorization: Bearer $G1T_TOKEN\""),
292+ format!("git clone {site}/{repo}.git /workspace/{name}"),
293+ ];
294+ if let Some(id) = session_id {
295+ steps.push(format!("mkdir -p /workspace/{name}/.git/g1t && echo {id} > /workspace/{name}/.git/g1t/session"));
296+ }
297+ steps.join(" \\\n && ")
298+}
299+
277300 /// Quotes `text` for a POSIX shell.
278301 fn shell_quote(text: &str) -> String {
279302 format!("'{}'", text.replace('\'', "'\"'\"'"))
954977 session_url: Some(format!("{}/{repo_name}/work/sessions/{}", self.site_url.trim_end_matches('/'), session.id)),
955978 session: Some(session),
956979 provider: Some("anthropic".to_owned()),
957− provider_session_url: Some(console_url(&provider_session)),
980+ provider_session_url: Some(console_url(row.config().console_workspace.as_deref(), &provider_session)),
958981 provider_session_id: Some(provider_session),
959982 handoff: false,
960983 command: None,
13571380 // Nothing secret is in what the agent is: its credentials are the vault's.
13581381 assert!(!body.to_string().contains("g1t_"));
13591382 assert!(system_prompt().contains("never in a URL"));
1383+ assert!(system_prompt().contains("g1t pr checks --watch --fail-fast"));
13601384 }
13611385
13621386 #[test]
1387+ fn console_links_open_in_the_keys_console_workspace() {
1388+ assert_eq!(console_url(Some("wrkspc_01AbC"), "sesn_1"), "https://platform.claude.com/workspaces/wrkspc_01AbC/sessions/sesn_1");
1389+ assert_eq!(console_url(None, "sesn_1"), "https://platform.claude.com/workspaces/default/sessions/sesn_1");
1390+ assert_eq!(console_url(Some(""), "sesn_1"), "https://platform.claude.com/workspaces/default/sessions/sesn_1");
1391+ }
1392+
1393+ #[test]
13631394 fn the_token_is_in_a_vault_never_in_the_session_or_its_prompt() {
13641395 let hosts = vec!["g1t.sh".to_owned(), "api.g1t.sh".to_owned(), "mcp.g1t.sh".to_owned()];
13651396 let mcp = mcp_credential("https://mcp.g1t.sh", "g1t_secret");
13911422 assert!(prompt.contains("Work on issue #4 in acme/web on g1t (https://g1t.sh/acme/web/issues/4): Greet"));
13921423 assert!(prompt.contains("<issue>\nSay hello.\n</issue>"));
13931424 assert!(prompt.contains("Also: Keep it short."));
1394− assert!(prompt.contains("Authorization: Bearer $G1T_TOKEN\" clone https://g1t.sh/acme/web.git /workspace/web"));
1425+ assert!(prompt.contains("curl -fsSL https://g1t.sh/install | G1T_INSTALL_DIR=/usr/local/bin sh"));
1426+ assert!(prompt.contains("git config --global http.https://g1t.sh/.extraHeader \"Authorization: Bearer $G1T_TOKEN\""));
1427+ assert!(prompt.contains("git clone https://g1t.sh/acme/web.git /workspace/web"));
1428+ assert!(prompt.contains("echo ses_1 > /workspace/web/.git/g1t/session"));
13951429 assert!(prompt.contains("G1t-Session: ses_1"));
13961430 let environment = environment_body("g1t-acme-con_1", &hosts);
13971431 assert_eq!(environment["config"]["networking"]["allowed_hosts"], json!(hosts));
+40−0
9898 .then(|| fail(FailureCode::Forbidden, "Only an owner of the workspace can manage its AI providers."))
9999 }
100100
101+/// A Console workspace's id from the id itself or any Console address in
102+/// that workspace (`…/workspaces/<id>/…`).
103+fn console_workspace(given: &str) -> Option<String> {
104+ let id = match given.split_once("/workspaces/") {
105+ Some((_, rest)) => rest.split(['/', '?', '#']).next().unwrap_or_default(),
106+ None => given,
107+ };
108+ let valid = !id.is_empty() && id.len() <= 64 && id.chars().all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-');
109+ valid.then(|| id.to_owned())
110+}
111+
101112 /// Checks an AI provider's settings, tidying them. The message says what
102113 /// to fix.
103114 fn check_config(provider: AiProvider, config: &mut AiConfig, secret: Option<&str>) -> std::result::Result<(), String> {
110121 for value in [&mut config.model, &mut config.auth_header] {
111122 *value = value.as_deref().map(str::trim).filter(|v| !v.is_empty()).map(str::to_owned);
112123 }
124+ config.console_workspace = match config.console_workspace.as_deref().map(str::trim).filter(|v| !v.is_empty()) {
125+ Some(given) if provider == AiProvider::Anthropic => Some(console_workspace(given).ok_or_else(|| {
126+ "Give the Console workspace's id, such as wrkspc_01AbC…, or paste an address from that workspace in the Console.".to_owned()
127+ })?),
128+ _ => None,
129+ };
113130 let needs = |present: bool, what: &str| if present { Ok(()) } else { Err(what.to_owned()) };
114131 match provider {
115132 AiProvider::AzureOpenai => {
708725 assert!(check_config(AiProvider::AzureOpenai, &mut azure, key).is_ok());
709726 }
710727
728+ #[test]
729+ fn an_anthropic_providers_console_workspace_is_its_id_from_an_address_or_itself() {
730+ let key = Some("sk-test");
731+ let mut pasted = AiConfig {
732+ console_workspace: Some(" https://platform.claude.com/workspaces/wrkspc_01AbC/sessions/sesn_1?event=e ".into()),
733+ ..AiConfig::default()
734+ };
735+ assert!(check_config(AiProvider::Anthropic, &mut pasted, key).is_ok());
736+ assert_eq!(pasted.console_workspace.as_deref(), Some("wrkspc_01AbC"));
737+ let mut bare = AiConfig { console_workspace: Some("wrkspc_01AbC".into()), ..AiConfig::default() };
738+ assert!(check_config(AiProvider::Anthropic, &mut bare, key).is_ok());
739+ assert_eq!(bare.console_workspace.as_deref(), Some("wrkspc_01AbC"));
740+ let mut empty = AiConfig { console_workspace: Some(" ".into()), ..AiConfig::default() };
741+ assert!(check_config(AiProvider::Anthropic, &mut empty, key).is_ok());
742+ assert_eq!(empty.console_workspace, None);
743+ let mut wrong = AiConfig { console_workspace: Some("not a workspace".into()), ..AiConfig::default() };
744+ assert!(check_config(AiProvider::Anthropic, &mut wrong, key).is_err());
745+ // Only Anthropic's Console has workspaces to link to.
746+ let mut other = AiConfig { console_workspace: Some("wrkspc_01AbC".into()), ..AiConfig::default() };
747+ assert!(check_config(AiProvider::Openai, &mut other, key).is_ok());
748+ assert_eq!(other.console_workspace, None);
749+ }
750+
711751 fn connection(provider: AiProvider, model: Option<&str>, models: &[&str]) -> AiConnection {
712752 AiConnection {
713753 id: "con_1".into(),

Comments

Sign in to comment.