@aion0/forge 0.10.71 → 0.10.74

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/RELEASE_NOTES.md CHANGED
@@ -1,11 +1,8 @@
1
- # Forge v0.10.71
1
+ # Forge v0.10.74
2
2
 
3
3
  Released: 2026-06-11
4
4
 
5
- ## Changes since v0.10.70
5
+ ## Changes since v0.10.73
6
6
 
7
- ### Other
8
- - fix(auth): persist AUTH_SECRET so sessions survive refresh/restart
9
7
 
10
-
11
- **Full Changelog**: https://github.com/aiwatching/forge/compare/v0.10.70...v0.10.71
8
+ **Full Changelog**: https://github.com/aiwatching/forge/compare/v0.10.73...v0.10.74
@@ -217,7 +217,7 @@ export default function EnterpriseBadge({ onOpenSettings: _onOpenSettings }: { o
217
217
  ok: boolean;
218
218
  steps_completed?: number;
219
219
  error?: string;
220
- post_login_terminal?: Array<{ name: string; ok: boolean; command?: string; error?: string }>;
220
+ post_login_terminal?: Array<{ name: string; ok: boolean; command?: string; error?: string; skipped?: boolean }>;
221
221
  }>) {
222
222
  if (entry.ok) {
223
223
  parts.push(`✓ ${entry.idp_host} (${entry.steps_completed || 0} steps)`);
@@ -229,6 +229,10 @@ export default function EnterpriseBadge({ onOpenSettings: _onOpenSettings }: { o
229
229
  }
230
230
  if (Array.isArray(entry.post_login_terminal)) {
231
231
  for (const r of entry.post_login_terminal) {
232
+ if (r.skipped) {
233
+ parts.push(`${r.name} ⊘ (n/a on this platform)`);
234
+ continue;
235
+ }
232
236
  parts.push(r.ok ? `${r.name} ✓` : `${r.name}: ${r.error || 'skipped'}`);
233
237
  if (!r.ok && r.command) {
234
238
  failedTerminals.push({ name: r.name, command: r.command, error: r.error });
@@ -0,0 +1,256 @@
1
+ # SSH → PAT migration: scope, what changed where, what's still needed
2
+
3
+ **Author**: Z + Claude · **Date**: 2026-06-12 · **Audience**: Forge / forge-connectors maintainers
4
+
5
+ ## Why this doc exists
6
+
7
+ While integrating Forge against the Fortinet corp GitLab inside a Docker
8
+ sandbox, we ran into the warning
9
+
10
+ ```
11
+ ✓ fac.corp.fortinet.com (3 steps, final=https://...)
12
+ · GitLab SSH 2FA: Keystroke mode is macOS-only.
13
+ ```
14
+
15
+ and a related glab error path. We sorted out what's actually broken vs
16
+ what just looks broken, and made a narrow fix in the sandbox repo
17
+ (corp-workspace). **A separate fix is still required in the Forge ↔
18
+ forge-connectors stack to benefit users who run Forge directly on
19
+ their machines.**
20
+
21
+ This doc lays out the full picture so the right person can decide
22
+ whether and how to land the second fix.
23
+
24
+ ---
25
+
26
+ ## 1. The three different things people mean by "SSH"
27
+
28
+ It's easy to conflate, but they are independent code paths:
29
+
30
+ | # | Thing | Who uses it | What was/is the issue |
31
+ |---|---|---|---|
32
+ | A | `git push ssh://git@…` from a Forge-managed clone | Pipeline shell steps | **Never an issue.** Forge's `connectorEnv()` injects `GITLAB_TOKEN` and clones with `https://oauth2:<PAT>@…` in `.git/config`. Push was already HTTPS+PAT. |
33
+ | B | `glab repo clone foo/bar` (convenience CLI) | Dev typing in workspace terminal; future workflow nodes | glab CLI's stock config has `git_protocol: ssh`. With no SSH key in the container that fails opaquely. Trivial to fix per workspace, but easy to forget. |
34
+ | C | `ssh git@dops-git106.fortinet-us.com 2fa_verify` post-IdP-login step | Triggered by Fortinet's `_idp` wizard after a successful FAC SAML login. Implemented by `lib/auth/terminal-keystroke.ts`. | The implementation **only works on macOS** because it uses `osascript` to type into the user's frontmost Terminal.app (the only PID that FortiClient lets through the corp tunnel). On Linux/container/Windows it returns `Keystroke mode is macOS-only.` and the IdP login shows a yellow warning. |
35
+
36
+ ### Why each path exists
37
+
38
+ - **A** exists because git push must work end-to-end for any agent that
39
+ edits + commits. Forge's framework already handles this transparently.
40
+
41
+ - **B** exists because dev workflow is more natural with `glab repo clone`
42
+ than `git clone $(glab repo view --url)`. Default protocol matters.
43
+
44
+ - **C** exists because FortiClient VPN gates each TCP session by the
45
+ process tree of the PID that opened it. Forge-spawned processes are
46
+ not in the user's whitelisted PID, so they can't reach the internal
47
+ GitLab over SSH. AppleScript-typing the command into the user's
48
+ frontmost Terminal.app makes it a direct child of the
49
+ user-whitelisted PID. Hence the macOS dependency.
50
+
51
+ ---
52
+
53
+ ## 2. What was changed in corp-workspace this round
54
+
55
+ **Only B** has been fixed in the corp-workspace repo:
56
+
57
+ ```diff
58
+ # ─── Git provider CLIs (gh + glab) ──────────────────────────────────────
59
+ ...
60
+ gh --version; glab --version
61
+ +
62
+ +USER neko
63
+ +RUN mkdir -p /home/neko/.config/glab-cli \
64
+ + && glab config set -g git_protocol https
65
+ +USER root
66
+ ```
67
+
68
+ Net effect inside any container built from this image: `glab repo clone`
69
+ defaults to HTTPS URLs and the in-env GITLAB_TOKEN authenticates them.
70
+ No effect on Forge backend, no effect on A or C.
71
+
72
+ The architecture-decisions doc in corp-workspace was updated with a §15
73
+ section that documents the SSH-removal stance.
74
+
75
+ ---
76
+
77
+ ## 3. What's NOT fixed by anything in corp-workspace
78
+
79
+ **C — the macOS-only post-login terminal step.** That warning still
80
+ fires every time a non-Mac (or sandboxed-Mac) Forge instance walks the
81
+ Fortinet IdP flow. It looks like a real failure to users seeing it for
82
+ the first time.
83
+
84
+ This needs fixing in TWO upstream places, one optional:
85
+
86
+ ### Required: `aiwatching/forge-connectors` — `enterprise-fortinet/wizards/fortinac.json`
87
+
88
+ Either remove the `post_login_terminal` array, or annotate it as
89
+ platform-specific. We recommend keeping it but tagging:
90
+
91
+ ```diff
92
+ {
93
+ "host": "fac.corp.fortinet.com",
94
+ "saml_sps": ["mantis","pmdb","tp","teams"],
95
+ "post_login_terminal": [
96
+ {
97
+ "name": "GitLab SSH 2FA",
98
+ "command": "ssh git@dops-git106.fortinet-us.com 2fa_verify",
99
+ "needs_otp": true,
100
+ + "platforms": ["darwin"],
101
+ "valid_minutes": 55
102
+ }
103
+ ]
104
+ }
105
+ ```
106
+
107
+ The semantics we want:
108
+ - macOS user running Forge natively → step runs as today
109
+ - macOS user running Forge in container → step skipped (no warning)
110
+ - Linux/Windows user → step skipped (no warning)
111
+
112
+ ### Optional but needed to honor the tag: `@aion0/forge` (Forge backend)
113
+
114
+ Only required if we go with the `platforms` annotation (option B in
115
+ corp-workspace's analysis). Without backend support the new key is
116
+ silently ignored — the step still runs, still fails on non-darwin,
117
+ warning still shows. The patch is small:
118
+
119
+ ```diff
120
+ // lib/auth/idp-login.ts (the loop that iterates post_login_terminal)
121
+ const results: TerminalResult[] = [];
122
+ for (const t of (block.post_login_terminal || [])) {
123
+ + // Wizard authors can scope an entry to specific platforms; on
124
+ + // others, skip silently rather than emit a noisy "macOS-only"
125
+ + // warning that confuses container/Linux users.
126
+ + if (Array.isArray(t.platforms) && t.platforms.length > 0
127
+ + && !t.platforms.includes(process.platform)) {
128
+ + continue;
129
+ + }
130
+ results.push(await runTerminalKeystroke(t, otp));
131
+ }
132
+ ```
133
+
134
+ Plus extending the `TerminalSpec` interface to include
135
+ `platforms?: NodeJS.Platform[]`.
136
+
137
+ ### Easier alternative: don't add a new field, just drop the step entirely
138
+
139
+ Replace the wizard's `post_login_terminal` entry with `[]` or remove
140
+ the key. Justification:
141
+
142
+ - The step exists to refresh GitLab's per-IP 2FA window for SSH push.
143
+ - Forge agent pushes via HTTPS+PAT (path A). Doesn't need 2FA refresh.
144
+ - Devs pushing from their host machine over SSH already type
145
+ `ssh ... 2fa_verify` themselves; auto-running it during SSO login
146
+ is convenience, not correctness.
147
+
148
+ Trade-off: macOS dev who got used to the auto-2fa convenience loses
149
+ that. They now have to type the command themselves once per 55 min if
150
+ they do SSH pushes.
151
+
152
+ Our recommendation: **start with the annotation approach (preserves
153
+ existing behavior for the small set of macOS-native users)**. Drop the
154
+ step entirely only if telemetry / feedback shows nobody actually relies
155
+ on it.
156
+
157
+ ---
158
+
159
+ ## 4. Decision matrix — impact of each option
160
+
161
+ | Change | Lines touched | Mac native (Forge on host) | Mac docker (corp-workspace) | Linux server (Forge native or docker) |
162
+ |---|---|---|---|---|
163
+ | nothing (status quo) | 0 | ✓ auto 2fa_verify works | ⚠️ warning shown | ⚠️ warning shown |
164
+ | corp-workspace Dockerfile only (done) | ~3 | (no change) | (no change for C; B fixed) | (no change for C; B fixed if they use the same image) |
165
+ | **forge-connectors + Forge backend, annotated** | ~10 | ✓ unchanged | ✓ silently skipped | ✓ silently skipped |
166
+ | **forge-connectors only, remove step** | ~5 | ✗ Mac native loses auto-2fa | ✓ silent | ✓ silent |
167
+
168
+ ---
169
+
170
+ ## 5. Open questions for the team
171
+
172
+ 1. **Is the auto-2fa convenience valuable enough to keep for macOS
173
+ natives?** If yes → go with annotation. If most macOS users actually
174
+ work in containers / Linux servers and rarely SSH-push directly, the
175
+ annotation adds a config knob nobody will ever change → drop the
176
+ step.
177
+
178
+ 2. **Are there other `post_login_terminal` use cases (or future ones)
179
+ that benefit from platform tagging?** If yes → annotation gives us
180
+ the lever. If no → just delete.
181
+
182
+ 3. **Should we also document a way to set up SSH inside the container?**
183
+ Some power user might want to mount their host SSH key into the
184
+ container for personal-signed git commits. We don't need to enable
185
+ it by default — just document it works if you bind-mount
186
+ `~/.ssh:/home/neko/.ssh:ro`.
187
+
188
+ ---
189
+
190
+ ## 6. Suggested action sequence
191
+
192
+ If you decide to land the annotation approach:
193
+
194
+ 1. **`@aion0/forge`** (Forge backend repo)
195
+ - Patch `lib/auth/idp-login.ts` per §3 above.
196
+ - Extend `TerminalSpec` in `lib/auth/terminal-keystroke.ts` with
197
+ `platforms?: NodeJS.Platform[]`.
198
+ - Add changelog entry.
199
+ - Bump patch version (e.g. 0.10.69 → 0.10.70). Publish.
200
+
201
+ 2. **`aiwatching/forge-connectors`** (wizard JSON repo)
202
+ - Open `enterprise-fortinet/wizards/fortinac.json`.
203
+ - Add `"platforms": ["darwin"]` to the FAC entry's
204
+ `post_login_terminal[0]`.
205
+ - Commit + push to `main`.
206
+
207
+ 3. **Verification path**
208
+ - In corp-workspace: `make sync-registry U=zliu` to pull new wizard.
209
+ Then `make login U=zliu`. Result: no more `GitLab SSH 2FA:
210
+ Keystroke mode is macOS-only.` warning in the output.
211
+ - In a separate macOS-native Forge install: `make sync-registry`
212
+ equivalent. Then trigger IdP login. Result: warning gone for
213
+ non-darwin, but on macOS native the AppleScript path still runs.
214
+
215
+ 4. (Optional) Land a follow-up to corp-workspace updating §15 to
216
+ reference the new platform-aware behavior so future readers
217
+ understand both halves of the fix.
218
+
219
+ If you instead decide to drop the step entirely, you only need step (2)
220
+ and don't have to bump Forge backend.
221
+
222
+ ---
223
+
224
+ ## 7. Outcome (2026-06-12)
225
+
226
+ All landed on branch `analysis/ssh-to-pat-migration` (Forge) + `main`
227
+ (forge-enterprise-agent-fortinet). Corrections and final state:
228
+
229
+ - **Location correction**: the wizard JSON lives in
230
+ `forge-enterprise-agent-fortinet/templates/wizards/fortinac.json`, not
231
+ `aiwatching/forge-connectors` as §3 said. The public forge-connectors
232
+ repo carries no wizard / `post_login_terminal` anywhere — verified, so
233
+ nothing to change there (zero blast radius for non-fortinet users).
234
+
235
+ - **Both options were taken, in sequence**:
236
+ 1. Annotation mechanism landed in Forge (`TerminalSpec.platforms`,
237
+ silent `skipped: true` + ⊘ in EnterpriseBadge) and the wizard was
238
+ tagged `"platforms": ["darwin"]`.
239
+ 2. After end-to-end validation that pipeline `git push` works over
240
+ HTTPS+PAT, the wizard's `post_login_terminal` entry was **deleted
241
+ entirely**. The `platforms` mechanism stays in Forge for future
242
+ wizard steps.
243
+
244
+ - **Beyond the doc's scope, the real root fix**: path A was *not* fully
245
+ "never an issue" — since v0.10.68/72 Forge prefers the user's local
246
+ checkout, whose origin may be SSH-style. Forge now injects
247
+ `GIT_CONFIG_COUNT/KEY/VALUE` insteadOf rules (git ≥ 2.31) via
248
+ `connectorEnv()` so `git@host:` / `ssh://git@host/` remotes rewrite to
249
+ `https://oauth2:<PAT>@host/` per task process. Empty token → no
250
+ injection → SSH behavior preserved as the backup path. Optional
251
+ `gitlab.config.ssh_aliases` covers SSH-host ≠ HTTPS-host setups.
252
+ `preflightGitAuth` inherits the same env so the probe matches push
253
+ behavior. `lib/init.ts` also defaults glab to `git_protocol https`.
254
+
255
+ - corp-workspace `architecture-decisions.md` §15 updated to reflect the
256
+ step removal (the §6.4 follow-up).
@@ -24,6 +24,9 @@ export interface TerminalSpec {
24
24
  prompt_markers?: string[];
25
25
  /** Validity window (informational only, surfaced to UI). */
26
26
  valid_minutes?: number;
27
+ /** When set, only run on these platforms; on others, silently skip
28
+ * instead of emitting the "macOS-only" warning. */
29
+ platforms?: NodeJS.Platform[];
27
30
  /** Regex (case-insensitive). If transcript matches, force verdict=fail
28
31
  * even when exit code is 0. Needed when the command prints failure
29
32
  * text to stdout without setting a non-zero exit code (e.g. GitLab
@@ -42,6 +45,9 @@ export interface TerminalResult {
42
45
  command: string;
43
46
  error?: string;
44
47
  transcript?: string;
48
+ /** Set when a `platforms` spec excluded the current OS — UI should
49
+ * treat this as "not applicable", not as failure. */
50
+ skipped?: boolean;
45
51
  }
46
52
 
47
53
  const DEFAULT_PROMPT_MARKERS = ['OTP', 'Token', 'verification', 'code:'];
@@ -71,6 +77,13 @@ export async function runTerminalKeystroke(
71
77
  spec: TerminalSpec,
72
78
  otp?: string,
73
79
  ): Promise<TerminalResult> {
80
+ // Wizard authors can scope to specific platforms; on others, silently
81
+ // skip so non-darwin Forge instances don't see a noisy "macOS-only"
82
+ // warning for steps that simply aren't applicable to them.
83
+ if (Array.isArray(spec.platforms) && spec.platforms.length > 0
84
+ && !spec.platforms.includes(process.platform)) {
85
+ return { name: spec.name, command: spec.command, ok: true, skipped: true };
86
+ }
74
87
  if (process.platform !== 'darwin') {
75
88
  return { name: spec.name, command: spec.command, ok: false, error: 'Keystroke mode is macOS-only.' };
76
89
  }
@@ -73,6 +73,12 @@ nodes: { ... }
73
73
  are ignored — only a genuine OTP marker blocks. Those still surface at push
74
74
  time as before.
75
75
 
76
+ > **Note**: with a GitLab connector PAT configured, SSH-style origins are
77
+ > transparently rewritten to HTTPS+PAT for both this preflight and the actual
78
+ > push (see *GitLab connector — git push avoids SSH 2FA* in `17-connectors.md`),
79
+ > so the OTP wall is normally never hit. The preflight remains as the safety
80
+ > net for the no-PAT / SSH-fallback path.
81
+
76
82
  ## Auth-required alerts (git OTP + expired connector logins)
77
83
 
78
84
  When a `git_push` pipeline hits the OTP wall, **or** any pipeline fails on an
@@ -23,6 +23,18 @@ Fix AUTH_SECRET so it persists:
23
23
  echo "AUTH_SECRET=$(openssl rand -hex 32)" >> ~/.forge/data/.env.local
24
24
  ```
25
25
 
26
+ ### Pipeline push asks for OTP / "2fa_verify" / "Keystroke mode is macOS-only"
27
+ Pipeline `git push` should never hit an SSH 2FA wall when the GitLab
28
+ connector has a PAT: Forge rewrites SSH-style remotes
29
+ (`git@host:owner/repo.git`) to HTTPS+PAT per task process (needs git ≥ 2.31).
30
+ If you still see a 2FA abort:
31
+ - Check Settings → Connectors → GitLab has a valid token.
32
+ - If your GitLab's SSH hostname differs from the HTTPS `base_url` host, add
33
+ it to the connector's `ssh_aliases`.
34
+ - An old wizard may still carry a "GitLab SSH 2FA" post-login step — resync
35
+ the enterprise marketplace to pull the updated wizard; the warning row
36
+ disappears from the Enterprise badge.
37
+
26
38
  ### Orphan processes after Ctrl+C
27
39
  Use `forge server stop` or:
28
40
  ```bash
@@ -413,6 +413,22 @@ Each prompt may include `url` + `url_label`, surfaced in the modal as
413
413
  a "↗ Get token" link next to the field so the user can jump straight
414
414
  to the page that issues the value.
415
415
 
416
+ ## GitLab connector — git push avoids SSH 2FA
417
+
418
+ When the GitLab connector has a PAT, Forge injects per-process git config
419
+ into every task it spawns (`GIT_CONFIG_COUNT/KEY/VALUE`, git ≥ 2.31) so
420
+ SSH-style remotes (`git@host:owner/repo.git`) are rewritten to
421
+ `https://oauth2:<PAT>@host/owner/repo.git` at push time. Effect: pipeline
422
+ `git push` nodes use the PAT and never trigger `ssh ... 2fa_verify`.
423
+
424
+ - No write to `~/.gitconfig` or the repo's `.git/config` — the rewrite is
425
+ scoped to the task process and its children only.
426
+ - Empty token → no rewrite injected → SSH behavior is preserved as a
427
+ backup path (useful if PAT use ever gets restricted).
428
+ - If the SSH host differs from the HTTPS `base_url` host, add the SSH
429
+ hostname(s) to the gitlab config's optional `ssh_aliases` array — Forge
430
+ rewrites those too.
431
+
416
432
  ## Migration from pre-v0.9
417
433
 
418
434
  Pre-v0.9 Forge stored connectors as built-in plugins under
package/lib/init.ts CHANGED
@@ -115,6 +115,19 @@ export function ensureInitialized() {
115
115
  catch (e) { console.warn('[init] startScratchCleanup failed:', (e as Error).message); }
116
116
  });
117
117
  time('autoDetectAgents', autoDetectAgents);
118
+ time('ensureGlabHttps', () => {
119
+ // Default glab CLI to HTTPS so `glab repo clone` uses the connector PAT
120
+ // instead of SSH (which would hit ssh 2fa_verify). Best-effort; absent
121
+ // glab or read-only config dir just skip.
122
+ try {
123
+ const { execSync } = require('node:child_process');
124
+ const cur = execSync('glab config get git_protocol 2>/dev/null', { encoding: 'utf-8', timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'] }).trim();
125
+ if (cur && cur !== 'https') {
126
+ execSync('glab config set -g git_protocol https', { timeout: 3000, stdio: ['pipe', 'pipe', 'pipe'] });
127
+ console.log(`[init] glab git_protocol: ${cur} → https`);
128
+ }
129
+ } catch {}
130
+ });
118
131
  time('logToolStatus', () => {
119
132
  try { const { logToolStatus } = require('./health'); logToolStatus(); }
120
133
  catch (e) { console.warn('[tools] health check failed:', (e as Error).message); }
package/lib/pipeline.ts CHANGED
@@ -842,11 +842,16 @@ function preflightGitAuth(pipeline: Pipeline): { message: string; action?: strin
842
842
  if (!existsSync(join(projectPath, '.git'))) return null;
843
843
 
844
844
  try {
845
+ // Inherit connectorEnv (GIT_CONFIG_* insteadOf rewrite) so a user-cloned
846
+ // SSH origin probes via HTTPS+PAT instead of hitting ssh 2fa_verify —
847
+ // same rewrite task-manager applies to pipeline shell nodes.
848
+ const { connectorEnv } = require('./task-manager') as typeof import('./task-manager');
845
849
  execSync('git ls-remote --heads origin', {
846
850
  cwd: projectPath,
847
851
  stdio: 'pipe',
848
852
  timeout: 15_000,
849
853
  encoding: 'utf8',
854
+ env: { ...process.env, ...connectorEnv() },
850
855
  });
851
856
  return null; // remote reachable, no OTP wall
852
857
  } catch (e) {
package/lib/projects.ts CHANGED
@@ -386,15 +386,25 @@ export function resolveOrCloneProject(name: string | undefined): ResolveResult {
386
386
  // 2. `name` is empty / unknown — pull the gitlab connector's
387
387
  // `default_project_path` (e.g. "fortinet/fortinac-dev") and use
388
388
  // it as the fallback repo.
389
- // Either way we clone into <dataDir>/cloned-projects/ and reuse on
390
- // subsequent runs. The previous behaviour silently dropped to scratch
391
- // and code-fix pipelines exploded with "fatal: not a git repository".
389
+ // Before cloning, scan local projectRoots for a checkout whose git
390
+ // origin already matches saves a redundant clone and also covers the
391
+ // "server just booted, registry/settings cache not warm yet" window
392
+ // where the user's real local FortiNAC was missed by the name-based
393
+ // lookups above.
392
394
  const gl = readGitlabConnector();
393
395
  if (gl) {
394
396
  const targetPath = trimmed && trimmed.includes('/')
395
397
  ? trimmed
396
398
  : (gl.default_project_path || '').trim();
397
399
  if (targetPath) {
400
+ const projects = scanProjects();
401
+ const want = normalizeRepoPath(targetPath) || targetPath.toLowerCase().replace(/^\/+|\/+$/g, '');
402
+ const wantBase = want.split('/').pop()!;
403
+ const byRepo = projects.filter((p) => p.repo && p.repo === want);
404
+ if (byRepo.length === 1) return { project: byRepo[0], source: 'existing' };
405
+ const byRepoBase = projects.filter((p) => p.repo && p.repo.split('/').pop() === wantBase);
406
+ if (byRepoBase.length === 1) return { project: byRepoBase[0], source: 'existing' };
407
+
398
408
  const cloned = tryGitlabClone(targetPath);
399
409
  if (cloned) return { project: cloned, source: 'gitlab-cloned', clone_url: `${gl.base_url}/${targetPath.replace(/^\/+|\/+$/g, '')}.git` };
400
410
  }
@@ -440,7 +440,7 @@ async function processNextTask() {
440
440
  * Only well-known connectors map to env vars. Token never leaves the
441
441
  * server — the bash child inherits the env directly.
442
442
  */
443
- function connectorEnv(): Record<string, string> {
443
+ export function connectorEnv(): Record<string, string> {
444
444
  try {
445
445
  const out: Record<string, string> = {};
446
446
  const gitlab = getInstalledConnector('gitlab');
@@ -450,13 +450,40 @@ function connectorEnv(): Record<string, string> {
450
450
  // glab CLI honours GITLAB_TOKEN; HTTP libs honour CI_JOB_TOKEN-style names too,
451
451
  // but GITLAB_TOKEN covers glab + most curl-based scripts in our pipelines.
452
452
  out.GITLAB_TOKEN = tok;
453
+ let httpsBase = '';
454
+ let httpsHost = '';
453
455
  if (typeof gitlab.config?.base_url === 'string' && gitlab.config.base_url) {
454
- // glab uses GITLAB_URI for the API host on self-hosted instances.
455
456
  try {
456
457
  const u = new URL(String(gitlab.config.base_url));
457
458
  out.GITLAB_URI = u.origin;
459
+ httpsBase = u.origin.replace(/\/+$/, '');
460
+ httpsHost = u.host;
458
461
  } catch {}
459
462
  }
463
+ // Transparent SSH→HTTPS rewrite for pipeline `git push`. Without this,
464
+ // a user-cloned `git@host:owner/repo.git` origin pushes over SSH and
465
+ // hits 2fa_verify even though we have a PAT. Per-process via git's
466
+ // GIT_CONFIG_COUNT/KEY/VALUE env triplet (git ≥ 2.31) — no write to
467
+ // ~/.gitconfig or the repo, no SSH leak when token is absent.
468
+ if (httpsBase && httpsHost) {
469
+ const aliases: string[] = Array.isArray(gitlab.config?.ssh_aliases)
470
+ ? gitlab.config.ssh_aliases.filter((s: unknown) => typeof s === 'string' && s.trim()).map((s: string) => s.trim())
471
+ : [];
472
+ const hosts = Array.from(new Set([httpsHost, ...aliases]));
473
+ const target = `${httpsBase}/`.replace(/^https?:\/\//, (m) => `${m}oauth2:${encodeURIComponent(tok)}@`);
474
+ const rules: Array<[string, string]> = [];
475
+ for (const h of hosts) {
476
+ rules.push([`url.${target}.insteadOf`, `git@${h}:`]);
477
+ rules.push([`url.${target}.insteadOf`, `ssh://git@${h}/`]);
478
+ }
479
+ if (rules.length > 0) {
480
+ out.GIT_CONFIG_COUNT = String(rules.length);
481
+ rules.forEach(([k, v], i) => {
482
+ out[`GIT_CONFIG_KEY_${i}`] = k;
483
+ out[`GIT_CONFIG_VALUE_${i}`] = v;
484
+ });
485
+ }
486
+ }
460
487
  }
461
488
  }
462
489
  const gh = getInstalledConnector('github-api');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aion0/forge",
3
- "version": "0.10.71",
3
+ "version": "0.10.74",
4
4
  "description": "Unified AI workflow platform — multi-model task orchestration, persistent sessions, web terminal, remote access",
5
5
  "type": "module",
6
6
  "scripts": {