@agentconnect.md/daemon 1.53.0-rc.5 → 1.53.0-rc.6
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.
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gh-token.js","names":[],"sources":["../../src/gitcred/env.ts","../../../protocol/dist/git-url.js","../../src/cp/gh-target.ts","../../src/gitcred/gh-token-ipc.ts","../../src/gitcred/gh-token-client.ts","../../src/shim/sandbox-paths.ts","../../src/shim/gh-token.ts"],"sourcesContent":["/**\n * The three environment names the credential channel travels on.\n *\n * A leaf on purpose: the same helper source runs as a daemon CLI subcommand and inside a sandbox\n * pod, and the in-sandbox build asserts that its bundle imports nothing but node builtins. Keeping\n * these here — rather than in `cp/gitcred-server.ts`, which pulls the daemon's credential cache —\n * is what lets one implementation serve both.\n */\n\nexport const GITCRED_CAPABILITY_ENV = 'AC_GITCRED_CAPABILITY'\n/** The agent identity minted TOGETHER with the capability (git-injection\n * gitCredentialEnv). Helpers prefer this pair over the agentId baked into a\n * `.git/config` helper line, which goes stale when an agent is deleted and\n * recreated under the same name over a surviving checkout. */\nexport const GITCRED_AGENT_ENV = 'AC_GITCRED_AGENT'\n/** Where a helper finds the socket, when that is not under this daemon's own root. A helper\n * running in a sandbox pod reaches the daemon through the shim's tunnel instead, and the pod's\n * filesystem has no daemon root to derive a path from. Non-secret: it is a path, and the\n * capability is what authorizes the request that travels over it. */\nexport const GITCRED_SOCKET_ENV = 'AC_GITCRED_SOCKET'\n","/**\n * Git repo address helpers, shared by the CP (normalize on write) and the\n * daemon (defensive normalize before clone).\n *\n * STORAGE INVARIANT: `workspace.gitRepo` is persisted as a FULL cloneable git\n * address (e.g. `https://github.com/acme/infra`), never the `acme/infra`\n * shorthand a user may type. UIs shorten it back to `org/repo` for display\n * with `gitRepoLabel`.\n */\nimport { WORKSPACE_GIT_ORIGINS_ENV } from './consts.js';\n/** Scheme-full address: https://, ssh://, git://, file://, … */\nconst SCHEME_RE = /^[a-z][a-z0-9+.-]*:\\/\\//i;\n/** Any URI scheme, including non-hierarchical forms such as `ext::…`. */\nconst ANY_SCHEME_RE = /^[a-z][a-z0-9+.-]*:/i;\n/** scp-like ssh address: git@github.com:acme/infra(.git) */\nconst SCP_RE = /^[\\w.-]+@[\\w.-]+:(.+)$/;\nconst SCP_PARTS_RE = /^([\\w.-]+)@([\\w.-]+):(.+)$/;\nconst CONTROL_RE = /[\\u0000-\\u001f\\u007f]/;\n/** Wildcard allowlist entry: a policy containing it admits every valid clone origin. */\nexport const WORKSPACE_GIT_ANY_ORIGIN = '*';\n/** Ease-of-use daemon default: any valid https/ssh origin. An operator tightens by\n * stating exact origins — which REPLACE this — or disables remote Git with []. */\nexport const DEFAULT_WORKSPACE_GIT_ALLOWED_ORIGINS = [WORKSPACE_GIT_ANY_ORIGIN];\n/**\n * Hard cap on an untrusted repo reference. Real clone addresses are far under\n * this; the bound exists so no amount of caller-supplied text can turn\n * normalization into a long synchronous scan on the CP's single event loop.\n */\nexport const MAX_GIT_REPO_LENGTH = 512;\nexport class GitCloneUrlError extends Error {\n constructor(message) {\n super(message);\n this.name = 'GitCloneUrlError';\n }\n}\n/**\n * Strip trailing `/` without a backtracking regex.\n *\n * `s.replace(/\\/+$/, '')` looks anchored but is not: the engine retries the\n * greedy `+` from every offset, so a long run of slashes that does NOT end the\n * string costs O(n²). These helpers run inside request validation on the\n * control plane's single-threaded event loop, where that is a stall for every\n * tenant, not just the caller.\n */\nfunction trimTrailingSlashes(s) {\n let end = s.length;\n while (end > 0 && s.charCodeAt(end - 1) === 0x2f /* '/' */)\n end--;\n return end === s.length ? s : s.slice(0, end);\n}\n/**\n * Normalize a user-supplied repo reference into a full cloneable git address.\n * Idempotent — full addresses pass through unchanged.\n *\n * - `https://…` / `ssh://…` / `git://…` / `git@host:…` → as-is\n * - `github.com/acme/infra` (host-prefixed shorthand) → `https://github.com/acme/infra`\n * - `acme/infra` (bare org/repo) → `https://github.com/acme/infra`\n * - anything else (e.g. a local path) → as-is\n */\nexport function normalizeGitUrl(input) {\n const s = trimTrailingSlashes(input.trim());\n if (!s || SCHEME_RE.test(s) || SCP_RE.test(s))\n return s;\n const segments = s.split('/');\n // host-prefixed shorthand: first segment looks like a hostname (has a dot)\n if (segments.length >= 3 && segments[0].includes('.'))\n return `https://${s}`;\n // bare org/repo → GitHub by default\n if (segments.length === 2 && segments.every((p) => p.length > 0))\n return `https://github.com/${s}`;\n return s;\n}\nfunction invalidCloneUrl(message) {\n throw new GitCloneUrlError(message);\n}\nfunction hasLocalPathPrefix(value) {\n return (value.startsWith('/') ||\n value.startsWith('\\\\') ||\n value.startsWith('./') ||\n value.startsWith('../') ||\n value.startsWith('~/') ||\n /^[a-z]:[\\\\/]/i.test(value));\n}\n/**\n * Normalize and validate an untrusted git clone target.\n *\n * This shared codec remains host-agnostic so the CP can store repositories for\n * different daemon deployments. Only credential-free HTTPS and SSH transports\n * are accepted; the daemon applies its operator-owned exact-origin policy at\n * the execution boundary.\n */\nexport function normalizeGitCloneUrl(input) {\n // Bound FIRST: every check below scans the value, and this is the one entry\n // point untrusted repo text reaches. A real address is nowhere near the cap.\n if (input.length > MAX_GIT_REPO_LENGTH)\n invalidCloneUrl('git clone url is too long');\n if (CONTROL_RE.test(input))\n invalidCloneUrl('git clone url must not contain control characters');\n const s = trimTrailingSlashes(input.trim());\n if (!s)\n invalidCloneUrl('git clone url must not be empty');\n if (s.startsWith('-'))\n invalidCloneUrl('git clone url must not start with \"-\"');\n if (/\\s/.test(s))\n invalidCloneUrl('git clone url must not contain whitespace');\n // Git and WHATWG URLs disagree about whether a backslash terminates the\n // authority. Reject it outright so validation, redaction, and Git cannot\n // select different credentials or hosts.\n if (s.includes('\\\\'))\n invalidCloneUrl('git clone url must not contain backslashes');\n if (hasLocalPathPrefix(s))\n invalidCloneUrl('local git paths are not supported');\n const scp = SCP_PARTS_RE.exec(s);\n if (scp) {\n const path = scp[3];\n if (!path || path.startsWith('-') || path.includes('?') || path.includes('#')) {\n invalidCloneUrl('invalid scp-style git clone url');\n }\n return s;\n }\n // Reject ext::, file:, Windows drive-like values, and every other\n // non-hierarchical/unsupported scheme before shorthand normalization.\n if (ANY_SCHEME_RE.test(s) && !SCHEME_RE.test(s)) {\n invalidCloneUrl('git clone url must use https or ssh');\n }\n const normalized = normalizeGitUrl(s);\n if (!SCHEME_RE.test(normalized))\n invalidCloneUrl('git clone url must identify a remote repository');\n let url;\n try {\n url = new URL(normalized);\n }\n catch {\n invalidCloneUrl('git clone url must be a valid absolute URL');\n }\n if (url.protocol !== 'https:' && url.protocol !== 'ssh:') {\n invalidCloneUrl('git clone url must use https or ssh');\n }\n if (url.protocol === 'https:' && (url.username || url.password)) {\n invalidCloneUrl('https git clone url must not contain credentials');\n }\n if (url.protocol === 'ssh:' && url.password) {\n invalidCloneUrl('ssh git clone url must not contain a password');\n }\n if (normalized.includes('?') || normalized.includes('#')) {\n invalidCloneUrl('git clone url must not contain a query or fragment');\n }\n if (!url.hostname || !url.pathname || url.pathname === '/') {\n invalidCloneUrl('git clone url must identify a remote repository');\n }\n return normalized;\n}\nconst GITHUB_SKILL_COMPONENT_RE = /^[A-Za-z0-9_.-]+$/;\nconst GITHUB_SKILL_DOT_SEGMENT_RE = /\\/(?:\\.|%2e)(?:\\.|%2e)?(?:\\/|$)/i;\n/** Normalize the deliberately narrow source vocabulary supported by the\n * daemon's bounded GitHub archive acquisition path. Unlike generic workspaces,\n * skill sources cannot name arbitrary Git servers until an equally bounded\n * transport exists for them. */\nexport function normalizeGitHubSkillSource(input) {\n const normalized = normalizeGitCloneUrl(input);\n // WHATWG URL parsing collapses literal and percent-encoded dot segments.\n // Reject them before parsing so admission cannot silently reinterpret an\n // unsafe tree subdirectory as a different ref/path.\n if (GITHUB_SKILL_DOT_SEGMENT_RE.test(normalized)) {\n invalidCloneUrl('GitHub skill source must not contain dot path segments');\n }\n const scp = SCP_PARTS_RE.exec(normalized);\n if (scp) {\n if (scp[1] !== 'git' || scp[2].toLowerCase().replace(/\\.+$/, '') !== 'github.com') {\n invalidCloneUrl('skill source must use GitHub');\n }\n assertGitHubSkillRepositoryPath(scp[3], false, false);\n return normalized;\n }\n const url = new URL(normalized);\n const protocol = url.protocol;\n if (url.hostname.toLowerCase().replace(/\\.+$/, '') !== 'github.com' || url.port) {\n invalidCloneUrl('skill source must use canonical GitHub');\n }\n if (protocol === 'https:') {\n if (url.username || url.password)\n invalidCloneUrl('GitHub skill source must not contain credentials');\n }\n else if (protocol === 'ssh:') {\n if (url.username !== 'git' || url.password) {\n invalidCloneUrl('GitHub SSH skill source must use the git role');\n }\n }\n else {\n invalidCloneUrl('GitHub skill source must use https or ssh');\n }\n assertGitHubSkillRepositoryPath(url.pathname, protocol === 'https:', true);\n return normalized;\n}\nfunction assertGitHubSkillRepositoryPath(path, allowTree, urlPath) {\n // URL pathnames have exactly one structural leading slash; scp paths have\n // none. Do not trim an arbitrary run here: doing so would admit nonstandard\n // server-side absolute paths that the bounded daemon transport cannot safely\n // canonicalize to GitHub HTTPS.\n if (urlPath ? !path.startsWith('/') || path.startsWith('//') : path.startsWith('/') || path.startsWith('~')) {\n invalidCloneUrl('GitHub skill source must use a repository-relative path');\n }\n const encodedParts = (urlPath ? path.slice(1) : path).split('/');\n if (encodedParts.some((part) => !part)) {\n invalidCloneUrl('GitHub skill source path must not contain empty components');\n }\n let parts;\n try {\n parts = encodedParts.map((part) => decodeURIComponent(part));\n }\n catch {\n invalidCloneUrl('GitHub skill source contains malformed URL encoding');\n }\n if (parts.some((part) => !part || part.includes('/') || part.includes('\\\\') || CONTROL_RE.test(part))) {\n invalidCloneUrl('GitHub skill source contains an unsafe encoded path component');\n }\n if (parts.length < 2 || !parts[0] || !parts[1]) {\n invalidCloneUrl('GitHub skill source must identify owner/repository');\n }\n const owner = parts[0];\n const repo = parts[1].replace(/\\.git$/i, '');\n if (!GITHUB_SKILL_COMPONENT_RE.test(owner) || !GITHUB_SKILL_COMPONENT_RE.test(repo)) {\n invalidCloneUrl('GitHub skill source contains an invalid owner or repository');\n }\n if (parts.length === 2)\n return;\n if (!allowTree || parts.length < 4 || parts[2] !== 'tree' || parts.slice(3).some((part) => !part)) {\n invalidCloneUrl('GitHub skill source path must be owner/repository or owner/repository/tree/ref[/subdir]');\n }\n const ref = parts[3];\n if (ref.length > 256 || ref === '.' || ref === '..') {\n invalidCloneUrl('GitHub skill source contains an unsafe tree ref');\n }\n const subDir = parts.slice(4);\n if (subDir.join('/').length > 1_024 || subDir.some((part) => part === '.' || part === '..')) {\n invalidCloneUrl('GitHub skill source contains an unsafe tree subdirectory');\n }\n}\nfunction canonicalGitOrigin(protocol, hostname, port) {\n // A final DNS root dot does not select a different host. WHATWG already\n // does this for special schemes; reparse through HTTPS because SSH is a\n // non-special scheme and otherwise preserves case / percent-encoded IDNs.\n let host;\n try {\n host = new URL(`https://${hostname}`).hostname.toLowerCase().replace(/\\.+$/, '');\n }\n catch {\n invalidCloneUrl('git origin must identify a valid host');\n }\n if (!host)\n invalidCloneUrl('git origin must identify a host');\n const defaultPort = protocol === 'https:' ? '443' : '22';\n return `${protocol}//${host}${port && port !== defaultPort ? `:${port}` : ''}`;\n}\n/**\n * Normalize an operator-owned allowlist entry to an exact scheme/host/port\n * origin, or the bare `*` wildcard. Partial wildcards, paths, credentials,\n * queries, and fragments are not policy syntax: repository paths remain\n * tenant-selected within an allowed origin.\n */\nexport function normalizeWorkspaceGitOrigin(input) {\n if (CONTROL_RE.test(input))\n invalidCloneUrl('git origin must not contain control characters');\n const raw = input.trim();\n if (raw === WORKSPACE_GIT_ANY_ORIGIN)\n return WORKSPACE_GIT_ANY_ORIGIN;\n if (!raw || /\\s/.test(raw) || raw.includes('\\\\') || raw.includes('*')) {\n invalidCloneUrl('git origin must be an exact https or ssh origin');\n }\n let url;\n try {\n url = new URL(raw);\n }\n catch {\n invalidCloneUrl('git origin must be a valid absolute URL');\n }\n if (url.protocol !== 'https:' && url.protocol !== 'ssh:') {\n invalidCloneUrl('git origin must use https or ssh');\n }\n const authorityStart = raw.indexOf('://') + 3;\n const pathStart = raw.indexOf('/', authorityStart);\n const authority = raw.slice(authorityStart, pathStart < 0 ? raw.length : pathStart);\n const path = pathStart < 0 ? '' : raw.slice(pathStart);\n if (authority.includes('@') ||\n raw.includes('?') ||\n raw.includes('#') ||\n url.username ||\n url.password ||\n (path !== '' && path !== '/')) {\n invalidCloneUrl('git origin must not contain credentials, a path, query, or fragment');\n }\n return canonicalGitOrigin(url.protocol, url.hostname, url.port);\n}\n/** Return the canonical exact origin selected by a validated clone URL. */\nexport function workspaceGitOriginOf(input) {\n const normalized = normalizeGitCloneUrl(input);\n const scp = SCP_PARTS_RE.exec(normalized);\n if (scp)\n return canonicalGitOrigin('ssh:', scp[2], '');\n const url = new URL(normalized);\n return canonicalGitOrigin(url.protocol, url.hostname, url.port);\n}\n/**\n * Normalize a clone URL and require its exact scheme/host/port origin to be in\n * the deployment policy; a policy carrying `*` admits every valid origin. The\n * caller owns the list; tenant input can never add to it.\n */\nexport function normalizeAllowedWorkspaceGitUrl(input, allowedOrigins) {\n const normalized = normalizeGitCloneUrl(input);\n const allowed = new Set(allowedOrigins.map(normalizeWorkspaceGitOrigin));\n if (allowed.has(WORKSPACE_GIT_ANY_ORIGIN))\n return normalized;\n const origin = workspaceGitOriginOf(normalized);\n if (!allowed.has(origin)) {\n // The refusal names the origin and the operator knob: the reader is usually a tenant whose\n // fix is asking the daemon's operator, so the message must say what to ask for.\n invalidCloneUrl(`git clone origin ${origin} is not allowed by this daemon — its operator can allow it via security.workspaceGitAllowedOrigins in the daemon config (${WORKSPACE_GIT_ORIGINS_ENV} for a cluster member)`);\n }\n return normalized;\n}\nfunction withoutQueryOrFragment(value) {\n const query = value.indexOf('?');\n const fragment = value.indexOf('#');\n const cut = query < 0 ? fragment : fragment < 0 ? query : Math.min(query, fragment);\n return cut < 0 ? value : value.slice(0, cut);\n}\nfunction redactHierarchicalUrlFallback(value) {\n const match = /^([a-z][a-z0-9+.-]*:\\/\\/)([^/]*)(.*)$/i.exec(value);\n if (!match)\n return value;\n const [, prefix, authority, path] = match;\n const at = authority.lastIndexOf('@');\n if (at < 0)\n return value;\n const host = authority.slice(at + 1);\n if (prefix.toLowerCase() !== 'ssh://')\n return `${prefix}${host}${path}`;\n const username = authority.slice(0, at).split(':', 1)[0];\n return `${prefix}${username ? `${username}@` : ''}${host}${path}`;\n}\nfunction redactMalformedHierarchicalUrl(value) {\n const withoutTail = withoutQueryOrFragment(value);\n const redacted = redactHierarchicalUrlFallback(withoutTail);\n if (redacted !== withoutTail)\n return redacted;\n const authority = /^([a-z][a-z0-9+.-]*:\\/\\/)([^/]*)/i.exec(withoutTail);\n // An unparseable colon-bearing authority may be user:password or a malformed\n // host/port. Returning only the scheme is conservative, non-secret, and\n // deliberately non-cloneable.\n return authority?.[2]?.includes(':') ? authority[1] : redacted;\n}\n/**\n * Best-effort redaction for stored or historical git addresses.\n *\n * This function is deliberately total: response serialization and logging must\n * not fail merely because a legacy value is malformed. Existing shorthand\n * normalization is retained, HTTPS/other URL userinfo is removed, SSH keeps\n * its non-secret username while dropping a password, and query/fragment data\n * is discarded.\n */\nexport function redactGitUrlSecrets(input) {\n const normalized = normalizeGitUrl(input);\n if (SCP_RE.test(normalized) || !SCHEME_RE.test(normalized))\n return withoutQueryOrFragment(normalized);\n // Git and WHATWG disagree on backslashes, so never let WHATWG reserialize an\n // ambiguous password as apparent path text.\n if (normalized.includes('\\\\'))\n return redactMalformedHierarchicalUrl(normalized);\n try {\n const url = new URL(normalized);\n if (url.protocol === 'ssh:') {\n url.password = '';\n }\n else {\n url.username = '';\n url.password = '';\n }\n url.search = '';\n url.hash = '';\n return normalizeGitUrl(url.toString());\n }\n catch {\n // Malformed hierarchical URLs still receive a conservative string-level\n // redaction so the total fallback cannot echo obvious credentials.\n return redactMalformedHierarchicalUrl(normalized);\n }\n}\n/** Canonical GitHub clone URL for an App-backed workspace. The GitHub\n * installation grant is tied to owner/repo, so a caller-supplied host or extra\n * path must never select different content while retaining that authority. */\nexport function normalizeGithubRepoUrl(input) {\n const redacted = redactGitUrlSecrets(input);\n const hasGitSuffix = redacted.endsWith('.git');\n const label = gitRepoLabel(redacted);\n const parts = label.split('/');\n if (parts.length !== 2 || parts.some((part) => !part)) {\n invalidCloneUrl('github repository must be exactly owner/repo');\n }\n return normalizeGitCloneUrl(`${label}${hasGitSuffix ? '.git' : ''}`);\n}\n/**\n * Shorten a stored git address to the `org/repo` display form.\n * `https://github.com/acme/infra.git` / `git@github.com:acme/infra` → `acme/infra`.\n * Unrecognized inputs come back unchanged (minus a trailing `.git`).\n */\nexport function gitRepoLabel(gitRepo) {\n const s = trimTrailingSlashes(gitRepo.trim()).replace(/\\.git$/, '');\n const scp = SCP_RE.exec(s);\n if (scp)\n return scp[1].replace(/^\\/+/, '');\n const url = /^[a-z][a-z0-9+.-]*:\\/\\/[^/]+\\/(.+)$/i.exec(s);\n if (url)\n return url[1];\n const segments = s.split('/');\n if (segments.length >= 3 && segments[0].includes('.'))\n return segments.slice(1).join('/');\n return s;\n}\n//# sourceMappingURL=git-url.js.map","/**\n * Target-repo resolution for the `run/bin/gh` wrapper (agent-multi-repo-authorization.md\n * decision 4, issue #457).\n *\n * The generated sh wrapper only locates the real `gh` and forwards argv; picking the repo\n * whose installation token must be minted happens HERE, as a pure function the hidden\n * `gh-token` CLI calls. Resolution order matches gh's own flag > command > environment >\n * current checkout precedence: the last `-R/--repo`, then the target the command already\n * names (a `gh repo <sub>` positional, the `gh api` endpoint path, a pull/issue URL),\n * then `GH_REPO`, then the cwd origin remote. Nothing left ⇒ the workspace token.\n */\nimport { gitRepoLabel } from '@agentconnect.md/protocol'\n\n// `gh repo` subcommands whose first positional names an EXISTING repository.\nconst REPO_SUBCOMMANDS = new Set([\n 'archive',\n 'clone',\n 'delete',\n 'edit',\n 'fork',\n 'set-default',\n 'sync',\n 'unarchive',\n 'view'\n])\n\n// Per-subcommand flags that consume the next argv entry, so it is a value and not the repo positional.\nconst REPO_VALUE_FLAGS: Record<string, ReadonlySet<string>> = {\n clone: new Set(['-u', '--upstream-remote-name']),\n edit: new Set([\n '--add-topic',\n '--default-branch',\n '-d',\n '--description',\n '-h',\n '--homepage',\n '--remove-topic',\n '--squash-merge-commit-message',\n '--visibility'\n ]),\n fork: new Set(['--fork-name', '--org', '--remote-name']),\n sync: new Set(['-b', '--branch', '-s', '--source']),\n view: new Set(['-b', '--branch', '-q', '--jq', '--json', '-t', '--template'])\n}\n\n// `gh api` flags that consume the next argv entry; every other flag there is a boolean.\nconst API_VALUE_FLAGS = new Set([\n '-X',\n '--method',\n '-H',\n '--header',\n '-f',\n '--raw-field',\n '-F',\n '--field',\n '-q',\n '--jq',\n '-t',\n '--template',\n '--input',\n '--cache',\n '--hostname',\n '-p',\n '--preview'\n])\n\n// A pull/issue web URL given as the SELECTOR pins the repository as firmly as `-R` does.\nconst HTML_URL_RE = /^https:\\/\\/github\\.com\\/([^/]+)\\/([^/]+)\\/(?:pull|issues)\\/\\d+/\n\n// `gh pr`/`gh issue` subcommands whose first positional is the selector (`<number> | <url> | <branch>`).\nconst SELECTOR_SUBCOMMANDS = new Set([\n 'checkout',\n 'checks',\n 'close',\n 'comment',\n 'delete',\n 'develop',\n 'diff',\n 'edit',\n 'lock',\n 'merge',\n 'pin',\n 'ready',\n 'reopen',\n 'review',\n 'revert',\n 'transfer',\n 'unlock',\n 'unpin',\n 'update-branch',\n 'view'\n])\n\n// Per-subcommand `gh pr`/`gh issue` flags that consume the next argv entry (`-c` is a value on close, a boolean on review).\nconst SELECTOR_VALUE_FLAGS: Record<string, ReadonlySet<string>> = {\n checkout: new Set(['-b', '--branch']),\n checks: new Set(['--json', '-q', '--jq', '-t', '--template', '-i', '--interval']),\n close: new Set(['-c', '--comment', '-r', '--reason']),\n comment: new Set(['-b', '--body', '-F', '--body-file']),\n develop: new Set(['-b', '--branch-repo', '-n', '--name', '--base']),\n diff: new Set(['--color']),\n edit: new Set([\n '-a',\n '--add-assignee',\n '--remove-assignee',\n '-l',\n '--add-label',\n '--remove-label',\n '-m',\n '--milestone',\n '-p',\n '--add-project',\n '--remove-project',\n '-r',\n '--add-reviewer',\n '--remove-reviewer',\n '-B',\n '--base',\n '-b',\n '--body',\n '-F',\n '--body-file',\n '-t',\n '--title'\n ]),\n lock: new Set(['-r', '--reason']),\n merge: new Set([\n '-A',\n '--author-email',\n '-b',\n '--body',\n '-F',\n '--body-file',\n '-t',\n '--subject',\n '--match-head-commit'\n ]),\n reopen: new Set(['-c', '--comment']),\n review: new Set(['-b', '--body', '-F', '--body-file']),\n revert: new Set(['-b', '--body', '-F', '--body-file', '-t', '--title']),\n view: new Set(['--json', '-q', '--jq', '-t', '--template'])\n}\n\n/** Resolve the repo a `gh` invocation targets; `cwdOrigin` is called only if nothing earlier answers. */\nexport function resolveGhTargetRepo(\n argv: readonly string[],\n env: { GH_REPO?: string },\n cwdOrigin: () => string | undefined\n): { repo?: string; defer?: boolean } {\n const candidate = flagRepo(argv) ?? commandRepo(argv) ?? nonEmpty(env.GH_REPO) ?? cwdOrigin()\n return normalizeRepoArg(candidate)\n}\n\n/**\n * Normalize a raw repo candidate. gh accepts `OWNER/REPO`, `HOST/OWNER/REPO` and full URLs\n * for `-R`/`GH_REPO`; the cwd fallback hands us whatever `git remote get-url origin` prints.\n * Unparseable ⇒ workspace token (repo undefined); a clearly non-github.com target ⇒ defer\n * (the wrapper runs the real gh untouched — `gitRepoLabel` strips ANY host, so the\n * github.com assertion lives here).\n */\nexport function normalizeRepoArg(raw?: string): { repo?: string; defer?: boolean } {\n const s = raw?.trim()\n if (!s) return {}\n // Bare OWNER/REPO (no host, no scheme).\n if (/^[^/\\s:@]+\\/[^/\\s:@]+$/.test(s)) return { repo: s.replace(/\\.git$/i, '') }\n // Full URL (https://git.example.com/…) or scp form (git@git.example.com:…).\n const host = /^[a-z][a-z0-9+.-]*:\\/\\/(?:[^/@]+@)?([^/:]+)/i.exec(s)?.[1] ?? /^[\\w.-]+@([\\w.-]+):/.exec(s)?.[1]\n if (host) {\n if (host.toLowerCase() !== 'github.com') return { defer: true }\n const segs = gitRepoLabel(s).split('/')\n return segs.length >= 2 && segs[0] && segs[1] ? { repo: `${segs[0]}/${segs[1]}` } : {}\n }\n // HOST/OWNER/REPO plain form (gh's own shape).\n const segs = s.split('/')\n if (segs.length === 3 && segs[0]!.includes('.')) {\n if (segs[0]!.toLowerCase() !== 'github.com') return { defer: true }\n return { repo: `${segs[1]}/${segs[2]!.replace(/\\.git$/i, '')}` }\n }\n return {}\n}\n\nfunction nonEmpty(v?: string): string | undefined {\n return v && v.trim() ? v : undefined\n}\n\n/** Last `-R`/`--repo` wins, matching gh's pflag; all four spellings are accepted. */\nfunction flagRepo(argv: readonly string[]): string | undefined {\n let repo = ''\n let prev = ''\n for (const a of argv) {\n if (prev === '-R' || prev === '--repo') repo = a\n if (a.startsWith('--repo=')) repo = a.slice('--repo='.length)\n else if (a.startsWith('-R=')) repo = a.slice('-R='.length)\n else if (a.startsWith('-R') && a.length > 2) repo = a.slice('-R'.length)\n prev = a\n }\n return nonEmpty(repo)\n}\n\n/** The target the command already carries: a repo positional, the `api` endpoint, or a pull/issue URL. */\nfunction commandRepo(argv: readonly string[]): string | undefined {\n const [cmd, sub] = argv\n if (cmd === 'repo' && sub && REPO_SUBCOMMANDS.has(sub)) return repoPositional(argv, sub)\n if (cmd === 'api') return apiEndpointRepo(argv)\n if ((cmd === 'pr' || cmd === 'issue') && sub && SELECTOR_SUBCOMMANDS.has(sub)) return selectorUrlRepo(argv, sub)\n return undefined\n}\n\n/** First positional of a `gh repo <sub>` command — only a slash-bearing one names a repository. */\nfunction repoPositional(argv: readonly string[], sub: string): string | undefined {\n const valueFlags = REPO_VALUE_FLAGS[sub]\n let skipValue = false\n for (const a of argv.slice(2)) {\n if (skipValue) {\n skipValue = false\n continue\n }\n if (a === '--') break\n if (valueFlags?.has(a)) {\n skipValue = true\n continue\n }\n if (a.startsWith('-')) continue\n return a.includes('/') ? a : undefined\n }\n return undefined\n}\n\n/** `gh api repos/{owner}/{repo}/…` names the repo in the endpoint itself — the 404 this fix exists for. */\nfunction apiEndpointRepo(argv: readonly string[]): string | undefined {\n const endpoint = apiEndpoint(argv)\n if (!endpoint) return undefined\n let path = endpoint\n if (/^[a-z][a-z0-9+.-]*:\\/\\//i.test(path)) {\n const onApi = /^https?:\\/\\/api\\.github\\.com\\/?(.*)$/i.exec(path)\n if (!onApi) return path // another host — normalizeRepoArg defers\n path = onApi[1]!\n }\n path = path.replace(/[?#].*$/, '').replace(/^\\/+/, '')\n const seg = /^repos\\/([^/]+)\\/([^/]+)(?:\\/|$)/.exec(path)\n if (!seg) return undefined // graphql, /user, /orgs/… — gh's own default applies\n const [, owner, repo] = seg\n // gh's `{owner}`/`{repo}` placeholders resolve from ITS default; leave them to the later layers.\n if (owner!.includes('{') || repo!.includes('{')) return undefined\n return `${owner}/${repo}`\n}\n\n/** First positional after `api`, skipping the flags that consume a value. */\nfunction apiEndpoint(argv: readonly string[]): string | undefined {\n let skipValue = false\n let endOfFlags = false\n for (const a of argv.slice(1)) {\n if (skipValue) {\n skipValue = false\n continue\n }\n if (!endOfFlags && a.startsWith('-') && a !== '-') {\n if (a === '--') {\n endOfFlags = true\n continue\n }\n if (a.includes('=')) continue // `--flag=value` / `-X=value` are self-contained\n if (API_VALUE_FLAGS.has(a)) {\n skipValue = true\n continue\n }\n continue // `-Xvalue` is self-contained; anything else is a boolean\n }\n return a\n }\n return undefined\n}\n\n/** The selector positional of a `gh pr`/`gh issue` command, only when it is a pull/issue URL; a URL in `--search`/`--body`/… is text. */\nfunction selectorUrlRepo(argv: readonly string[], sub: string): string | undefined {\n const valueFlags = SELECTOR_VALUE_FLAGS[sub]\n let skipValue = false\n let endOfFlags = false\n for (const a of argv.slice(2)) {\n if (skipValue) {\n skipValue = false\n continue\n }\n if (!endOfFlags && a.startsWith('-') && a !== '-') {\n if (a === '--') {\n endOfFlags = true\n continue\n }\n if (a.includes('=')) continue // `--flag=value` is self-contained\n if (valueFlags?.has(a)) {\n skipValue = true\n continue\n }\n continue // an unknown flag is treated as a boolean; a stray value then reads as a non-URL selector below\n }\n return HTML_URL_RE.test(a) ? a : undefined\n }\n return undefined\n}\n","// The gitcred socket call itself, split out of `gh-token-client.ts` so a second in-sandbox entry can\n// reach a gh token without also pulling in the gh-argv target resolver and its `git remote` probe.\n// Node builtins only: every consumer of this file is bundled into the runtime image.\nimport { createConnection } from 'node:net'\nimport { GITCRED_CAPABILITY_ENV } from './env.js'\n\nexport interface GitCredIpcReply {\n ok: boolean\n password?: string\n error?: string\n}\n\n/** One newline-delimited-JSON round trip on the gitcred socket. Never rejects: an unreachable\n * daemon is an answer (`ok:false`), and callers report it as data. */\nexport function gitcredIpc(path: string, msg: unknown): Promise<GitCredIpcReply> {\n return new Promise((resolve) => {\n const sock = createConnection(path)\n let buf = ''\n const fail = (error: string) => resolve({ ok: false, error })\n sock.setTimeout(15_000, () => {\n sock.destroy()\n fail('daemon did not answer in time')\n })\n sock.on('connect', () => sock.write(JSON.stringify(msg) + '\\n'))\n sock.on('data', (c) => {\n buf += c.toString('utf8')\n const nl = buf.indexOf('\\n')\n if (nl === -1) return\n sock.destroy()\n try {\n resolve(JSON.parse(buf.slice(0, nl)) as GitCredIpcReply)\n } catch {\n fail('malformed daemon reply')\n }\n })\n sock.on('error', (e) => fail(`cannot reach the daemon socket at ${path}: ${e.message}`))\n })\n}\n\n/** A GH_TOKEN-plane token for one repository, or a thrown reason. Fetched per use rather than\n * cached here: these tokens are short-lived, and the daemon/CP side already caches and clamps. */\nexport async function fetchGhToken(\n args: { agentId: string; repoFullName: string; socketPath: string; capability?: string },\n env: NodeJS.ProcessEnv = process.env\n): Promise<string> {\n const res = await gitcredIpc(args.socketPath, {\n op: 'get',\n agentId: args.agentId,\n capability: args.capability ?? env[GITCRED_CAPABILITY_ENV],\n plane: 'gh',\n repoFullName: args.repoFullName\n })\n if (!res.ok || !res.password) {\n throw new Error(\n `no gh credentials for agent ${args.agentId} on ${args.repoFullName}: ${res.error ?? 'unknown error'}`\n )\n }\n return res.password\n}\n","// The gh wrapper's token fetch, independent of WHERE it runs (issue #457).\n// One implementation for both entries: the daemon CLI dials its own socket, the in-pod entry dials the shim's tunnel.\n// Resolves the TARGET repo with the pure `cp/gh-target.ts` resolver, then proxies gitcred with `plane: 'gh'` —\n// the widened GH_TOKEN capability set; caching, coalescing and clamping all live daemon/CP-side.\n// Exit codes are the wrapper's contract: 0 = token on stdout, 1 = refused/unreachable (reason on stderr),\n// 2 = \"not ours\" (the target names a non-github.com host), which makes the wrapper run the real gh untouched.\nimport { execFileSync } from 'node:child_process'\nimport { resolveGhTargetRepo } from '../cp/gh-target.js'\nimport { GITCRED_CAPABILITY_ENV } from './env.js'\nimport { gitcredIpc } from './gh-token-ipc.js'\n\n/** Fetch the token for this gh invocation and print it — nothing else ever reaches stdout. */\nexport async function emitGhToken(agentId: string, ghArgv: readonly string[], socketPath: string): Promise<void> {\n const target = resolveGhTargetRepo(ghArgv, process.env, cwdOriginRemote)\n if (target.defer) {\n process.exitCode = 2\n return\n }\n\n const res = await gitcredIpc(socketPath, {\n op: 'get',\n agentId,\n capability: process.env[GITCRED_CAPABILITY_ENV],\n plane: 'gh',\n repoFullName: target.repo\n })\n if (!res.ok || !res.password) {\n process.stderr.write(\n `agentconnect: no gh credentials for agent ${agentId}${target.repo ? ` on ${target.repo}` : ''}: ${res.error ?? 'unknown error'}\\n`\n )\n process.exitCode = 1\n return\n }\n process.stdout.write(res.password)\n}\n\n/** The last-resort target: the origin remote of the directory the agent ran gh in. */\nfunction cwdOriginRemote(): string | undefined {\n try {\n const out = execFileSync('git', ['remote', 'get-url', 'origin'], {\n cwd: process.cwd(),\n encoding: 'utf8',\n stdio: ['ignore', 'pipe', 'ignore']\n })\n return out.trim() || undefined\n } catch {\n return undefined\n }\n}\n","/**\n * Paths the RUNTIME IMAGE fixes, as opposed to paths this daemon owns.\n *\n * They live in their own module because the distinction is the whole point: a daemon-derived path\n * means nothing inside a sandbox, and the bugs that come from mixing the two coordinate systems\n * are silent — git asks a credential helper that exists on a machine it is not on, and the failure\n * surfaces as an authentication error. Anything here has a counterpart in\n * `docker/runtime-sandbox.Dockerfile`, and changing one without the other breaks the pod.\n */\n\n/** The credential helper git runs inside the pod. Root-owned and read-only, like the shim. */\nexport const SANDBOX_GIT_CREDENTIAL_HELPER = '/opt/agentconnect/bin/git-credential'\n\n/** The gh wrapper's token fetch in the pod — the in-sandbox twin of the daemon's hidden `gh-token` subcommand. */\nexport const SANDBOX_GH_TOKEN_ENTRY = '/opt/agentconnect/shim/gh-token.js'\n\n/** The in-pod merge-when-ready watcher the shim spawns per armed pull request — one process, killed\n * on disarm and gone with the pod. Its presence is REPORTED by the automerge handler rather than\n * assumed: an image built before it ships none, and the daemon must read that skew, not guess. */\nexport const SANDBOX_AUTO_MERGE_ENTRY = '/opt/agentconnect/shim/auto-merge.js'\n\n/** The AgentConnect tool server the agent's harness spawns in the pod, reached over the `mcp` tunnel.\n * Reported to the daemon by the probe rather than assumed: an image built before it ships none. */\nexport const SANDBOX_MCP_BRIDGE_ENTRY = '/opt/agentconnect/shim/mcp-bridge.js'\n\n/** The ONLY image directory prepended to the runtime's PATH: the gh wrapper and nothing else. */\n// Its own dir rather than reusing bin/ or shim/: those hold the credential helper and the runtime-table\n// generator, and neither should become a command an agent can run by name.\nexport const SANDBOX_GH_WRAPPER_DIR = '/opt/agentconnect/pathbin'\n\n/** Where daemon-written, per-agent git configuration is materialized in the pod. Under /run rather\n * than the workspace volume: it is regenerated per launch and belongs to the POD, so a resumed\n * workspace must not carry a previous incarnation's copy. */\nexport const SANDBOX_GIT_CONFIG_DIR = '/run/agentconnect/git'\n\n/** Shim-owned scratch space for bounded skill snapshots; callers receive opaque handles only. */\nexport const SANDBOX_SKILL_STAGING_DIR = '/run/agentconnect/skills-staging'\n\n/**\n * Where a git-repo workspace is checked out, relative to the pod's workspace mount.\n *\n * A subdirectory rather than the mount itself, because the mount is also the runtime's HOME: a\n * checkout at the root would put the repository's working tree on top of `.claude`, `.codex` and\n * `.config`, where `git status` reports them as untracked and `git clean` would delete them. A\n * from-scratch workspace keeps using the root — it has no working tree to confuse with HOME, and\n * moving it would strand every volume already provisioned.\n */\nexport const SANDBOX_CHECKOUT_DIR = 'repo'\n\n/**\n * The daemon-side servers the shim serves locally, and the in-pod path of each.\n *\n * A plain record here rather than beside the tunnel's schemas, because the credential helper needs\n * the gitcred path and nothing else: importing it from a module that also holds zod schemas made\n * rolldown emit a chunk shared with the channel bundle — a third file the image never copies, and a\n * 136 KB one at that. `tunnel.ts` re-exports this typed against its own enum, so the two cannot\n * name different sets.\n */\nexport type SandboxTunnelName = 'gitcred' | 'mcp'\nexport const SANDBOX_TUNNEL_PATHS: Readonly<Record<SandboxTunnelName, string>> = Object.freeze({\n gitcred: '/run/agentconnect/gitcred.sock',\n mcp: '/run/agentconnect/mcp.sock'\n})\n\n/** The no-search DeepSeek Harness preset the image bakes (docker/runtime-sandbox/bake-dsh-preset.mjs),\n * which the shim copies into the pod's `$DSH_HOME/.agent-presets` before launching that runtime. Its\n * presence is CONSULTED rather than assumed: an image built before it ships none, and such a pod must\n * keep launching exactly as it always did. */\nexport const SANDBOX_DSH_PRESET_DIR = '/opt/agentconnect/dsh/agent-presets/standard-no-search'\n\n/** The preset id the directory above supplies — the roster reads it from the directory NAME, so this\n * is the same string as that path's last segment and the settings default the shim writes. */\nexport const SANDBOX_DSH_PRESET_ID = 'standard-no-search'\n","#!/usr/bin/env node\n// The in-sandbox half of the gh wrapper: `/opt/agentconnect/pathbin/gh` runs this once per `gh` invocation.\n// Its own entry for the same reason the credential helper is one — the image copies ONE file per bundle, so two\n// entries whose graphs are disjoint stay two single files where a shared module would emit a chunk nothing copies.\n// It holds no policy: the daemon decides whether this agent may have a token and for which repository. What\n// arrives here is the agent's gh argv and the capability the daemon minted for this launch.\nimport { GITCRED_SOCKET_ENV } from '../gitcred/env.js'\nimport { emitGhToken } from '../gitcred/gh-token-client.js'\nimport { SANDBOX_TUNNEL_PATHS } from './sandbox-paths.js'\n\nasync function main(): Promise<number> {\n // `<agentId> -- <gh argv…>`, positional and in that order: the wrapper appends the agent's own argv after `--`.\n const [agentId, ...rest] = process.argv.slice(2)\n if (!agentId) {\n process.stderr.write('agentconnect: gh-token expects <agentId> -- <gh argv…>\\n')\n return 2\n }\n const ghArgv = rest[0] === '--' ? rest.slice(1) : rest\n // The tunnel's path unless something names another; a pod has no daemon root to derive one from.\n const socketPath = process.env[GITCRED_SOCKET_ENV]?.trim() || SANDBOX_TUNNEL_PATHS.gitcred\n await emitGhToken(agentId, ghArgv, socketPath)\n // The fetch reports a refusal by setting exitCode, and that has to reach the wrapper: exit 2 makes it run the\n // real gh untouched, so answering 2 for a refusal would silently swap \"denied\" for \"unauthenticated\".\n return typeof process.exitCode === 'number' ? process.exitCode : 0\n}\n\nmain().then(\n (code) => process.exit(code),\n (err: unknown) => {\n process.stderr.write(`agentconnect: gh-token failed: ${(err as Error).message}\\n`)\n process.exit(1)\n }\n)\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"gh-token.js","names":[],"sources":["../../src/gitcred/env.ts","../../../protocol/dist/git-url.js","../../src/cp/gh-target.ts","../../src/gitcred/gh-token-ipc.ts","../../src/gitcred/gh-token-client.ts","../../src/shim/sandbox-paths.ts","../../src/shim/gh-token.ts"],"sourcesContent":["/**\n * The three environment names the credential channel travels on.\n *\n * A leaf on purpose: the same helper source runs as a daemon CLI subcommand and inside a sandbox\n * pod, and the in-sandbox build asserts that its bundle imports nothing but node builtins. Keeping\n * these here — rather than in `cp/gitcred-server.ts`, which pulls the daemon's credential cache —\n * is what lets one implementation serve both.\n */\n\nexport const GITCRED_CAPABILITY_ENV = 'AC_GITCRED_CAPABILITY'\n/** The agent identity minted TOGETHER with the capability (git-injection\n * gitCredentialEnv). Helpers prefer this pair over the agentId baked into a\n * `.git/config` helper line, which goes stale when an agent is deleted and\n * recreated under the same name over a surviving checkout. */\nexport const GITCRED_AGENT_ENV = 'AC_GITCRED_AGENT'\n/** Where a helper finds the socket, when that is not under this daemon's own root. A helper\n * running in a sandbox pod reaches the daemon through the shim's tunnel instead, and the pod's\n * filesystem has no daemon root to derive a path from. Non-secret: it is a path, and the\n * capability is what authorizes the request that travels over it. */\nexport const GITCRED_SOCKET_ENV = 'AC_GITCRED_SOCKET'\n","/**\n * Git repo address helpers, shared by the CP (normalize on write) and the\n * daemon (defensive normalize before clone).\n *\n * STORAGE INVARIANT: `workspace.gitRepo` is persisted as a FULL cloneable git\n * address (e.g. `https://github.com/acme/infra`), never the `acme/infra`\n * shorthand a user may type. UIs shorten it back to `org/repo` for display\n * with `gitRepoLabel`.\n */\nimport { WORKSPACE_GIT_ORIGINS_ENV } from './consts.js';\n/** Scheme-full address: https://, ssh://, git://, file://, … */\nconst SCHEME_RE = /^[a-z][a-z0-9+.-]*:\\/\\//i;\n/** Any URI scheme, including non-hierarchical forms such as `ext::…`. */\nconst ANY_SCHEME_RE = /^[a-z][a-z0-9+.-]*:/i;\n/** scp-like ssh address: git@github.com:acme/infra(.git) */\nconst SCP_RE = /^[\\w.-]+@[\\w.-]+:(.+)$/;\nconst SCP_PARTS_RE = /^([\\w.-]+)@([\\w.-]+):(.+)$/;\nconst CONTROL_RE = /[\\u0000-\\u001f\\u007f]/;\n/** Wildcard allowlist entry: a policy containing it admits every valid clone origin. */\nexport const WORKSPACE_GIT_ANY_ORIGIN = '*';\n/** Ease-of-use daemon default: any valid https/ssh origin. An operator tightens by\n * stating exact origins — which REPLACE this — or disables remote Git with []. */\nexport const DEFAULT_WORKSPACE_GIT_ALLOWED_ORIGINS = [WORKSPACE_GIT_ANY_ORIGIN];\n/**\n * Hard cap on an untrusted repo reference. Real clone addresses are far under\n * this; the bound exists so no amount of caller-supplied text can turn\n * normalization into a long synchronous scan on the CP's single event loop.\n */\nexport const MAX_GIT_REPO_LENGTH = 512;\nexport class GitCloneUrlError extends Error {\n constructor(message) {\n super(message);\n this.name = 'GitCloneUrlError';\n }\n}\n/**\n * Strip trailing `/` without a backtracking regex.\n *\n * `s.replace(/\\/+$/, '')` looks anchored but is not: the engine retries the\n * greedy `+` from every offset, so a long run of slashes that does NOT end the\n * string costs O(n²). These helpers run inside request validation on the\n * control plane's single-threaded event loop, where that is a stall for every\n * tenant, not just the caller.\n */\nfunction trimTrailingSlashes(s) {\n let end = s.length;\n while (end > 0 && s.charCodeAt(end - 1) === 0x2f /* '/' */)\n end--;\n return end === s.length ? s : s.slice(0, end);\n}\n/**\n * Normalize a user-supplied repo reference into a full cloneable git address.\n * Idempotent — full addresses pass through unchanged.\n *\n * - `https://…` / `ssh://…` / `git://…` / `git@host:…` → as-is\n * - `github.com/acme/infra` (host-prefixed shorthand) → `https://github.com/acme/infra`\n * - `acme/infra` (bare org/repo) → `https://github.com/acme/infra`\n * - anything else (e.g. a local path) → as-is\n */\nexport function normalizeGitUrl(input) {\n const s = trimTrailingSlashes(input.trim());\n if (!s || SCHEME_RE.test(s) || SCP_RE.test(s))\n return s;\n const segments = s.split('/');\n // host-prefixed shorthand: first segment looks like a hostname (has a dot)\n if (segments.length >= 3 && segments[0].includes('.'))\n return `https://${s}`;\n // bare org/repo → GitHub by default\n if (segments.length === 2 && segments.every((p) => p.length > 0))\n return `https://github.com/${s}`;\n return s;\n}\nfunction invalidCloneUrl(message) {\n throw new GitCloneUrlError(message);\n}\nfunction hasLocalPathPrefix(value) {\n return (value.startsWith('/') ||\n value.startsWith('\\\\') ||\n value.startsWith('./') ||\n value.startsWith('../') ||\n value.startsWith('~/') ||\n /^[a-z]:[\\\\/]/i.test(value));\n}\n/**\n * Normalize and validate an untrusted git clone target.\n *\n * This shared codec remains host-agnostic so the CP can store repositories for\n * different daemon deployments. Only credential-free HTTPS and SSH transports\n * are accepted; the daemon applies its operator-owned exact-origin policy at\n * the execution boundary.\n */\nexport function normalizeGitCloneUrl(input) {\n // Bound FIRST: every check below scans the value, and this is the one entry\n // point untrusted repo text reaches. A real address is nowhere near the cap.\n if (input.length > MAX_GIT_REPO_LENGTH)\n invalidCloneUrl('git clone url is too long');\n if (CONTROL_RE.test(input))\n invalidCloneUrl('git clone url must not contain control characters');\n const s = trimTrailingSlashes(input.trim());\n if (!s)\n invalidCloneUrl('git clone url must not be empty');\n if (s.startsWith('-'))\n invalidCloneUrl('git clone url must not start with \"-\"');\n if (/\\s/.test(s))\n invalidCloneUrl('git clone url must not contain whitespace');\n // Git and WHATWG URLs disagree about whether a backslash terminates the\n // authority. Reject it outright so validation, redaction, and Git cannot\n // select different credentials or hosts.\n if (s.includes('\\\\'))\n invalidCloneUrl('git clone url must not contain backslashes');\n if (hasLocalPathPrefix(s))\n invalidCloneUrl('local git paths are not supported');\n const scp = SCP_PARTS_RE.exec(s);\n if (scp) {\n const path = scp[3];\n if (!path || path.startsWith('-') || path.includes('?') || path.includes('#')) {\n invalidCloneUrl('invalid scp-style git clone url');\n }\n return s;\n }\n // Reject ext::, file:, Windows drive-like values, and every other\n // non-hierarchical/unsupported scheme before shorthand normalization.\n if (ANY_SCHEME_RE.test(s) && !SCHEME_RE.test(s)) {\n invalidCloneUrl('git clone url must use https or ssh');\n }\n const normalized = normalizeGitUrl(s);\n if (!SCHEME_RE.test(normalized))\n invalidCloneUrl('git clone url must identify a remote repository');\n let url;\n try {\n url = new URL(normalized);\n }\n catch {\n invalidCloneUrl('git clone url must be a valid absolute URL');\n }\n if (url.protocol !== 'https:' && url.protocol !== 'ssh:') {\n invalidCloneUrl('git clone url must use https or ssh');\n }\n if (url.protocol === 'https:' && (url.username || url.password)) {\n invalidCloneUrl('https git clone url must not contain credentials');\n }\n if (url.protocol === 'ssh:' && url.password) {\n invalidCloneUrl('ssh git clone url must not contain a password');\n }\n if (normalized.includes('?') || normalized.includes('#')) {\n invalidCloneUrl('git clone url must not contain a query or fragment');\n }\n if (!url.hostname || !url.pathname || url.pathname === '/') {\n invalidCloneUrl('git clone url must identify a remote repository');\n }\n return normalized;\n}\nconst GITHUB_SKILL_COMPONENT_RE = /^[A-Za-z0-9_.-]+$/;\nconst GITHUB_SKILL_DOT_SEGMENT_RE = /\\/(?:\\.|%2e)(?:\\.|%2e)?(?:\\/|$)/i;\n/** Normalize the deliberately narrow source vocabulary supported by the\n * daemon's bounded GitHub archive acquisition path. Unlike generic workspaces,\n * skill sources cannot name arbitrary Git servers until an equally bounded\n * transport exists for them. */\nexport function normalizeGitHubSkillSource(input) {\n const normalized = normalizeGitCloneUrl(input);\n // WHATWG URL parsing collapses literal and percent-encoded dot segments.\n // Reject them before parsing so admission cannot silently reinterpret an\n // unsafe tree subdirectory as a different ref/path.\n if (GITHUB_SKILL_DOT_SEGMENT_RE.test(normalized)) {\n invalidCloneUrl('GitHub skill source must not contain dot path segments');\n }\n const scp = SCP_PARTS_RE.exec(normalized);\n if (scp) {\n if (scp[1] !== 'git' || scp[2].toLowerCase().replace(/\\.+$/, '') !== 'github.com') {\n invalidCloneUrl('skill source must use GitHub');\n }\n assertGitHubSkillRepositoryPath(scp[3], false, false);\n return normalized;\n }\n const url = new URL(normalized);\n const protocol = url.protocol;\n if (url.hostname.toLowerCase().replace(/\\.+$/, '') !== 'github.com' || url.port) {\n invalidCloneUrl('skill source must use canonical GitHub');\n }\n if (protocol === 'https:') {\n if (url.username || url.password)\n invalidCloneUrl('GitHub skill source must not contain credentials');\n }\n else if (protocol === 'ssh:') {\n if (url.username !== 'git' || url.password) {\n invalidCloneUrl('GitHub SSH skill source must use the git role');\n }\n }\n else {\n invalidCloneUrl('GitHub skill source must use https or ssh');\n }\n assertGitHubSkillRepositoryPath(url.pathname, protocol === 'https:', true);\n return normalized;\n}\nfunction assertGitHubSkillRepositoryPath(path, allowTree, urlPath) {\n // URL pathnames have exactly one structural leading slash; scp paths have\n // none. Do not trim an arbitrary run here: doing so would admit nonstandard\n // server-side absolute paths that the bounded daemon transport cannot safely\n // canonicalize to GitHub HTTPS.\n if (urlPath ? !path.startsWith('/') || path.startsWith('//') : path.startsWith('/') || path.startsWith('~')) {\n invalidCloneUrl('GitHub skill source must use a repository-relative path');\n }\n const encodedParts = (urlPath ? path.slice(1) : path).split('/');\n if (encodedParts.some((part) => !part)) {\n invalidCloneUrl('GitHub skill source path must not contain empty components');\n }\n let parts;\n try {\n parts = encodedParts.map((part) => decodeURIComponent(part));\n }\n catch {\n invalidCloneUrl('GitHub skill source contains malformed URL encoding');\n }\n if (parts.some((part) => !part || part.includes('/') || part.includes('\\\\') || CONTROL_RE.test(part))) {\n invalidCloneUrl('GitHub skill source contains an unsafe encoded path component');\n }\n if (parts.length < 2 || !parts[0] || !parts[1]) {\n invalidCloneUrl('GitHub skill source must identify owner/repository');\n }\n const owner = parts[0];\n const repo = parts[1].replace(/\\.git$/i, '');\n if (!GITHUB_SKILL_COMPONENT_RE.test(owner) || !GITHUB_SKILL_COMPONENT_RE.test(repo)) {\n invalidCloneUrl('GitHub skill source contains an invalid owner or repository');\n }\n if (parts.length === 2)\n return;\n if (!allowTree || parts.length < 4 || parts[2] !== 'tree' || parts.slice(3).some((part) => !part)) {\n invalidCloneUrl('GitHub skill source path must be owner/repository or owner/repository/tree/ref[/subdir]');\n }\n const ref = parts[3];\n if (ref.length > 256 || ref === '.' || ref === '..') {\n invalidCloneUrl('GitHub skill source contains an unsafe tree ref');\n }\n const subDir = parts.slice(4);\n if (subDir.join('/').length > 1_024 || subDir.some((part) => part === '.' || part === '..')) {\n invalidCloneUrl('GitHub skill source contains an unsafe tree subdirectory');\n }\n}\nfunction canonicalGitOrigin(protocol, hostname, port) {\n // A final DNS root dot does not select a different host. WHATWG already\n // does this for special schemes; reparse through HTTPS because SSH is a\n // non-special scheme and otherwise preserves case / percent-encoded IDNs.\n let host;\n try {\n host = new URL(`https://${hostname}`).hostname.toLowerCase().replace(/\\.+$/, '');\n }\n catch {\n invalidCloneUrl('git origin must identify a valid host');\n }\n if (!host)\n invalidCloneUrl('git origin must identify a host');\n const defaultPort = protocol === 'https:' ? '443' : '22';\n return `${protocol}//${host}${port && port !== defaultPort ? `:${port}` : ''}`;\n}\n/**\n * Normalize an operator-owned allowlist entry to an exact scheme/host/port\n * origin, or the bare `*` wildcard. Partial wildcards, paths, credentials,\n * queries, and fragments are not policy syntax: repository paths remain\n * tenant-selected within an allowed origin.\n */\nexport function normalizeWorkspaceGitOrigin(input) {\n if (CONTROL_RE.test(input))\n invalidCloneUrl('git origin must not contain control characters');\n const raw = input.trim();\n if (raw === WORKSPACE_GIT_ANY_ORIGIN)\n return WORKSPACE_GIT_ANY_ORIGIN;\n if (!raw || /\\s/.test(raw) || raw.includes('\\\\') || raw.includes('*')) {\n invalidCloneUrl('git origin must be an exact https or ssh origin');\n }\n let url;\n try {\n url = new URL(raw);\n }\n catch {\n invalidCloneUrl('git origin must be a valid absolute URL');\n }\n if (url.protocol !== 'https:' && url.protocol !== 'ssh:') {\n invalidCloneUrl('git origin must use https or ssh');\n }\n const authorityStart = raw.indexOf('://') + 3;\n const pathStart = raw.indexOf('/', authorityStart);\n const authority = raw.slice(authorityStart, pathStart < 0 ? raw.length : pathStart);\n const path = pathStart < 0 ? '' : raw.slice(pathStart);\n if (authority.includes('@') ||\n raw.includes('?') ||\n raw.includes('#') ||\n url.username ||\n url.password ||\n (path !== '' && path !== '/')) {\n invalidCloneUrl('git origin must not contain credentials, a path, query, or fragment');\n }\n return canonicalGitOrigin(url.protocol, url.hostname, url.port);\n}\n/** Return the canonical exact origin selected by a validated clone URL. */\nexport function workspaceGitOriginOf(input) {\n const normalized = normalizeGitCloneUrl(input);\n const scp = SCP_PARTS_RE.exec(normalized);\n if (scp)\n return canonicalGitOrigin('ssh:', scp[2], '');\n const url = new URL(normalized);\n return canonicalGitOrigin(url.protocol, url.hostname, url.port);\n}\n/**\n * Normalize a clone URL and require its exact scheme/host/port origin to be in\n * the deployment policy; a policy carrying `*` admits every valid origin. The\n * caller owns the list; tenant input can never add to it.\n */\nexport function normalizeAllowedWorkspaceGitUrl(input, allowedOrigins) {\n const normalized = normalizeGitCloneUrl(input);\n const allowed = new Set(allowedOrigins.map(normalizeWorkspaceGitOrigin));\n if (allowed.has(WORKSPACE_GIT_ANY_ORIGIN))\n return normalized;\n const origin = workspaceGitOriginOf(normalized);\n if (!allowed.has(origin)) {\n // The refusal names the origin and the operator knob: the reader is usually a tenant whose\n // fix is asking the daemon's operator, so the message must say what to ask for.\n invalidCloneUrl(`git clone origin ${origin} is not allowed by this daemon — its operator can allow it via security.workspaceGitAllowedOrigins in the daemon config (${WORKSPACE_GIT_ORIGINS_ENV} for a cluster member)`);\n }\n return normalized;\n}\nfunction withoutQueryOrFragment(value) {\n const query = value.indexOf('?');\n const fragment = value.indexOf('#');\n const cut = query < 0 ? fragment : fragment < 0 ? query : Math.min(query, fragment);\n return cut < 0 ? value : value.slice(0, cut);\n}\nfunction redactHierarchicalUrlFallback(value) {\n const match = /^([a-z][a-z0-9+.-]*:\\/\\/)([^/]*)(.*)$/i.exec(value);\n if (!match)\n return value;\n const [, prefix, authority, path] = match;\n const at = authority.lastIndexOf('@');\n if (at < 0)\n return value;\n const host = authority.slice(at + 1);\n if (prefix.toLowerCase() !== 'ssh://')\n return `${prefix}${host}${path}`;\n const username = authority.slice(0, at).split(':', 1)[0];\n return `${prefix}${username ? `${username}@` : ''}${host}${path}`;\n}\nfunction redactMalformedHierarchicalUrl(value) {\n const withoutTail = withoutQueryOrFragment(value);\n const redacted = redactHierarchicalUrlFallback(withoutTail);\n if (redacted !== withoutTail)\n return redacted;\n const authority = /^([a-z][a-z0-9+.-]*:\\/\\/)([^/]*)/i.exec(withoutTail);\n // An unparseable colon-bearing authority may be user:password or a malformed\n // host/port. Returning only the scheme is conservative, non-secret, and\n // deliberately non-cloneable.\n return authority?.[2]?.includes(':') ? authority[1] : redacted;\n}\n/**\n * Best-effort redaction for stored or historical git addresses.\n *\n * This function is deliberately total: response serialization and logging must\n * not fail merely because a legacy value is malformed. Existing shorthand\n * normalization is retained, HTTPS/other URL userinfo is removed, SSH keeps\n * its non-secret username while dropping a password, and query/fragment data\n * is discarded.\n */\nexport function redactGitUrlSecrets(input) {\n const normalized = normalizeGitUrl(input);\n if (SCP_RE.test(normalized) || !SCHEME_RE.test(normalized))\n return withoutQueryOrFragment(normalized);\n // Git and WHATWG disagree on backslashes, so never let WHATWG reserialize an\n // ambiguous password as apparent path text.\n if (normalized.includes('\\\\'))\n return redactMalformedHierarchicalUrl(normalized);\n try {\n const url = new URL(normalized);\n if (url.protocol === 'ssh:') {\n url.password = '';\n }\n else {\n url.username = '';\n url.password = '';\n }\n url.search = '';\n url.hash = '';\n return normalizeGitUrl(url.toString());\n }\n catch {\n // Malformed hierarchical URLs still receive a conservative string-level\n // redaction so the total fallback cannot echo obvious credentials.\n return redactMalformedHierarchicalUrl(normalized);\n }\n}\n/** Canonical GitHub clone URL for an App-backed workspace. The GitHub\n * installation grant is tied to owner/repo, so a caller-supplied host or extra\n * path must never select different content while retaining that authority. */\nexport function normalizeGithubRepoUrl(input) {\n const redacted = redactGitUrlSecrets(input);\n const hasGitSuffix = redacted.endsWith('.git');\n const label = gitRepoLabel(redacted);\n const parts = label.split('/');\n if (parts.length !== 2 || parts.some((part) => !part)) {\n invalidCloneUrl('github repository must be exactly owner/repo');\n }\n return normalizeGitCloneUrl(`${label}${hasGitSuffix ? '.git' : ''}`);\n}\n/**\n * Shorten a stored git address to the `org/repo` display form.\n * `https://github.com/acme/infra.git` / `git@github.com:acme/infra` → `acme/infra`.\n * Unrecognized inputs come back unchanged (minus a trailing `.git`).\n */\nexport function gitRepoLabel(gitRepo) {\n const s = trimTrailingSlashes(gitRepo.trim()).replace(/\\.git$/, '');\n const scp = SCP_RE.exec(s);\n if (scp)\n return scp[1].replace(/^\\/+/, '');\n const url = /^[a-z][a-z0-9+.-]*:\\/\\/[^/]+\\/(.+)$/i.exec(s);\n if (url)\n return url[1];\n const segments = s.split('/');\n if (segments.length >= 3 && segments[0].includes('.'))\n return segments.slice(1).join('/');\n return s;\n}\n//# sourceMappingURL=git-url.js.map","/**\n * Target-repo resolution for the `run/bin/gh` wrapper (agent-multi-repo-authorization.md\n * decision 4, issue #457).\n *\n * The generated sh wrapper only locates the real `gh` and forwards argv; picking the repo\n * whose installation token must be minted happens HERE, as a pure function the hidden\n * `gh-token` CLI calls. Resolution order matches gh's own flag > command > environment >\n * current checkout precedence: the last `-R/--repo`, then the target the command already\n * names (a `gh repo <sub>` positional, the `gh api` endpoint path, a pull/issue URL),\n * then `GH_REPO`, then the cwd origin remote. Nothing left ⇒ the workspace token.\n */\nimport { gitRepoLabel } from '@agentconnect.md/protocol'\n\n// `gh repo` subcommands whose first positional names an EXISTING repository.\nconst REPO_SUBCOMMANDS = new Set([\n 'archive',\n 'clone',\n 'delete',\n 'edit',\n 'fork',\n 'set-default',\n 'sync',\n 'unarchive',\n 'view'\n])\n\n// Per-subcommand flags that consume the next argv entry, so it is a value and not the repo positional.\nconst REPO_VALUE_FLAGS: Record<string, ReadonlySet<string>> = {\n clone: new Set(['-u', '--upstream-remote-name']),\n edit: new Set([\n '--add-topic',\n '--default-branch',\n '-d',\n '--description',\n '-h',\n '--homepage',\n '--remove-topic',\n '--squash-merge-commit-message',\n '--visibility'\n ]),\n fork: new Set(['--fork-name', '--org', '--remote-name']),\n sync: new Set(['-b', '--branch', '-s', '--source']),\n view: new Set(['-b', '--branch', '-q', '--jq', '--json', '-t', '--template'])\n}\n\n// `gh api` flags that consume the next argv entry; every other flag there is a boolean.\nconst API_VALUE_FLAGS = new Set([\n '-X',\n '--method',\n '-H',\n '--header',\n '-f',\n '--raw-field',\n '-F',\n '--field',\n '-q',\n '--jq',\n '-t',\n '--template',\n '--input',\n '--cache',\n '--hostname',\n '-p',\n '--preview'\n])\n\n// A pull/issue web URL given as the SELECTOR pins the repository as firmly as `-R` does.\nconst HTML_URL_RE = /^https:\\/\\/github\\.com\\/([^/]+)\\/([^/]+)\\/(?:pull|issues)\\/\\d+/\n\n// `gh pr`/`gh issue` subcommands whose first positional is the selector (`<number> | <url> | <branch>`).\nconst SELECTOR_SUBCOMMANDS = new Set([\n 'checkout',\n 'checks',\n 'close',\n 'comment',\n 'delete',\n 'develop',\n 'diff',\n 'edit',\n 'lock',\n 'merge',\n 'pin',\n 'ready',\n 'reopen',\n 'review',\n 'revert',\n 'transfer',\n 'unlock',\n 'unpin',\n 'update-branch',\n 'view'\n])\n\n// Per-subcommand `gh pr`/`gh issue` flags that consume the next argv entry (`-c` is a value on close, a boolean on review).\nconst SELECTOR_VALUE_FLAGS: Record<string, ReadonlySet<string>> = {\n checkout: new Set(['-b', '--branch']),\n checks: new Set(['--json', '-q', '--jq', '-t', '--template', '-i', '--interval']),\n close: new Set(['-c', '--comment', '-r', '--reason']),\n comment: new Set(['-b', '--body', '-F', '--body-file']),\n develop: new Set(['-b', '--branch-repo', '-n', '--name', '--base']),\n diff: new Set(['--color']),\n edit: new Set([\n '-a',\n '--add-assignee',\n '--remove-assignee',\n '-l',\n '--add-label',\n '--remove-label',\n '-m',\n '--milestone',\n '-p',\n '--add-project',\n '--remove-project',\n '-r',\n '--add-reviewer',\n '--remove-reviewer',\n '-B',\n '--base',\n '-b',\n '--body',\n '-F',\n '--body-file',\n '-t',\n '--title'\n ]),\n lock: new Set(['-r', '--reason']),\n merge: new Set([\n '-A',\n '--author-email',\n '-b',\n '--body',\n '-F',\n '--body-file',\n '-t',\n '--subject',\n '--match-head-commit'\n ]),\n reopen: new Set(['-c', '--comment']),\n review: new Set(['-b', '--body', '-F', '--body-file']),\n revert: new Set(['-b', '--body', '-F', '--body-file', '-t', '--title']),\n view: new Set(['--json', '-q', '--jq', '-t', '--template'])\n}\n\n/** Resolve the repo a `gh` invocation targets; `cwdOrigin` is called only if nothing earlier answers. */\nexport function resolveGhTargetRepo(\n argv: readonly string[],\n env: { GH_REPO?: string },\n cwdOrigin: () => string | undefined\n): { repo?: string; defer?: boolean } {\n const candidate = flagRepo(argv) ?? commandRepo(argv) ?? nonEmpty(env.GH_REPO) ?? cwdOrigin()\n return normalizeRepoArg(candidate)\n}\n\n/**\n * Normalize a raw repo candidate. gh accepts `OWNER/REPO`, `HOST/OWNER/REPO` and full URLs\n * for `-R`/`GH_REPO`; the cwd fallback hands us whatever `git remote get-url origin` prints.\n * Unparseable ⇒ workspace token (repo undefined); a clearly non-github.com target ⇒ defer\n * (the wrapper runs the real gh untouched — `gitRepoLabel` strips ANY host, so the\n * github.com assertion lives here).\n */\nexport function normalizeRepoArg(raw?: string): { repo?: string; defer?: boolean } {\n const s = raw?.trim()\n if (!s) return {}\n // Bare OWNER/REPO (no host, no scheme).\n if (/^[^/\\s:@]+\\/[^/\\s:@]+$/.test(s)) return { repo: s.replace(/\\.git$/i, '') }\n // Full URL (https://git.example.com/…) or scp form (git@git.example.com:…).\n const host = /^[a-z][a-z0-9+.-]*:\\/\\/(?:[^/@]+@)?([^/:]+)/i.exec(s)?.[1] ?? /^[\\w.-]+@([\\w.-]+):/.exec(s)?.[1]\n if (host) {\n if (host.toLowerCase() !== 'github.com') return { defer: true }\n const segs = gitRepoLabel(s).split('/')\n return segs.length >= 2 && segs[0] && segs[1] ? { repo: `${segs[0]}/${segs[1]}` } : {}\n }\n // HOST/OWNER/REPO plain form (gh's own shape).\n const segs = s.split('/')\n if (segs.length === 3 && segs[0]!.includes('.')) {\n if (segs[0]!.toLowerCase() !== 'github.com') return { defer: true }\n return { repo: `${segs[1]}/${segs[2]!.replace(/\\.git$/i, '')}` }\n }\n return {}\n}\n\nfunction nonEmpty(v?: string): string | undefined {\n return v && v.trim() ? v : undefined\n}\n\n/** Last `-R`/`--repo` wins, matching gh's pflag; all four spellings are accepted. */\nfunction flagRepo(argv: readonly string[]): string | undefined {\n let repo = ''\n let prev = ''\n for (const a of argv) {\n if (prev === '-R' || prev === '--repo') repo = a\n if (a.startsWith('--repo=')) repo = a.slice('--repo='.length)\n else if (a.startsWith('-R=')) repo = a.slice('-R='.length)\n else if (a.startsWith('-R') && a.length > 2) repo = a.slice('-R'.length)\n prev = a\n }\n return nonEmpty(repo)\n}\n\n/** The target the command already carries: a repo positional, the `api` endpoint, or a pull/issue URL. */\nfunction commandRepo(argv: readonly string[]): string | undefined {\n const [cmd, sub] = argv\n if (cmd === 'repo' && sub && REPO_SUBCOMMANDS.has(sub)) return repoPositional(argv, sub)\n if (cmd === 'api') return apiEndpointRepo(argv)\n if ((cmd === 'pr' || cmd === 'issue') && sub && SELECTOR_SUBCOMMANDS.has(sub)) return selectorUrlRepo(argv, sub)\n return undefined\n}\n\n/** First positional of a `gh repo <sub>` command — only a slash-bearing one names a repository. */\nfunction repoPositional(argv: readonly string[], sub: string): string | undefined {\n const valueFlags = REPO_VALUE_FLAGS[sub]\n let skipValue = false\n for (const a of argv.slice(2)) {\n if (skipValue) {\n skipValue = false\n continue\n }\n if (a === '--') break\n if (valueFlags?.has(a)) {\n skipValue = true\n continue\n }\n if (a.startsWith('-')) continue\n return a.includes('/') ? a : undefined\n }\n return undefined\n}\n\n/** `gh api repos/{owner}/{repo}/…` names the repo in the endpoint itself — the 404 this fix exists for. */\nfunction apiEndpointRepo(argv: readonly string[]): string | undefined {\n const endpoint = apiEndpoint(argv)\n if (!endpoint) return undefined\n let path = endpoint\n if (/^[a-z][a-z0-9+.-]*:\\/\\//i.test(path)) {\n const onApi = /^https?:\\/\\/api\\.github\\.com\\/?(.*)$/i.exec(path)\n if (!onApi) return path // another host — normalizeRepoArg defers\n path = onApi[1]!\n }\n path = path.replace(/[?#].*$/, '').replace(/^\\/+/, '')\n const seg = /^repos\\/([^/]+)\\/([^/]+)(?:\\/|$)/.exec(path)\n if (!seg) return undefined // graphql, /user, /orgs/… — gh's own default applies\n const [, owner, repo] = seg\n // gh's `{owner}`/`{repo}` placeholders resolve from ITS default; leave them to the later layers.\n if (owner!.includes('{') || repo!.includes('{')) return undefined\n return `${owner}/${repo}`\n}\n\n/** First positional after `api`, skipping the flags that consume a value. */\nfunction apiEndpoint(argv: readonly string[]): string | undefined {\n let skipValue = false\n let endOfFlags = false\n for (const a of argv.slice(1)) {\n if (skipValue) {\n skipValue = false\n continue\n }\n if (!endOfFlags && a.startsWith('-') && a !== '-') {\n if (a === '--') {\n endOfFlags = true\n continue\n }\n if (a.includes('=')) continue // `--flag=value` / `-X=value` are self-contained\n if (API_VALUE_FLAGS.has(a)) {\n skipValue = true\n continue\n }\n continue // `-Xvalue` is self-contained; anything else is a boolean\n }\n return a\n }\n return undefined\n}\n\n/** The selector positional of a `gh pr`/`gh issue` command, only when it is a pull/issue URL; a URL in `--search`/`--body`/… is text. */\nfunction selectorUrlRepo(argv: readonly string[], sub: string): string | undefined {\n const valueFlags = SELECTOR_VALUE_FLAGS[sub]\n let skipValue = false\n let endOfFlags = false\n for (const a of argv.slice(2)) {\n if (skipValue) {\n skipValue = false\n continue\n }\n if (!endOfFlags && a.startsWith('-') && a !== '-') {\n if (a === '--') {\n endOfFlags = true\n continue\n }\n if (a.includes('=')) continue // `--flag=value` is self-contained\n if (valueFlags?.has(a)) {\n skipValue = true\n continue\n }\n continue // an unknown flag is treated as a boolean; a stray value then reads as a non-URL selector below\n }\n return HTML_URL_RE.test(a) ? a : undefined\n }\n return undefined\n}\n","// The gitcred socket call itself, split out of `gh-token-client.ts` so a second in-sandbox entry can\n// reach a gh token without also pulling in the gh-argv target resolver and its `git remote` probe.\n// Node builtins only: every consumer of this file is bundled into the runtime image.\nimport { createConnection } from 'node:net'\nimport { GITCRED_CAPABILITY_ENV } from './env.js'\n\nexport interface GitCredIpcReply {\n ok: boolean\n password?: string\n error?: string\n}\n\n/** One newline-delimited-JSON round trip on the gitcred socket. Never rejects: an unreachable\n * daemon is an answer (`ok:false`), and callers report it as data. */\nexport function gitcredIpc(path: string, msg: unknown): Promise<GitCredIpcReply> {\n return new Promise((resolve) => {\n const sock = createConnection(path)\n let buf = ''\n const fail = (error: string) => resolve({ ok: false, error })\n sock.setTimeout(15_000, () => {\n sock.destroy()\n fail('daemon did not answer in time')\n })\n sock.on('connect', () => sock.write(JSON.stringify(msg) + '\\n'))\n sock.on('data', (c) => {\n buf += c.toString('utf8')\n const nl = buf.indexOf('\\n')\n if (nl === -1) return\n sock.destroy()\n try {\n resolve(JSON.parse(buf.slice(0, nl)) as GitCredIpcReply)\n } catch {\n fail('malformed daemon reply')\n }\n })\n sock.on('error', (e) => fail(`cannot reach the daemon socket at ${path}: ${e.message}`))\n })\n}\n\n/** A GH_TOKEN-plane token for one repository, or a thrown reason. Fetched per use rather than\n * cached here: these tokens are short-lived, and the daemon/CP side already caches and clamps. */\nexport async function fetchGhToken(\n args: { agentId: string; repoFullName: string; socketPath: string; capability?: string },\n env: NodeJS.ProcessEnv = process.env\n): Promise<string> {\n const res = await gitcredIpc(args.socketPath, {\n op: 'get',\n agentId: args.agentId,\n capability: args.capability ?? env[GITCRED_CAPABILITY_ENV],\n plane: 'gh',\n repoFullName: args.repoFullName\n })\n if (!res.ok || !res.password) {\n throw new Error(\n `no gh credentials for agent ${args.agentId} on ${args.repoFullName}: ${res.error ?? 'unknown error'}`\n )\n }\n return res.password\n}\n","// The gh wrapper's token fetch, independent of WHERE it runs (issue #457).\n// One implementation for both entries: the daemon CLI dials its own socket, the in-pod entry dials the shim's tunnel.\n// Resolves the TARGET repo with the pure `cp/gh-target.ts` resolver, then proxies gitcred with `plane: 'gh'` —\n// the widened GH_TOKEN capability set; caching, coalescing and clamping all live daemon/CP-side.\n// Exit codes are the wrapper's contract: 0 = token on stdout, 1 = refused/unreachable (reason on stderr),\n// 2 = \"not ours\" (the target names a non-github.com host), which makes the wrapper run the real gh untouched.\nimport { execFileSync } from 'node:child_process'\nimport { resolveGhTargetRepo } from '../cp/gh-target.js'\nimport { GITCRED_CAPABILITY_ENV } from './env.js'\nimport { gitcredIpc } from './gh-token-ipc.js'\n\n/** Fetch the token for this gh invocation and print it — nothing else ever reaches stdout. */\nexport async function emitGhToken(agentId: string, ghArgv: readonly string[], socketPath: string): Promise<void> {\n const target = resolveGhTargetRepo(ghArgv, process.env, cwdOriginRemote)\n if (target.defer) {\n process.exitCode = 2\n return\n }\n\n const res = await gitcredIpc(socketPath, {\n op: 'get',\n agentId,\n capability: process.env[GITCRED_CAPABILITY_ENV],\n plane: 'gh',\n repoFullName: target.repo\n })\n if (!res.ok || !res.password) {\n process.stderr.write(\n `agentconnect: no gh credentials for agent ${agentId}${target.repo ? ` on ${target.repo}` : ''}: ${res.error ?? 'unknown error'}\\n`\n )\n process.exitCode = 1\n return\n }\n process.stdout.write(res.password)\n}\n\n/** The last-resort target: the origin remote of the directory the agent ran gh in. */\nfunction cwdOriginRemote(): string | undefined {\n try {\n const out = execFileSync('git', ['remote', 'get-url', 'origin'], {\n cwd: process.cwd(),\n encoding: 'utf8',\n stdio: ['ignore', 'pipe', 'ignore']\n })\n return out.trim() || undefined\n } catch {\n return undefined\n }\n}\n","/**\n * Paths the RUNTIME IMAGE fixes, as opposed to paths this daemon owns.\n *\n * They live in their own module because the distinction is the whole point: a daemon-derived path\n * means nothing inside a sandbox, and the bugs that come from mixing the two coordinate systems\n * are silent — git asks a credential helper that exists on a machine it is not on, and the failure\n * surfaces as an authentication error. Anything here has a counterpart in\n * `docker/runtime-sandbox.Dockerfile`, and changing one without the other breaks the pod.\n */\n\n/** The credential helper git runs inside the pod. Root-owned and read-only, like the shim. */\nexport const SANDBOX_GIT_CREDENTIAL_HELPER = '/opt/agentconnect/bin/git-credential'\n\n/** The gh wrapper's token fetch in the pod — the in-sandbox twin of the daemon's hidden `gh-token` subcommand. */\nexport const SANDBOX_GH_TOKEN_ENTRY = '/opt/agentconnect/shim/gh-token.js'\n\n/** The in-pod merge-when-ready watcher the shim spawns per armed pull request — one process, killed\n * on disarm and gone with the pod. Its presence is REPORTED by the automerge handler rather than\n * assumed: an image built before it ships none, and the daemon must read that skew, not guess. */\nexport const SANDBOX_AUTO_MERGE_ENTRY = '/opt/agentconnect/shim/auto-merge.js'\n\n/** The AgentConnect tool server the agent's harness spawns in the pod, reached over the `mcp` tunnel.\n * Reported to the daemon by the probe rather than assumed: an image built before it ships none. */\nexport const SANDBOX_MCP_BRIDGE_ENTRY = '/opt/agentconnect/shim/mcp-bridge.js'\n\n/** The ONLY image directory prepended to the runtime's PATH: the gh and agent-browser wrappers. */\n// Its own dir rather than reusing bin/ or shim/: those hold the credential helper and the runtime-table\n// generator, and neither should become a command an agent can run by name.\nexport const SANDBOX_GH_WRAPPER_DIR = '/opt/agentconnect/pathbin'\n\n/** Pod env naming the Chrome the image bakes — agent-browser's only browser-location hook, so an ACP child\n * without it downloads one of its own. Set by the image, projected onto the child by acp-runner. */\nexport const SANDBOX_BROWSER_EXECUTABLE_ENV = 'AGENT_BROWSER_EXECUTABLE_PATH'\n\n/** Where daemon-written, per-agent git configuration is materialized in the pod. Under /run rather\n * than the workspace volume: it is regenerated per launch and belongs to the POD, so a resumed\n * workspace must not carry a previous incarnation's copy. */\nexport const SANDBOX_GIT_CONFIG_DIR = '/run/agentconnect/git'\n\n/** Shim-owned scratch space for bounded skill snapshots; callers receive opaque handles only. */\nexport const SANDBOX_SKILL_STAGING_DIR = '/run/agentconnect/skills-staging'\n\n/**\n * Where a git-repo workspace is checked out, relative to the pod's workspace mount.\n *\n * A subdirectory rather than the mount itself, because the mount is also the runtime's HOME: a\n * checkout at the root would put the repository's working tree on top of `.claude`, `.codex` and\n * `.config`, where `git status` reports them as untracked and `git clean` would delete them. A\n * from-scratch workspace keeps using the root — it has no working tree to confuse with HOME, and\n * moving it would strand every volume already provisioned.\n */\nexport const SANDBOX_CHECKOUT_DIR = 'repo'\n\n/**\n * The daemon-side servers the shim serves locally, and the in-pod path of each.\n *\n * A plain record here rather than beside the tunnel's schemas, because the credential helper needs\n * the gitcred path and nothing else: importing it from a module that also holds zod schemas made\n * rolldown emit a chunk shared with the channel bundle — a third file the image never copies, and a\n * 136 KB one at that. `tunnel.ts` re-exports this typed against its own enum, so the two cannot\n * name different sets.\n */\nexport type SandboxTunnelName = 'gitcred' | 'mcp'\nexport const SANDBOX_TUNNEL_PATHS: Readonly<Record<SandboxTunnelName, string>> = Object.freeze({\n gitcred: '/run/agentconnect/gitcred.sock',\n mcp: '/run/agentconnect/mcp.sock'\n})\n\n/** The no-search DeepSeek Harness preset the image bakes (docker/runtime-sandbox/bake-dsh-preset.mjs),\n * which the shim copies into the pod's `$DSH_HOME/.agent-presets` before launching that runtime. Its\n * presence is CONSULTED rather than assumed: an image built before it ships none, and such a pod must\n * keep launching exactly as it always did. */\nexport const SANDBOX_DSH_PRESET_DIR = '/opt/agentconnect/dsh/agent-presets/standard-no-search'\n\n/** The preset id the directory above supplies — the roster reads it from the directory NAME, so this\n * is the same string as that path's last segment and the settings default the shim writes. */\nexport const SANDBOX_DSH_PRESET_ID = 'standard-no-search'\n","#!/usr/bin/env node\n// The in-sandbox half of the gh wrapper: `/opt/agentconnect/pathbin/gh` runs this once per `gh` invocation.\n// Its own entry for the same reason the credential helper is one — the image copies ONE file per bundle, so two\n// entries whose graphs are disjoint stay two single files where a shared module would emit a chunk nothing copies.\n// It holds no policy: the daemon decides whether this agent may have a token and for which repository. What\n// arrives here is the agent's gh argv and the capability the daemon minted for this launch.\nimport { GITCRED_SOCKET_ENV } from '../gitcred/env.js'\nimport { emitGhToken } from '../gitcred/gh-token-client.js'\nimport { SANDBOX_TUNNEL_PATHS } from './sandbox-paths.js'\n\nasync function main(): Promise<number> {\n // `<agentId> -- <gh argv…>`, positional and in that order: the wrapper appends the agent's own argv after `--`.\n const [agentId, ...rest] = process.argv.slice(2)\n if (!agentId) {\n process.stderr.write('agentconnect: gh-token expects <agentId> -- <gh argv…>\\n')\n return 2\n }\n const ghArgv = rest[0] === '--' ? rest.slice(1) : rest\n // The tunnel's path unless something names another; a pod has no daemon root to derive one from.\n const socketPath = process.env[GITCRED_SOCKET_ENV]?.trim() || SANDBOX_TUNNEL_PATHS.gitcred\n await emitGhToken(agentId, ghArgv, socketPath)\n // The fetch reports a refusal by setting exitCode, and that has to reach the wrapper: exit 2 makes it run the\n // real gh untouched, so answering 2 for a refusal would silently swap \"denied\" for \"unauthenticated\".\n return typeof process.exitCode === 'number' ? process.exitCode : 0\n}\n\nmain().then(\n (code) => process.exit(code),\n (err: unknown) => {\n process.stderr.write(`agentconnect: gh-token failed: ${(err as Error).message}\\n`)\n process.exit(1)\n }\n)\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"git-credential.js","names":[],"sources":["../../src/gitcred/env.ts","../../src/gitcred/managed-hosts.ts","../../src/gitcred/helper.ts","../../src/shim/sandbox-paths.ts","../../src/shim/git-credential.ts"],"sourcesContent":["/**\n * The three environment names the credential channel travels on.\n *\n * A leaf on purpose: the same helper source runs as a daemon CLI subcommand and inside a sandbox\n * pod, and the in-sandbox build asserts that its bundle imports nothing but node builtins. Keeping\n * these here — rather than in `cp/gitcred-server.ts`, which pulls the daemon's credential cache —\n * is what lets one implementation serve both.\n */\n\nexport const GITCRED_CAPABILITY_ENV = 'AC_GITCRED_CAPABILITY'\n/** The agent identity minted TOGETHER with the capability (git-injection\n * gitCredentialEnv). Helpers prefer this pair over the agentId baked into a\n * `.git/config` helper line, which goes stale when an agent is deleted and\n * recreated under the same name over a surviving checkout. */\nexport const GITCRED_AGENT_ENV = 'AC_GITCRED_AGENT'\n/** Where a helper finds the socket, when that is not under this daemon's own root. A helper\n * running in a sandbox pod reaches the daemon through the shim's tunnel instead, and the pod's\n * filesystem has no daemon root to derive a path from. Non-secret: it is a path, and the\n * capability is what authorizes the request that travels over it. */\nexport const GITCRED_SOCKET_ENV = 'AC_GITCRED_SOCKET'\n","/**\n * The injected host→provider table (gitlab-com-integration.md §24.4) and the parsing both ends of\n * the credential channel share.\n *\n * A leaf with NO imports on purpose: the credential helper and the `glab` token entry are bundled\n * for the sandbox image and may pull in nothing but node builtins, while the daemon writes the same\n * table at injection time. Each entry carries the FULL normalized base URL — scheme, host,\n * non-default port, and any path prefix — because with `useHttpPath` a prefixed install hands git a\n * credential `path` that starts with that prefix, and a bare hostname could not strip it.\n */\n\nexport type ManagedCredentialProvider = 'github' | 'gitlab'\n\n/** One managed code host: the provider plus the normalized base URL its consumers address. */\nexport interface ManagedCredentialHost {\n provider: ManagedCredentialProvider\n /** Scheme, lower-cased host, non-default port, and any path prefix; never a trailing slash. */\n baseUrl: string\n}\n\n/** The env name the table travels on, minted beside the capability and agent identity. */\nexport const GITCRED_HOSTS_ENV = 'AC_GITCRED_HOSTS'\n\nexport const GITHUB_MANAGED_HOST: ManagedCredentialHost = { provider: 'github', baseUrl: 'https://github.com' }\n\n/** The default value of the GitLab host axis (§24.1) — absent is GitLab.com, never a second mode. */\nexport const GITLAB_COM_BASE_URL = 'https://gitlab.com'\n\nfunction trimTrailingSlashes(value: string): string {\n let end = value.length\n while (end > 0 && value.charCodeAt(end - 1) === 47) end -= 1\n return value.slice(0, end)\n}\n\nfunction trimSurroundingSlashes(value: string): string {\n let start = 0\n while (start < value.length && value.charCodeAt(start) === 47) start += 1\n return trimTrailingSlashes(value.slice(start))\n}\n\n/** The GitLab instance a spec's GitLab consumers address; an absent host means GitLab.com (§24.1). */\nexport function gitlabManagedHost(gitlabHost?: string): ManagedCredentialHost {\n const trimmed = trimTrailingSlashes((gitlabHost ?? '').trim())\n return { provider: 'gitlab', baseUrl: trimmed === '' ? GITLAB_COM_BASE_URL : trimmed }\n}\n\n/** The table one agent's git classifies against: GitHub plus the one GitLab instance its spec names. */\nexport function managedHostTableFor(gitlabHost?: string): ManagedCredentialHost[] {\n return [GITHUB_MANAGED_HOST, gitlabManagedHost(gitlabHost)]\n}\n\n/** `github=https://github.com gitlab=https://gitlab.example.test:8443/gitlab` — space separated,\n * which no absolute URL may contain. */\nexport function encodeManagedHostTable(hosts: readonly ManagedCredentialHost[]): string {\n return hosts.map((entry) => `${entry.provider}=${entry.baseUrl}`).join(' ')\n}\n\n/** Absent or unparseable ⇒ the default table, which is what a deployment on GitLab.com means. */\nexport function decodeManagedHostTable(raw: string | undefined): ManagedCredentialHost[] {\n const entries: ManagedCredentialHost[] = []\n for (const token of (raw ?? '').split(/\\s+/)) {\n const eq = token.indexOf('=')\n if (eq <= 0) continue\n const provider = token.slice(0, eq)\n const baseUrl = trimTrailingSlashes(token.slice(eq + 1))\n if (provider !== 'github' && provider !== 'gitlab') continue\n if (parseManagedBaseUrl(baseUrl) === undefined) continue\n entries.push({ provider, baseUrl })\n }\n return entries.length > 0 ? entries : managedHostTableFor()\n}\n\nexport interface ManagedBaseUrlParts {\n /** Lower-cased scheme without the colon, as git spells it on the credential `protocol` line. */\n protocol: string\n /** Lower-cased host including a non-default port, as git spells it on the `host` line. */\n host: string\n /** Path prefix without surrounding slashes; empty for an instance at the URL root. */\n pathPrefix: string\n}\n\nexport function parseManagedBaseUrl(baseUrl: string): ManagedBaseUrlParts | undefined {\n const match = /^([a-z][a-z0-9+.-]*):\\/\\/([^/?#\\s]+)(\\/[^?#\\s]*)?$/i.exec(baseUrl.trim())\n if (!match) return undefined\n return {\n protocol: match[1]!.toLowerCase(),\n host: match[2]!.toLowerCase(),\n pathPrefix: trimSurroundingSlashes(match[3] ?? '')\n }\n}\n\n/**\n * The request path with the entry's prefix removed on an EXACT segment boundary, or undefined when\n * the prefix does not apply — a second GitLab at another prefix on the same host is not ours.\n */\nexport function stripHostPathPrefix(path: string, pathPrefix: string): string | undefined {\n const cleaned = path.replace(/^\\/+/, '')\n if (pathPrefix === '') return cleaned\n if (!cleaned.startsWith(pathPrefix)) return undefined\n const rest = cleaned.slice(pathPrefix.length)\n if (rest === '') return ''\n if (!rest.startsWith('/')) return undefined\n return rest.replace(/^\\/+/, '')\n}\n\n/** What git hands a credential helper on stdin, as far as routing cares. */\nexport interface ManagedHostQuery {\n protocol?: string\n host?: string\n path?: string\n}\n\nexport interface ManagedHostMatch {\n entry: ManagedCredentialHost\n /** The request path with the entry's prefix stripped; absent when git sent no path. */\n path?: string\n}\n\n/**\n * The table entry a credential request belongs to — undefined means \"not ours\", and the caller must\n * stay silent rather than guess. Host comparison is exact, so a host that is a prefix or a suffix of\n * a managed one never matches; among matches the longest applicable path prefix wins.\n */\nexport function matchManagedHost(\n table: readonly ManagedCredentialHost[],\n query: ManagedHostQuery\n): ManagedHostMatch | undefined {\n const protocol = query.protocol?.toLowerCase()\n const host = query.host?.toLowerCase()\n if (host === undefined) return undefined\n let best: ManagedHostMatch | undefined\n let bestPrefixLength = -1\n for (const entry of table) {\n const parts = parseManagedBaseUrl(entry.baseUrl)\n if (!parts) continue\n if (parts.host !== host) continue\n if (protocol !== undefined && protocol !== parts.protocol) continue\n let path: string | undefined\n if (query.path !== undefined) {\n path = stripHostPathPrefix(query.path, parts.pathPrefix)\n if (path === undefined) continue\n } else if (parts.pathPrefix !== '') {\n // A prefixed install cannot be recognized without the path git was asked for.\n continue\n }\n if (parts.pathPrefix.length <= bestPrefixLength) continue\n bestPrefixLength = parts.pathPrefix.length\n best = { entry, ...(path !== undefined ? { path } : {}) }\n }\n return best\n}\n","/**\n * The git credential helper itself (docs/designs/github-app-git-credentials.md §Local Helper\n * Channel), independent of where it runs.\n *\n * Speaks git's credential-helper protocol on stdin/stdout and proxies to a gitcred socket with the\n * runtime-only agent capability; the token exists only on the reply pipe. Actions:\n * get → username=x-access-token / password=<token> (host must be github.com)\n * erase → forward to the daemon (invalidate the cached token git just had rejected)\n * store → no-op\n *\n * The `path` git sends (useHttpPath=true is part of the injection) ROUTES the\n * request since issue #457: it is parsed to \"owner/repo\" and forwarded, so an\n * authorized non-workspace repo gets ITS token (multi-repo design decision 5).\n * Path absent/unparseable ⇒ the workspace token, the pre-#457 behavior. This\n * is routing, not authorization — the CP gate decides; token scope enforces\n * the real boundary at GitHub either way.\n *\n * WHICH hosts are ours comes from the injected table (§24.4), never from two literals and never\n * from the agent's own environment as a hint: the daemon writes it beside the capability at\n * injection time, each entry carrying the full base URL, so a prefixed install's path prefix is\n * stripped before the project path is parsed.\n *\n * Exit style: on ANY failure print a human-actionable line to stderr and exit 1\n * — since #251 surfaces turn failures to end users, this text is user-facing.\n * NEVER print or log the token outside the protocol response.\n *\n * WHICH socket to dial is the caller's business, and that is the whole reason this is a leaf: the\n * daemon CLI derives it from its own root, while in a sandbox pod the same code dials the path the\n * shim serves the daemon's socket on. Its only imports are node builtins and the env names, because\n * the in-sandbox bundle is asserted to import nothing else.\n */\nimport { createConnection } from 'node:net'\nimport { GITCRED_AGENT_ENV, GITCRED_CAPABILITY_ENV } from './env.js'\nimport { decodeManagedHostTable, GITCRED_HOSTS_ENV, matchManagedHost } from './managed-hosts.js'\n\ninterface HelperInput {\n protocol?: string\n host?: string\n path?: string\n password?: string\n}\n\n/**\n * The argv id comes from a config file (`.git/config` helper line) that can\n * outlive the agent that wrote it — deleted + recreated under the same name,\n * the checkout survives with the DEAD id and the daemon denies every request.\n * The env identity is minted together with the capability at spawn/clone\n * (gitCredentialEnv), so when present it names the agent actually invoking\n * git and always matches the capability sent beside it.\n */\nexport function effectiveAgentId(argvAgentId: string): string {\n return process.env[GITCRED_AGENT_ENV] || argvAgentId\n}\n\nexport async function runGitCredential(action: string, agentId: string, socketPath: string): Promise<void> {\n if (action === 'store') return // git offers the accepted credential back — nothing to do\n\n agentId = effectiveAgentId(agentId)\n\n const input = parseStdin(await readStdin())\n const match = matchManagedHost(decodeManagedHostTable(process.env[GITCRED_HOSTS_ENV]), input)\n // GitLab keeps full subgroup depth from the instance's path prefix (§13.2); github stays owner/repo.\n const gitlab = match?.entry.provider === 'gitlab'\n const repo = match?.path === undefined ? undefined : gitlab ? projectFromPath(match.path) : repoFromPath(match.path)\n\n // Not ours — stay silent so git can try other helpers; an absent host still means the workspace.\n if (input.host !== undefined && match === undefined) return\n\n if (action === 'erase') {\n // Route the invalidation to the same (agent, repo) key the get used.\n await ipc(socketPath, {\n op: 'erase',\n agentId,\n capability: process.env[GITCRED_CAPABILITY_ENV],\n password: input.password,\n repoFullName: repo,\n ...(gitlab ? { provider: 'gitlab' } : {})\n }).catch(() => undefined) // best-effort\n return\n }\n if (action !== 'get') return // unknown actions are ignored per the helper contract\n\n const res = await ipc(socketPath, {\n op: 'get',\n agentId,\n capability: process.env[GITCRED_CAPABILITY_ENV],\n repoFullName: repo,\n ...(gitlab ? { provider: 'gitlab' } : {})\n })\n if (!res.ok || !res.username || !res.password) {\n process.stderr.write(\n `agentconnect: no git credentials for agent ${agentId}${repo ? ` on ${repo}` : ''}: ${res.error ?? 'unknown error'}\\n` +\n `(the daemon must be running and connected to the control plane)\\n`\n )\n process.exitCode = 1\n return\n }\n // Diagnostics-only sanity (the daemon guard already refuses cross-repo\n // grants): a mismatch here means an old daemon answered with the workspace\n // token — GitHub's single-repo token scope still enforces the boundary.\n if (repo !== undefined) {\n const want = normalizeRepoPath(res.repoFullName ?? '')\n if (want && want !== repo) {\n process.stderr.write(\n `agentconnect: note — git asked for ${repo} but the daemon answered for ${want} ` +\n `(daemon predates per-repo credentials?)\\n`\n )\n }\n }\n process.stdout.write(`username=${res.username}\\npassword=${res.password}\\n`)\n}\n\nfunction normalizeRepoPath(p: string): string {\n return p\n .replace(/^\\/+/, '')\n .replace(/\\.git$/i, '')\n .toLowerCase()\n}\n\n/** The full namespaced GitLab project path from git's credential `path` —\n * arbitrary subgroup depth, tolerating a leading slash, a `.git` suffix, and\n * LFS-ish subpaths (`group/sub/project.git/info/lfs`). */\nexport function projectFromPath(p: string): string | undefined {\n const cleaned = p.replace(/^\\/+/, '')\n const gitSuffix = cleaned.search(/\\.git(?:\\/|$)/i)\n const path = (gitSuffix >= 0 ? cleaned.slice(0, gitSuffix) : cleaned).replace(/\\/+$/, '')\n return path.includes('/') ? path.toLowerCase() : undefined\n}\n\n/** \"owner/repo\" from git's credential `path` — tolerates a leading slash, a\n * `.git` suffix, and LFS-ish subpaths (`owner/repo.git/info/lfs`). */\nexport function repoFromPath(p: string): string | undefined {\n const segs = p.replace(/^\\/+/, '').split('/')\n const owner = segs[0]\n const repo = segs[1]?.replace(/\\.git$/i, '')\n if (!owner || !repo) return undefined\n return `${owner}/${repo}`.toLowerCase()\n}\n\nfunction parseStdin(text: string): HelperInput {\n const out: Record<string, string> = {}\n for (const line of text.split('\\n')) {\n const eq = line.indexOf('=')\n if (eq > 0) out[line.slice(0, eq)] = line.slice(eq + 1)\n }\n return out\n}\n\nfunction readStdin(): Promise<string> {\n return new Promise((resolve) => {\n let buf = ''\n process.stdin.setEncoding('utf8')\n process.stdin.on('data', (c) => (buf += c))\n process.stdin.on('end', () => resolve(buf))\n process.stdin.on('error', () => resolve(buf))\n })\n}\n\ninterface IpcReply {\n ok: boolean\n username?: string\n password?: string\n repoFullName?: string\n error?: string\n}\n\nfunction ipc(path: string, msg: unknown): Promise<IpcReply> {\n return new Promise((resolve) => {\n const sock = createConnection(path)\n let buf = ''\n const fail = (error: string) => resolve({ ok: false, error })\n sock.setTimeout(15_000, () => {\n sock.destroy()\n fail('daemon did not answer in time')\n })\n sock.on('connect', () => sock.write(JSON.stringify(msg) + '\\n'))\n sock.on('data', (c) => {\n buf += c.toString('utf8')\n const nl = buf.indexOf('\\n')\n if (nl === -1) return\n sock.destroy()\n try {\n resolve(JSON.parse(buf.slice(0, nl)) as IpcReply)\n } catch {\n fail('malformed daemon reply')\n }\n })\n sock.on('error', (e) => fail(`cannot reach the daemon socket at ${path}: ${e.message}`))\n })\n}\n","/**\n * Paths the RUNTIME IMAGE fixes, as opposed to paths this daemon owns.\n *\n * They live in their own module because the distinction is the whole point: a daemon-derived path\n * means nothing inside a sandbox, and the bugs that come from mixing the two coordinate systems\n * are silent — git asks a credential helper that exists on a machine it is not on, and the failure\n * surfaces as an authentication error. Anything here has a counterpart in\n * `docker/runtime-sandbox.Dockerfile`, and changing one without the other breaks the pod.\n */\n\n/** The credential helper git runs inside the pod. Root-owned and read-only, like the shim. */\nexport const SANDBOX_GIT_CREDENTIAL_HELPER = '/opt/agentconnect/bin/git-credential'\n\n/** The gh wrapper's token fetch in the pod — the in-sandbox twin of the daemon's hidden `gh-token` subcommand. */\nexport const SANDBOX_GH_TOKEN_ENTRY = '/opt/agentconnect/shim/gh-token.js'\n\n/** The in-pod merge-when-ready watcher the shim spawns per armed pull request — one process, killed\n * on disarm and gone with the pod. Its presence is REPORTED by the automerge handler rather than\n * assumed: an image built before it ships none, and the daemon must read that skew, not guess. */\nexport const SANDBOX_AUTO_MERGE_ENTRY = '/opt/agentconnect/shim/auto-merge.js'\n\n/** The AgentConnect tool server the agent's harness spawns in the pod, reached over the `mcp` tunnel.\n * Reported to the daemon by the probe rather than assumed: an image built before it ships none. */\nexport const SANDBOX_MCP_BRIDGE_ENTRY = '/opt/agentconnect/shim/mcp-bridge.js'\n\n/** The ONLY image directory prepended to the runtime's PATH: the gh wrapper and nothing else. */\n// Its own dir rather than reusing bin/ or shim/: those hold the credential helper and the runtime-table\n// generator, and neither should become a command an agent can run by name.\nexport const SANDBOX_GH_WRAPPER_DIR = '/opt/agentconnect/pathbin'\n\n/** Where daemon-written, per-agent git configuration is materialized in the pod. Under /run rather\n * than the workspace volume: it is regenerated per launch and belongs to the POD, so a resumed\n * workspace must not carry a previous incarnation's copy. */\nexport const SANDBOX_GIT_CONFIG_DIR = '/run/agentconnect/git'\n\n/** Shim-owned scratch space for bounded skill snapshots; callers receive opaque handles only. */\nexport const SANDBOX_SKILL_STAGING_DIR = '/run/agentconnect/skills-staging'\n\n/**\n * Where a git-repo workspace is checked out, relative to the pod's workspace mount.\n *\n * A subdirectory rather than the mount itself, because the mount is also the runtime's HOME: a\n * checkout at the root would put the repository's working tree on top of `.claude`, `.codex` and\n * `.config`, where `git status` reports them as untracked and `git clean` would delete them. A\n * from-scratch workspace keeps using the root — it has no working tree to confuse with HOME, and\n * moving it would strand every volume already provisioned.\n */\nexport const SANDBOX_CHECKOUT_DIR = 'repo'\n\n/**\n * The daemon-side servers the shim serves locally, and the in-pod path of each.\n *\n * A plain record here rather than beside the tunnel's schemas, because the credential helper needs\n * the gitcred path and nothing else: importing it from a module that also holds zod schemas made\n * rolldown emit a chunk shared with the channel bundle — a third file the image never copies, and a\n * 136 KB one at that. `tunnel.ts` re-exports this typed against its own enum, so the two cannot\n * name different sets.\n */\nexport type SandboxTunnelName = 'gitcred' | 'mcp'\nexport const SANDBOX_TUNNEL_PATHS: Readonly<Record<SandboxTunnelName, string>> = Object.freeze({\n gitcred: '/run/agentconnect/gitcred.sock',\n mcp: '/run/agentconnect/mcp.sock'\n})\n\n/** The no-search DeepSeek Harness preset the image bakes (docker/runtime-sandbox/bake-dsh-preset.mjs),\n * which the shim copies into the pod's `$DSH_HOME/.agent-presets` before launching that runtime. Its\n * presence is CONSULTED rather than assumed: an image built before it ships none, and such a pod must\n * keep launching exactly as it always did. */\nexport const SANDBOX_DSH_PRESET_DIR = '/opt/agentconnect/dsh/agent-presets/standard-no-search'\n\n/** The preset id the directory above supplies — the roster reads it from the directory NAME, so this\n * is the same string as that path's last segment and the settings default the shim writes. */\nexport const SANDBOX_DSH_PRESET_ID = 'standard-no-search'\n","#!/usr/bin/env node\n/**\n * The in-sandbox git credential helper. Lives at a fixed path in the runtime image\n * (`/opt/agentconnect/bin/git-credential` wraps it), root-owned and read-only like the shim.\n *\n * Its own entry rather than a mode of the shim, for two reasons that point the same way. Git spawns\n * a credential helper once per operation, and the shim entry pulls in the WebSocket client — a cost\n * and a dependency graph this has no use for. And the image copies ONE file per bundle, so two\n * entries whose graphs are disjoint stay two single files, where a shared module would make\n * rolldown emit a chunk that never gets copied.\n *\n * It holds no policy: the daemon decides whether this agent may have a credential and for which\n * repository. What arrives here is the request git wrote on stdin and the capability the daemon\n * minted for this launch.\n */\nimport { GITCRED_SOCKET_ENV } from '../gitcred/env.js'\nimport { runGitCredential } from '../gitcred/helper.js'\nimport { SANDBOX_TUNNEL_PATHS } from './sandbox-paths.js'\n\nasync function main(): Promise<number> {\n // `<agentId> <action>`, positional and in that order: git APPENDS the action to whatever the\n // helper line named, so the id it was configured with comes first.\n const [agentId, action] = process.argv.slice(2)\n if (!action) {\n process.stderr.write('agentconnect: git-credential expects <agentId> <action>\\n')\n return 2\n }\n // The tunnel's path unless something names another; a pod has no daemon root to derive one from.\n const socketPath = process.env[GITCRED_SOCKET_ENV]?.trim() || SANDBOX_TUNNEL_PATHS.gitcred\n await runGitCredential(action, agentId ?? '', socketPath)\n // The helper reports a missing credential by setting exitCode, and that has to reach git: a zero\n // exit with no output reads as \"this helper has no opinion\" rather than as a failure.\n return typeof process.exitCode === 'number' ? process.exitCode : 0\n}\n\nmain().then(\n (code) => process.exit(code),\n (err: unknown) => {\n process.stderr.write(`agentconnect: git-credential failed: ${(err as Error).message}\\n`)\n process.exit(1)\n }\n)\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"git-credential.js","names":[],"sources":["../../src/gitcred/env.ts","../../src/gitcred/managed-hosts.ts","../../src/gitcred/helper.ts","../../src/shim/sandbox-paths.ts","../../src/shim/git-credential.ts"],"sourcesContent":["/**\n * The three environment names the credential channel travels on.\n *\n * A leaf on purpose: the same helper source runs as a daemon CLI subcommand and inside a sandbox\n * pod, and the in-sandbox build asserts that its bundle imports nothing but node builtins. Keeping\n * these here — rather than in `cp/gitcred-server.ts`, which pulls the daemon's credential cache —\n * is what lets one implementation serve both.\n */\n\nexport const GITCRED_CAPABILITY_ENV = 'AC_GITCRED_CAPABILITY'\n/** The agent identity minted TOGETHER with the capability (git-injection\n * gitCredentialEnv). Helpers prefer this pair over the agentId baked into a\n * `.git/config` helper line, which goes stale when an agent is deleted and\n * recreated under the same name over a surviving checkout. */\nexport const GITCRED_AGENT_ENV = 'AC_GITCRED_AGENT'\n/** Where a helper finds the socket, when that is not under this daemon's own root. A helper\n * running in a sandbox pod reaches the daemon through the shim's tunnel instead, and the pod's\n * filesystem has no daemon root to derive a path from. Non-secret: it is a path, and the\n * capability is what authorizes the request that travels over it. */\nexport const GITCRED_SOCKET_ENV = 'AC_GITCRED_SOCKET'\n","/**\n * The injected host→provider table (gitlab-com-integration.md §24.4) and the parsing both ends of\n * the credential channel share.\n *\n * A leaf with NO imports on purpose: the credential helper and the `glab` token entry are bundled\n * for the sandbox image and may pull in nothing but node builtins, while the daemon writes the same\n * table at injection time. Each entry carries the FULL normalized base URL — scheme, host,\n * non-default port, and any path prefix — because with `useHttpPath` a prefixed install hands git a\n * credential `path` that starts with that prefix, and a bare hostname could not strip it.\n */\n\nexport type ManagedCredentialProvider = 'github' | 'gitlab'\n\n/** One managed code host: the provider plus the normalized base URL its consumers address. */\nexport interface ManagedCredentialHost {\n provider: ManagedCredentialProvider\n /** Scheme, lower-cased host, non-default port, and any path prefix; never a trailing slash. */\n baseUrl: string\n}\n\n/** The env name the table travels on, minted beside the capability and agent identity. */\nexport const GITCRED_HOSTS_ENV = 'AC_GITCRED_HOSTS'\n\nexport const GITHUB_MANAGED_HOST: ManagedCredentialHost = { provider: 'github', baseUrl: 'https://github.com' }\n\n/** The default value of the GitLab host axis (§24.1) — absent is GitLab.com, never a second mode. */\nexport const GITLAB_COM_BASE_URL = 'https://gitlab.com'\n\nfunction trimTrailingSlashes(value: string): string {\n let end = value.length\n while (end > 0 && value.charCodeAt(end - 1) === 47) end -= 1\n return value.slice(0, end)\n}\n\nfunction trimSurroundingSlashes(value: string): string {\n let start = 0\n while (start < value.length && value.charCodeAt(start) === 47) start += 1\n return trimTrailingSlashes(value.slice(start))\n}\n\n/** The GitLab instance a spec's GitLab consumers address; an absent host means GitLab.com (§24.1). */\nexport function gitlabManagedHost(gitlabHost?: string): ManagedCredentialHost {\n const trimmed = trimTrailingSlashes((gitlabHost ?? '').trim())\n return { provider: 'gitlab', baseUrl: trimmed === '' ? GITLAB_COM_BASE_URL : trimmed }\n}\n\n/** The table one agent's git classifies against: GitHub plus the one GitLab instance its spec names. */\nexport function managedHostTableFor(gitlabHost?: string): ManagedCredentialHost[] {\n return [GITHUB_MANAGED_HOST, gitlabManagedHost(gitlabHost)]\n}\n\n/** `github=https://github.com gitlab=https://gitlab.example.test:8443/gitlab` — space separated,\n * which no absolute URL may contain. */\nexport function encodeManagedHostTable(hosts: readonly ManagedCredentialHost[]): string {\n return hosts.map((entry) => `${entry.provider}=${entry.baseUrl}`).join(' ')\n}\n\n/** Absent or unparseable ⇒ the default table, which is what a deployment on GitLab.com means. */\nexport function decodeManagedHostTable(raw: string | undefined): ManagedCredentialHost[] {\n const entries: ManagedCredentialHost[] = []\n for (const token of (raw ?? '').split(/\\s+/)) {\n const eq = token.indexOf('=')\n if (eq <= 0) continue\n const provider = token.slice(0, eq)\n const baseUrl = trimTrailingSlashes(token.slice(eq + 1))\n if (provider !== 'github' && provider !== 'gitlab') continue\n if (parseManagedBaseUrl(baseUrl) === undefined) continue\n entries.push({ provider, baseUrl })\n }\n return entries.length > 0 ? entries : managedHostTableFor()\n}\n\nexport interface ManagedBaseUrlParts {\n /** Lower-cased scheme without the colon, as git spells it on the credential `protocol` line. */\n protocol: string\n /** Lower-cased host including a non-default port, as git spells it on the `host` line. */\n host: string\n /** Path prefix without surrounding slashes; empty for an instance at the URL root. */\n pathPrefix: string\n}\n\nexport function parseManagedBaseUrl(baseUrl: string): ManagedBaseUrlParts | undefined {\n const match = /^([a-z][a-z0-9+.-]*):\\/\\/([^/?#\\s]+)(\\/[^?#\\s]*)?$/i.exec(baseUrl.trim())\n if (!match) return undefined\n return {\n protocol: match[1]!.toLowerCase(),\n host: match[2]!.toLowerCase(),\n pathPrefix: trimSurroundingSlashes(match[3] ?? '')\n }\n}\n\n/**\n * The request path with the entry's prefix removed on an EXACT segment boundary, or undefined when\n * the prefix does not apply — a second GitLab at another prefix on the same host is not ours.\n */\nexport function stripHostPathPrefix(path: string, pathPrefix: string): string | undefined {\n const cleaned = path.replace(/^\\/+/, '')\n if (pathPrefix === '') return cleaned\n if (!cleaned.startsWith(pathPrefix)) return undefined\n const rest = cleaned.slice(pathPrefix.length)\n if (rest === '') return ''\n if (!rest.startsWith('/')) return undefined\n return rest.replace(/^\\/+/, '')\n}\n\n/** What git hands a credential helper on stdin, as far as routing cares. */\nexport interface ManagedHostQuery {\n protocol?: string\n host?: string\n path?: string\n}\n\nexport interface ManagedHostMatch {\n entry: ManagedCredentialHost\n /** The request path with the entry's prefix stripped; absent when git sent no path. */\n path?: string\n}\n\n/**\n * The table entry a credential request belongs to — undefined means \"not ours\", and the caller must\n * stay silent rather than guess. Host comparison is exact, so a host that is a prefix or a suffix of\n * a managed one never matches; among matches the longest applicable path prefix wins.\n */\nexport function matchManagedHost(\n table: readonly ManagedCredentialHost[],\n query: ManagedHostQuery\n): ManagedHostMatch | undefined {\n const protocol = query.protocol?.toLowerCase()\n const host = query.host?.toLowerCase()\n if (host === undefined) return undefined\n let best: ManagedHostMatch | undefined\n let bestPrefixLength = -1\n for (const entry of table) {\n const parts = parseManagedBaseUrl(entry.baseUrl)\n if (!parts) continue\n if (parts.host !== host) continue\n if (protocol !== undefined && protocol !== parts.protocol) continue\n let path: string | undefined\n if (query.path !== undefined) {\n path = stripHostPathPrefix(query.path, parts.pathPrefix)\n if (path === undefined) continue\n } else if (parts.pathPrefix !== '') {\n // A prefixed install cannot be recognized without the path git was asked for.\n continue\n }\n if (parts.pathPrefix.length <= bestPrefixLength) continue\n bestPrefixLength = parts.pathPrefix.length\n best = { entry, ...(path !== undefined ? { path } : {}) }\n }\n return best\n}\n","/**\n * The git credential helper itself (docs/designs/github-app-git-credentials.md §Local Helper\n * Channel), independent of where it runs.\n *\n * Speaks git's credential-helper protocol on stdin/stdout and proxies to a gitcred socket with the\n * runtime-only agent capability; the token exists only on the reply pipe. Actions:\n * get → username=x-access-token / password=<token> (host must be github.com)\n * erase → forward to the daemon (invalidate the cached token git just had rejected)\n * store → no-op\n *\n * The `path` git sends (useHttpPath=true is part of the injection) ROUTES the\n * request since issue #457: it is parsed to \"owner/repo\" and forwarded, so an\n * authorized non-workspace repo gets ITS token (multi-repo design decision 5).\n * Path absent/unparseable ⇒ the workspace token, the pre-#457 behavior. This\n * is routing, not authorization — the CP gate decides; token scope enforces\n * the real boundary at GitHub either way.\n *\n * WHICH hosts are ours comes from the injected table (§24.4), never from two literals and never\n * from the agent's own environment as a hint: the daemon writes it beside the capability at\n * injection time, each entry carrying the full base URL, so a prefixed install's path prefix is\n * stripped before the project path is parsed.\n *\n * Exit style: on ANY failure print a human-actionable line to stderr and exit 1\n * — since #251 surfaces turn failures to end users, this text is user-facing.\n * NEVER print or log the token outside the protocol response.\n *\n * WHICH socket to dial is the caller's business, and that is the whole reason this is a leaf: the\n * daemon CLI derives it from its own root, while in a sandbox pod the same code dials the path the\n * shim serves the daemon's socket on. Its only imports are node builtins and the env names, because\n * the in-sandbox bundle is asserted to import nothing else.\n */\nimport { createConnection } from 'node:net'\nimport { GITCRED_AGENT_ENV, GITCRED_CAPABILITY_ENV } from './env.js'\nimport { decodeManagedHostTable, GITCRED_HOSTS_ENV, matchManagedHost } from './managed-hosts.js'\n\ninterface HelperInput {\n protocol?: string\n host?: string\n path?: string\n password?: string\n}\n\n/**\n * The argv id comes from a config file (`.git/config` helper line) that can\n * outlive the agent that wrote it — deleted + recreated under the same name,\n * the checkout survives with the DEAD id and the daemon denies every request.\n * The env identity is minted together with the capability at spawn/clone\n * (gitCredentialEnv), so when present it names the agent actually invoking\n * git and always matches the capability sent beside it.\n */\nexport function effectiveAgentId(argvAgentId: string): string {\n return process.env[GITCRED_AGENT_ENV] || argvAgentId\n}\n\nexport async function runGitCredential(action: string, agentId: string, socketPath: string): Promise<void> {\n if (action === 'store') return // git offers the accepted credential back — nothing to do\n\n agentId = effectiveAgentId(agentId)\n\n const input = parseStdin(await readStdin())\n const match = matchManagedHost(decodeManagedHostTable(process.env[GITCRED_HOSTS_ENV]), input)\n // GitLab keeps full subgroup depth from the instance's path prefix (§13.2); github stays owner/repo.\n const gitlab = match?.entry.provider === 'gitlab'\n const repo = match?.path === undefined ? undefined : gitlab ? projectFromPath(match.path) : repoFromPath(match.path)\n\n // Not ours — stay silent so git can try other helpers; an absent host still means the workspace.\n if (input.host !== undefined && match === undefined) return\n\n if (action === 'erase') {\n // Route the invalidation to the same (agent, repo) key the get used.\n await ipc(socketPath, {\n op: 'erase',\n agentId,\n capability: process.env[GITCRED_CAPABILITY_ENV],\n password: input.password,\n repoFullName: repo,\n ...(gitlab ? { provider: 'gitlab' } : {})\n }).catch(() => undefined) // best-effort\n return\n }\n if (action !== 'get') return // unknown actions are ignored per the helper contract\n\n const res = await ipc(socketPath, {\n op: 'get',\n agentId,\n capability: process.env[GITCRED_CAPABILITY_ENV],\n repoFullName: repo,\n ...(gitlab ? { provider: 'gitlab' } : {})\n })\n if (!res.ok || !res.username || !res.password) {\n process.stderr.write(\n `agentconnect: no git credentials for agent ${agentId}${repo ? ` on ${repo}` : ''}: ${res.error ?? 'unknown error'}\\n` +\n `(the daemon must be running and connected to the control plane)\\n`\n )\n process.exitCode = 1\n return\n }\n // Diagnostics-only sanity (the daemon guard already refuses cross-repo\n // grants): a mismatch here means an old daemon answered with the workspace\n // token — GitHub's single-repo token scope still enforces the boundary.\n if (repo !== undefined) {\n const want = normalizeRepoPath(res.repoFullName ?? '')\n if (want && want !== repo) {\n process.stderr.write(\n `agentconnect: note — git asked for ${repo} but the daemon answered for ${want} ` +\n `(daemon predates per-repo credentials?)\\n`\n )\n }\n }\n process.stdout.write(`username=${res.username}\\npassword=${res.password}\\n`)\n}\n\nfunction normalizeRepoPath(p: string): string {\n return p\n .replace(/^\\/+/, '')\n .replace(/\\.git$/i, '')\n .toLowerCase()\n}\n\n/** The full namespaced GitLab project path from git's credential `path` —\n * arbitrary subgroup depth, tolerating a leading slash, a `.git` suffix, and\n * LFS-ish subpaths (`group/sub/project.git/info/lfs`). */\nexport function projectFromPath(p: string): string | undefined {\n const cleaned = p.replace(/^\\/+/, '')\n const gitSuffix = cleaned.search(/\\.git(?:\\/|$)/i)\n const path = (gitSuffix >= 0 ? cleaned.slice(0, gitSuffix) : cleaned).replace(/\\/+$/, '')\n return path.includes('/') ? path.toLowerCase() : undefined\n}\n\n/** \"owner/repo\" from git's credential `path` — tolerates a leading slash, a\n * `.git` suffix, and LFS-ish subpaths (`owner/repo.git/info/lfs`). */\nexport function repoFromPath(p: string): string | undefined {\n const segs = p.replace(/^\\/+/, '').split('/')\n const owner = segs[0]\n const repo = segs[1]?.replace(/\\.git$/i, '')\n if (!owner || !repo) return undefined\n return `${owner}/${repo}`.toLowerCase()\n}\n\nfunction parseStdin(text: string): HelperInput {\n const out: Record<string, string> = {}\n for (const line of text.split('\\n')) {\n const eq = line.indexOf('=')\n if (eq > 0) out[line.slice(0, eq)] = line.slice(eq + 1)\n }\n return out\n}\n\nfunction readStdin(): Promise<string> {\n return new Promise((resolve) => {\n let buf = ''\n process.stdin.setEncoding('utf8')\n process.stdin.on('data', (c) => (buf += c))\n process.stdin.on('end', () => resolve(buf))\n process.stdin.on('error', () => resolve(buf))\n })\n}\n\ninterface IpcReply {\n ok: boolean\n username?: string\n password?: string\n repoFullName?: string\n error?: string\n}\n\nfunction ipc(path: string, msg: unknown): Promise<IpcReply> {\n return new Promise((resolve) => {\n const sock = createConnection(path)\n let buf = ''\n const fail = (error: string) => resolve({ ok: false, error })\n sock.setTimeout(15_000, () => {\n sock.destroy()\n fail('daemon did not answer in time')\n })\n sock.on('connect', () => sock.write(JSON.stringify(msg) + '\\n'))\n sock.on('data', (c) => {\n buf += c.toString('utf8')\n const nl = buf.indexOf('\\n')\n if (nl === -1) return\n sock.destroy()\n try {\n resolve(JSON.parse(buf.slice(0, nl)) as IpcReply)\n } catch {\n fail('malformed daemon reply')\n }\n })\n sock.on('error', (e) => fail(`cannot reach the daemon socket at ${path}: ${e.message}`))\n })\n}\n","/**\n * Paths the RUNTIME IMAGE fixes, as opposed to paths this daemon owns.\n *\n * They live in their own module because the distinction is the whole point: a daemon-derived path\n * means nothing inside a sandbox, and the bugs that come from mixing the two coordinate systems\n * are silent — git asks a credential helper that exists on a machine it is not on, and the failure\n * surfaces as an authentication error. Anything here has a counterpart in\n * `docker/runtime-sandbox.Dockerfile`, and changing one without the other breaks the pod.\n */\n\n/** The credential helper git runs inside the pod. Root-owned and read-only, like the shim. */\nexport const SANDBOX_GIT_CREDENTIAL_HELPER = '/opt/agentconnect/bin/git-credential'\n\n/** The gh wrapper's token fetch in the pod — the in-sandbox twin of the daemon's hidden `gh-token` subcommand. */\nexport const SANDBOX_GH_TOKEN_ENTRY = '/opt/agentconnect/shim/gh-token.js'\n\n/** The in-pod merge-when-ready watcher the shim spawns per armed pull request — one process, killed\n * on disarm and gone with the pod. Its presence is REPORTED by the automerge handler rather than\n * assumed: an image built before it ships none, and the daemon must read that skew, not guess. */\nexport const SANDBOX_AUTO_MERGE_ENTRY = '/opt/agentconnect/shim/auto-merge.js'\n\n/** The AgentConnect tool server the agent's harness spawns in the pod, reached over the `mcp` tunnel.\n * Reported to the daemon by the probe rather than assumed: an image built before it ships none. */\nexport const SANDBOX_MCP_BRIDGE_ENTRY = '/opt/agentconnect/shim/mcp-bridge.js'\n\n/** The ONLY image directory prepended to the runtime's PATH: the gh and agent-browser wrappers. */\n// Its own dir rather than reusing bin/ or shim/: those hold the credential helper and the runtime-table\n// generator, and neither should become a command an agent can run by name.\nexport const SANDBOX_GH_WRAPPER_DIR = '/opt/agentconnect/pathbin'\n\n/** Pod env naming the Chrome the image bakes — agent-browser's only browser-location hook, so an ACP child\n * without it downloads one of its own. Set by the image, projected onto the child by acp-runner. */\nexport const SANDBOX_BROWSER_EXECUTABLE_ENV = 'AGENT_BROWSER_EXECUTABLE_PATH'\n\n/** Where daemon-written, per-agent git configuration is materialized in the pod. Under /run rather\n * than the workspace volume: it is regenerated per launch and belongs to the POD, so a resumed\n * workspace must not carry a previous incarnation's copy. */\nexport const SANDBOX_GIT_CONFIG_DIR = '/run/agentconnect/git'\n\n/** Shim-owned scratch space for bounded skill snapshots; callers receive opaque handles only. */\nexport const SANDBOX_SKILL_STAGING_DIR = '/run/agentconnect/skills-staging'\n\n/**\n * Where a git-repo workspace is checked out, relative to the pod's workspace mount.\n *\n * A subdirectory rather than the mount itself, because the mount is also the runtime's HOME: a\n * checkout at the root would put the repository's working tree on top of `.claude`, `.codex` and\n * `.config`, where `git status` reports them as untracked and `git clean` would delete them. A\n * from-scratch workspace keeps using the root — it has no working tree to confuse with HOME, and\n * moving it would strand every volume already provisioned.\n */\nexport const SANDBOX_CHECKOUT_DIR = 'repo'\n\n/**\n * The daemon-side servers the shim serves locally, and the in-pod path of each.\n *\n * A plain record here rather than beside the tunnel's schemas, because the credential helper needs\n * the gitcred path and nothing else: importing it from a module that also holds zod schemas made\n * rolldown emit a chunk shared with the channel bundle — a third file the image never copies, and a\n * 136 KB one at that. `tunnel.ts` re-exports this typed against its own enum, so the two cannot\n * name different sets.\n */\nexport type SandboxTunnelName = 'gitcred' | 'mcp'\nexport const SANDBOX_TUNNEL_PATHS: Readonly<Record<SandboxTunnelName, string>> = Object.freeze({\n gitcred: '/run/agentconnect/gitcred.sock',\n mcp: '/run/agentconnect/mcp.sock'\n})\n\n/** The no-search DeepSeek Harness preset the image bakes (docker/runtime-sandbox/bake-dsh-preset.mjs),\n * which the shim copies into the pod's `$DSH_HOME/.agent-presets` before launching that runtime. Its\n * presence is CONSULTED rather than assumed: an image built before it ships none, and such a pod must\n * keep launching exactly as it always did. */\nexport const SANDBOX_DSH_PRESET_DIR = '/opt/agentconnect/dsh/agent-presets/standard-no-search'\n\n/** The preset id the directory above supplies — the roster reads it from the directory NAME, so this\n * is the same string as that path's last segment and the settings default the shim writes. */\nexport const SANDBOX_DSH_PRESET_ID = 'standard-no-search'\n","#!/usr/bin/env node\n/**\n * The in-sandbox git credential helper. Lives at a fixed path in the runtime image\n * (`/opt/agentconnect/bin/git-credential` wraps it), root-owned and read-only like the shim.\n *\n * Its own entry rather than a mode of the shim, for two reasons that point the same way. Git spawns\n * a credential helper once per operation, and the shim entry pulls in the WebSocket client — a cost\n * and a dependency graph this has no use for. And the image copies ONE file per bundle, so two\n * entries whose graphs are disjoint stay two single files, where a shared module would make\n * rolldown emit a chunk that never gets copied.\n *\n * It holds no policy: the daemon decides whether this agent may have a credential and for which\n * repository. What arrives here is the request git wrote on stdin and the capability the daemon\n * minted for this launch.\n */\nimport { GITCRED_SOCKET_ENV } from '../gitcred/env.js'\nimport { runGitCredential } from '../gitcred/helper.js'\nimport { SANDBOX_TUNNEL_PATHS } from './sandbox-paths.js'\n\nasync function main(): Promise<number> {\n // `<agentId> <action>`, positional and in that order: git APPENDS the action to whatever the\n // helper line named, so the id it was configured with comes first.\n const [agentId, action] = process.argv.slice(2)\n if (!action) {\n process.stderr.write('agentconnect: git-credential expects <agentId> <action>\\n')\n return 2\n }\n // The tunnel's path unless something names another; a pod has no daemon root to derive one from.\n const socketPath = process.env[GITCRED_SOCKET_ENV]?.trim() || SANDBOX_TUNNEL_PATHS.gitcred\n await runGitCredential(action, agentId ?? '', socketPath)\n // The helper reports a missing credential by setting exitCode, and that has to reach git: a zero\n // exit with no output reads as \"this helper has no opinion\" rather than as a failure.\n return typeof process.exitCode === 'number' ? process.exitCode : 0\n}\n\nmain().then(\n (code) => process.exit(code),\n (err: unknown) => {\n process.stderr.write(`agentconnect: git-credential failed: ${(err as Error).message}\\n`)\n process.exit(1)\n }\n)\n"],"mappings":"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"}
|
package/dist/shim/index.js
CHANGED
|
@@ -43154,8 +43154,11 @@ discriminatedUnion("event", [object({
|
|
|
43154
43154
|
/** The AgentConnect tool server the agent's harness spawns in the pod, reached over the `mcp` tunnel.
|
|
43155
43155
|
* Reported to the daemon by the probe rather than assumed: an image built before it ships none. */
|
|
43156
43156
|
const SANDBOX_MCP_BRIDGE_ENTRY = "/opt/agentconnect/shim/mcp-bridge.js";
|
|
43157
|
-
/** The ONLY image directory prepended to the runtime's PATH: the gh
|
|
43157
|
+
/** The ONLY image directory prepended to the runtime's PATH: the gh and agent-browser wrappers. */
|
|
43158
43158
|
const SANDBOX_GH_WRAPPER_DIR = "/opt/agentconnect/pathbin";
|
|
43159
|
+
/** Pod env naming the Chrome the image bakes — agent-browser's only browser-location hook, so an ACP child
|
|
43160
|
+
* without it downloads one of its own. Set by the image, projected onto the child by acp-runner. */
|
|
43161
|
+
const SANDBOX_BROWSER_EXECUTABLE_ENV = "AGENT_BROWSER_EXECUTABLE_PATH";
|
|
43159
43162
|
/** Shim-owned scratch space for bounded skill snapshots; callers receive opaque handles only. */
|
|
43160
43163
|
const SANDBOX_SKILL_STAGING_DIR = "/run/agentconnect/skills-staging";
|
|
43161
43164
|
const SANDBOX_TUNNEL_PATHS$1 = Object.freeze({
|
|
@@ -43393,6 +43396,8 @@ var AcpRunner = class {
|
|
|
43393
43396
|
}
|
|
43394
43397
|
const podEnv = this.deps.podEnv ?? {};
|
|
43395
43398
|
for (const [name, value] of Object.entries(sandboxProviderEnv(payload.command, podEnv))) if (!env[name]) env[name] = value;
|
|
43399
|
+
const bakedBrowser = podEnv[SANDBOX_BROWSER_EXECUTABLE_ENV];
|
|
43400
|
+
if (bakedBrowser && !env["AGENT_BROWSER_EXECUTABLE_PATH"]) env[SANDBOX_BROWSER_EXECUTABLE_ENV] = bakedBrowser;
|
|
43396
43401
|
if (sandboxProfile(payload.command) === "codex") {
|
|
43397
43402
|
fillInCodexConfigFloor(env, podEnv, (message) => this.deps.log?.warn(message));
|
|
43398
43403
|
fillInCodexBaseUrl(env, podEnv, (message) => this.deps.log?.warn(message));
|