@cmmd-center/forge 0.13.11 → 0.13.13

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.
Files changed (53) hide show
  1. package/dist/{BunHttpServer-C7-1JJ7b.cjs → BunHttpServer-C0oPP_3A.cjs} +4 -4
  2. package/dist/{BunHttpServer-CVPGkizx.mjs → BunHttpServer-FbbQmsl_.mjs} +4 -4
  3. package/dist/{BunServices-CPrZSIfE.mjs → BunServices-DC7nhA3w.mjs} +2 -2
  4. package/dist/{BunServices-Dl-3897e.cjs → BunServices-DkenwD7S.cjs} +2 -2
  5. package/dist/{Cache-Cf3ARNaS.cjs → Cache-BmGc_2BZ.cjs} +2 -2
  6. package/dist/{Cache-C9cwdCDt.mjs → Cache-D57GWb9-.mjs} +2 -2
  7. package/dist/{HttpServer-CggZzScj.cjs → HttpServer-BBzRBmVt.cjs} +2 -2
  8. package/dist/{HttpServer-COaBeM7-.mjs → HttpServer-Bclwvu0I.mjs} +2 -2
  9. package/dist/{NodeHttpServer-UQnvZnmu.mjs → NodeHttpServer-BJ4l15f-.mjs} +5 -5
  10. package/dist/{NodeHttpServer-Bi339XFk.cjs → NodeHttpServer-Cjd3bFrD.cjs} +5 -5
  11. package/dist/{NodePTY-UOaRyQsj.cjs → NodePTY-BehazUMi.cjs} +2 -2
  12. package/dist/{NodePTY-B1u6RHXk.mjs → NodePTY-BhezYy9H.mjs} +2 -2
  13. package/dist/{NodeServices-R0BZsZ72.cjs → NodeServices-BvdIc9Vh.cjs} +1 -1
  14. package/dist/{NodeServices-CKFIZVu5.cjs → NodeServices-CtC4a-h5.cjs} +2 -2
  15. package/dist/{NodeServices-z1Ac4lEO.mjs → NodeServices-D3IxOzHl.mjs} +2 -2
  16. package/dist/{NodeSqliteClient-CyJvo9y9.cjs → NodeSqliteClient-CfsdC3AB.cjs} +4 -4
  17. package/dist/{NodeSqliteClient-BrzqdOsF.mjs → NodeSqliteClient-md8xd9hY.mjs} +4 -4
  18. package/dist/{NodeTerminal-pYVVRTiD.mjs → NodeTerminal-9iHZIdt1.mjs} +2 -2
  19. package/dist/{NodeTerminal-B89MobLp.cjs → NodeTerminal-CO-i-6Je.cjs} +2 -2
  20. package/dist/{Path-BunGrE8F.cjs → Path-BQOH_fWQ.cjs} +7 -1
  21. package/dist/{Path-evOm-H0Q.mjs → Path-CZJ1gPPo.mjs} +2 -2
  22. package/dist/{SqlError-CezTFGvr.mjs → SqlError-BMBkezUO.mjs} +2 -2
  23. package/dist/{SqlError-1wIMjXAf.cjs → SqlError-g4pLzNWR.cjs} +2 -2
  24. package/dist/{SqliteClient-D3Yd3ZHn.mjs → SqliteClient-BKB695RF.mjs} +3 -3
  25. package/dist/{SqliteClient-bo7iui1t.cjs → SqliteClient-O1ABIngw.cjs} +3 -3
  26. package/dist/bin.cjs +1451 -233
  27. package/dist/bin.mjs +1470 -260
  28. package/dist/client/assets/{CanvasPanel-z0F-jdf1.js → CanvasPanel-RpD9Eh_S.js} +2 -2
  29. package/dist/client/assets/{CompositeItem-P9UVXCKv.js → CompositeItem-DEIBUHJR.js} +2 -2
  30. package/dist/client/assets/{DiffPanel-BrlSm5kH.js → DiffPanel-cwm7LEtZ.js} +2 -2
  31. package/dist/client/assets/{DiffPanel.logic-OZhO8GfQ.js → DiffPanel.logic-B9AWYFxE.js} +2 -2
  32. package/dist/client/assets/{DiffPanelShell-DnugngHM.js → DiffPanelShell-BhokrrDZ.js} +112 -112
  33. package/dist/client/assets/{DiffWorkerPoolProvider-BOD7cQPe.js → DiffWorkerPoolProvider-BZjEmEzL.js} +2 -2
  34. package/dist/client/assets/{PullRequestCodePanel-DACYpcbJ.js → PullRequestCodePanel-BPJINzCp.js} +2 -2
  35. package/dist/client/assets/{Virtualizer-R_LG4CvG.js → Virtualizer-i6HiQ7Qm.js} +2 -2
  36. package/dist/client/assets/{canvasStateStore-CdQxd2aO.js → canvasStateStore-NrAxDqvM.js} +2 -2
  37. package/dist/client/assets/cmmdAuthRedirect-BahlX0gF.js +2 -0
  38. package/dist/client/assets/{index-BXzYKPmD.js → index-DMWfw0Ih.js} +61 -61
  39. package/dist/client/assets/index-dEPiAWVG.css +1 -0
  40. package/dist/client/assets/{popover-CUSlb3eK.js → popover-Z8Tvl-OD.js} +2 -2
  41. package/dist/client/assets/src-BU7yHzO9.js +3 -0
  42. package/dist/client/assets/{toggle-BREV66nT.js → toggle-CWYdWYBn.js} +2 -2
  43. package/dist/client/index.html +11 -11
  44. package/dist/forge-build.json +1 -1
  45. package/dist/{open-D8mGyWmt.mjs → open-FIgTDosf.mjs} +3 -3
  46. package/dist/runtimeBootstrap.cjs +1 -1
  47. package/dist/runtimeBootstrap.mjs +1 -1
  48. package/dist/{runtimeControlAuthority-B3kNmYRT.cjs → runtimeControlAuthority-BkVB0uAA.cjs} +17 -1
  49. package/dist/{runtimeControlAuthority-D_zKr_eI.mjs → runtimeControlAuthority-E_kdeD_P.mjs} +12 -2
  50. package/package.json +1 -1
  51. package/dist/client/assets/cmmdAuthRedirect-Bp1uQaQ9.js +0 -2
  52. package/dist/client/assets/index-Bn-ASQRG.css +0 -1
  53. package/dist/client/assets/src-BL-mOkT1.js +0 -3
package/dist/bin.mjs CHANGED
@@ -2,16 +2,16 @@
2
2
 
3
3
  import { a as __toCommonJS, i as __require, n as __esmMin, o as __toESM, r as __exportAll, t as __commonJSMin } from "./chunk-C5ckVeoz.mjs";
4
4
  import { $ as encodeEffect, $a as make$39, $c as fromNullishOr, $i as spaced, $l as constTrue, $n as filterMapOrFail, $r as runPromise, $s as millis, $t as stringifyJson, Aa as Scope$1, Ai as void_$1, Al as format, An as cached$3, Ar as match$1, As as uninterruptibleMask, At as withConstructorDefault, Ba as CurrentLogSpans, Bc as add$2, Bi as counter, Bl as compose$1, Bn as catch_, Bo as addFinalizer, Br as orDie, Bs as make$42, Bt as head, C as Record, Ca as mergeAll, Ci as tx$1, Cn as andThen, Co as hasDies, Cr as logInfo, Cs as sleep$3, Ct as optionalKey, D as TaggedStruct, Da as succeedContext, Di as uninterruptibleMask$1, Dn as as$3, Do as interruptors, Ds as suspend, Dt as toCodecStringTree, E as TaggedErrorClass, Ea as succeed$2, Ei as uninterruptible, En as annotateSpans, Eo as interrupt$2, Er as map$3, Et as toCodecJson, F as UndefinedOr, Fa as forkUnsafe, Fc as isSuccess$1, Fi as withParentSpan, Fn as catchFilter, Fr as onError$1, Fs as MinimumTraceLevel, G as decodeExit, Ga as done, Gc as getUnsafe$1, Gi as withAttributes, Gl as isNullish, Gn as die$1, Gr as provideService, Gs as format$1, Gt as Transformation, H as declare, Hc as get$3, Hi as snapshotUnsafe, Hl as isNotNull, Hn as contextWith, Ho as as$2, Hr as promise, Ht as size$2, I as Union, Ia as make$37, Ii as withSpan, In as catchIf, Io as ConsoleRef, Ir as onExit, Is as NativeSpan, It as assign, J as decodeUnknownEffect, Ja as failCause$3, Jc as merge$2, Ji as exponential, Jn as exit, Jr as repeat, Js as hours, Jt as transformOrFail, K as decodeSync, Kc as make$35, Ki as addDelay, Kl as isObject$1, Ko as consolePretty$1, Kr as race, Ks as fromInput, L as Unknown, La as makeUnsafe$4, Lc as succeed$1, Li as withTracerEnabled, Ln as catchReason, Lo as CurrentLoggers$1, Ls as ParentSpan, Lt as append$1, M as URLFromString, Ma as addFinalizerExit, Mi as whileLoop, Mn as callback, Mo as pretty, Ms as void_$2, Na as close, Ni as withFiber$1, Nn as catchCause, No as prettyErrors, Nr as matchEffect, O as TemplateLiteral, Oa as sync$2, Ol as NodeInspectSymbol, On as asSome, Or as mapError, Os as sync, Ot as toJsonSchemaDocument, P as Undefined, Pa as closeUnsafe, Pc as isFailure$1, Pl as equals, Pn as catchDefect, Po as squash, Pr as never$1, Q as encode$4, Qc as flatten$1, Qi as recurs, Qn as filter$1, Qr as runForkWith, Qs as max, R as UnknownFromJsonString$1, Ra as provide$2, Rc as Reference$1, Ri as yieldNow, Rn as catchTag, Rr as onInterrupt, Rs as Tracer, Rt as drop$1, S as Option, Sa as merge$3, Sc as sort, Si as try_, Sn as all, Sr as logFatal, Ss as scopeAddFinalizerExit, St as optional$4, T as Struct, Ta as provideMerge, Ti as undefined_, Tl as withFiber, Tn as annotateLogs, To as hasInterruptsOnly, Tr as makeSpanScoped, Tt as tag, Ua as TracerTimingEnabled, Ui as timer, Ul as isNotNullish, Un as currentSpan, Ur as provide, Ut as toArray, V as brand, Va as MinimumLogLevel, Vc as empty$1, Vi as gauge, Vl as hasProperty, Vn as context, Vo as andThen$1, Vr as orElseSucceed, Vt as isEmpty$2, W as decodeEffect, Wa as _await, Wi as update$3, Wl as isNotUndefined, Wr as provideContext, Ws as days, X as decodeUnknownOption, Xc as firstSomeOf, Xl as cast, Xn as failCause$1, Xr as retry, Xt as onSome, Y as decodeUnknownExit, Yi as forever$2, Yn as fail, Yr as result, Z as decodeUnknownSync, Za as isDeferred, Zc as flatMap$2, Zi as passthrough$1, Zl as compose, Zr as runFork, Zt as parseJson$1, _ as NonEmptyArray, _a as effectContext, _i as tapErrorTag, _l as YieldableProto, _n as toUtc, _t as isSchemaError, a as DateTimeUtc, aa as Random$1, ac as toSeconds, al as isSome, an as value, ao as interrupt$3, ar as fnUntraced, at as isBetween, au as identity, b as NullOr, bi as timeoutOrElse, bn as acquireUseRelease, br as logDebug, bs as reportCauseUnsafe, bt as makeFilter, c as Defect, ca as Clock, cc as empty$3, ci as sleep$4, cl as none$4, cn as add$3, co as match$2, cr as forkChild, cs as flatMap$1, ct as isGreaterThanOrEqualTo, d as Exit, dc as flatMapNullishOr, di as succeedSome, dl as make$41, dn as isGreaterThanOrEqualTo$1, dr as forkScoped, ds as forkDetach, dt as isMaxLength, ea as catchDone, ec as min, el as fromUndefinedOr, er as filterOrFail, et as encodeSync, eu as constUndefined, fa as TaggedError, fc as fromIterable, fi as suspend$1, fn as make$38, fr as gen, ft as isMaxProperties, g as Never, ga as effect, gi as tapError, gl as PipeInspectableProto, gn as toEpochMillis, gr as interruptible, gt as isSchema, h as Literals, ha as buildWithMemoMap, hc as isReadonlyArrayNonEmpty, hi as tapCause, hn as toDate, hr as interrupt$1, ht as isPattern, i as Class, ic as toMillis, ii as scoped, il as isNone, in as make$40, io as failCause$2, ir as fn$1, it as is$1, ja as addFinalizer$1, jc as fail$1, jn as cachedWithTTL, jo as map$6, jr as matchCauseEffect, jt as withDecodingDefault, k as Trim, ka as unwrap, ki as useSpan, kl as toJson, kn as asVoid, ks as tracerLogger$1, kt as toType, l as Duration, la as currentTimeMillis, lc as ensure, li as succeed, ll as some, ln as distance, lr as forkDetach$1, ls as fnUntraced$1, lt as isInt, m as Literal, ma as build, mc as isArrayNonEmpty, mi as tap, mn as now, mo as StackTrace, mr as ignoreCause, ms as loggerMake, mt as isNonEmpty, n as ArraySchema, nc as seconds, nl as getOrNull, no as die$2, nt as encodeUnknownExit, nu as constant, o as DateTimeUtcFromString, oa as clamp, oc as zero, oi as service, ol as map$5, oo as isFailure, or as forEach, ot as isFinite$1, ou as pipe$1, p as Int$1, pa as CurrentMemoMap, pi as sync$1, pn as makeUnsafe$5, po as NoSuchElementError, pr as ignore, pt as isMinLength, q as decodeTo, qa as fail$3, qc as makeUnsafe$3, qi as both, ql as isTagged, qn as ensuring, qr as raceFirst, qs as fromInputUnsafe, r as Boolean$1, ra as isDoneCause, rc as times, ri as scope$1, rl as getOrUndefined, rn as makeFormatterDefault, ro as fail$2, rr as flatten$2, rt as fromJsonString, ru as dual, sa as parse$6, sc as allocate, si as serviceOption, sl as match, sn as encodeBase64Url$2, so as isSuccess, sr as forever$1, st as isGreaterThan, su as pipeArguments, t as Any, tc as minutes, ti as runPromiseWith, tl as getOrElse, tn as InvalidValue$1, to as succeed$3, tr as flatMap, ts as fiberInterrupt, tt as encodeUnknownEffect, tu as constVoid, u as ErrorClass, ua as Class$1, uc as filter, ui as succeedNone, ul as Number$2, un as formatIso, uo as void_$3, ur as forkIn, us as forever, ut as isLessThanOrEqualTo, v as NonEmptyString, va as effectDiscard, vi as timeout$1, vn as Transaction$1, vo as done$1, vr as isEffect, w as String$1, wa as provide$1, wi as txRetry, wl as isEffect$1, wn as annotateCurrentSpan, wo as hasInterrupts, wr as logWarning$1, wt as suspend$2, x as Number$1, xa as launch$1, xc as reverse, xi as tryPromise, xn as addFinalizer$2, xr as logError, xs as scope, xt as mutable, y as Null, ya as empty$4, yc as map$4, yi as timeoutOption, yn as acquireRelease, yo as fail$4, yr as log$4, yt as make$36, z as Void, za as CurrentLogAnnotations, zc as Service$1, zi as zipWith, zn as catchTags, zr as option, zs as externalSpan, zt as empty$2 } from "./Schema-DOBWF8gQ.mjs";
5
- import { $ as tick, $t as runIn, A as isStream, At as failCause$4, Bt as takeAll, C as fromEffect, D as fromQueue, Dt as bounded, E as fromPull, Et as makeUnsafe$6, Ft as offerUnsafe, G as runDrain, Gt as unbounded$1, H as provideContext$1, Ht as publish, I as merge$4, J as runForEachArray, Jt as makeUnsafe$7, K as runFold, Lt as shutdown$1, M as map$7, Mt as make$45, N as mapAccum, Nt as offer$1, Ot as end, P as mapEffect$1, Pt as offerAll, Q as takeWhile, Rt as sliding, S as fromChannel, St as fromTransform, T as fromPubSub, Tt as make$44, U as run$2, Ut as shutdown$2, Vt as unbounded$2, W as runCollect, Wt as subscribe$1, X as succeed$4, Xt as getCurrent, Yt as await_, Zt as interrupt$4, at as unwrap$1, b as flatMap$3, c as callback$1, ct as get$5, d as debounce$1, et as toAsyncIterable, f as decodeText$1, g as ensuring$1, gt as forEach$1, h as encodeText, ht as drain, j as make$43, k as fromSubscription, l as catchCause$1, lt as make$47, m as empty$5, ot as get$4, q as runForEach, r as FileSystem, st as make$46, t as Path$2, tt as toPull, u as concat$2, v as failCause$5, w as fromIterable$1, x as fromAsyncIterable, xt as fromSubscriptionArray, y as filter$2, yt as mapInput, z as onExit$1, zt as take$1 } from "./Path-evOm-H0Q.mjs";
6
- import { _ as ChildProcessSpawner, a as Stdio, g as make$48, o as make$49, r as isQuitError } from "./NodeTerminal-pYVVRTiD.mjs";
7
- import { n as layer$7 } from "./NodeServices-z1Ac4lEO.mjs";
8
- import { $ as file, A as SocketErrorReason, At as merge$5, Bt as set$3, Ct as HttpClientErrorSchema, Dt as empty$7, Ft as getAndSet, G as TracerPropagationEnabled, Ht as updateAndGet, I as HttpServerError, It as getUnsafe$2, J as transformResponse, K as filterStatusOk$1, Lt as make$50, Ot as fromInput$1, Pt as get$6, Q as empty$6, Rt as makeUnsafe$8, St as setBody, Tt as jsonUnsafe$1, U as layer$8, Vt as update$4, W as HttpClient, Z as symbol$1, _ as schemaBodyUrlParams, _t as acceptJson, a as withLogAddress, at as setCookie, bt as get$7, ct as stream, d as logger$1, dt as filterStatusOk, et as isHttpServerResponse, f as withLoggerDisabled, ft as matchStatus, g as schemaBodyJson, gt as schemaBodyJson$1, i as serve$1, it as schemaJson, kt as fromRecordUnsafe, lt as text$2, m as ParsedSearchParams, nt as raw, ot as setCookies, p as HttpServerRequest, q as retryTransient, rt as redirect, st as setHeader, t as HttpServer, tt as jsonUnsafe, u as cors, ut as uint8Array, v as schemaHeaders, vt as bearerToken, wt as isHttpClientError, xt as post, y as toURL, yt as bodyJson, z as RouteNotFound, zt as modify$3 } from "./HttpServer-COaBeM7-.mjs";
5
+ import { $ as takeWhile, A as fromSubscription, B as onExit$1, Bt as take$1, C as fromChannel, Ct as fromTransform, D as fromPull, Dt as makeUnsafe$6, E as fromPubSub, Et as make$44, F as mapEffect$1, Ft as offerAll, G as runCollect, Gt as subscribe$1, Ht as unbounded$2, I as mapError$1, It as offerUnsafe, J as runForEach, K as runDrain, Kt as unbounded$1, L as merge$4, M as make$43, N as map$7, Nt as make$45, O as fromQueue, Ot as bounded, P as mapAccum, Pt as offer$1, Qt as interrupt$4, Rt as shutdown$1, S as fromAsyncIterable, St as fromSubscriptionArray, T as fromIterable$1, U as provideContext$1, Ut as publish, Vt as takeAll, W as run$2, Wt as shutdown$2, Xt as await_, Y as runForEachArray, Yt as makeUnsafe$7, Z as succeed$4, Zt as getCurrent, _t as forEach$1, b as flatMap$3, bt as mapInput, c as callback$1, ct as make$46, d as debounce$1, en as runIn, et as tick, f as decodeText$1, g as ensuring$1, gt as drain, h as encodeText, j as isStream, jt as failCause$4, kt as end, l as catchCause$1, lt as get$5, m as empty$5, nt as toPull, ot as unwrap$1, q as runFold, r as FileSystem, st as get$4, t as Path$2, tt as toAsyncIterable, u as concat$2, ut as make$47, v as failCause$5, w as fromEffect, x as fromArray, y as filter$2, zt as sliding } from "./Path-CZJ1gPPo.mjs";
6
+ import { _ as ChildProcessSpawner, a as Stdio, g as make$48, o as make$49, r as isQuitError } from "./NodeTerminal-9iHZIdt1.mjs";
7
+ import { n as layer$7 } from "./NodeServices-D3IxOzHl.mjs";
8
+ import { $ as file, A as SocketErrorReason, At as merge$5, Bt as set$3, Ct as HttpClientErrorSchema, Dt as empty$7, Ft as getAndSet, G as TracerPropagationEnabled, Ht as updateAndGet, I as HttpServerError, It as getUnsafe$2, J as transformResponse, K as filterStatusOk$1, Lt as make$50, Ot as fromInput$1, Pt as get$6, Q as empty$6, Rt as makeUnsafe$8, St as setBody, Tt as jsonUnsafe$1, U as layer$8, Vt as update$4, W as HttpClient, Z as symbol$1, _ as schemaBodyUrlParams, _t as acceptJson, a as withLogAddress, at as setCookie, bt as get$7, ct as stream, d as logger$1, dt as filterStatusOk, et as isHttpServerResponse, f as withLoggerDisabled, ft as matchStatus, g as schemaBodyJson, gt as schemaBodyJson$1, i as serve$1, it as schemaJson, kt as fromRecordUnsafe, lt as text$2, m as ParsedSearchParams, nt as raw, ot as setCookies, p as HttpServerRequest, q as retryTransient, rt as redirect, st as setHeader, t as HttpServer, tt as jsonUnsafe, u as cors, ut as uint8Array, v as schemaHeaders, vt as bearerToken, wt as isHttpClientError, xt as post, y as toURL, yt as bodyJson, z as RouteNotFound, zt as modify$3 } from "./HttpServer-Bclwvu0I.mjs";
9
9
  import { _ as withDefault$3, a as all$1, c as logLevel, d as option$1, f as port, g as url$2, i as TrueValues, l as map$8, m as string$7, n as FalseValues, o as boolean$5, p as schema$2, r as Record$1, s as int$2, t as Boolean$2, u as number$2 } from "./Config-Cdfpphpo.mjs";
10
- import { a as make$51, i as keys, n as getOption, o as makeWith$1, r as invalidate, s as set$4, t as get$8 } from "./Cache-C9cwdCDt.mjs";
11
- import { S as layer$9, _ as defaultEscape, a as DeadlockError, b as makeCompiler$1, c as SqlError, d as UnknownError, g as custom, h as make$52, i as ConstraintError, l as SqlSyntaxError, m as SqlClient, n as AuthorizationError, o as LockTimeoutError, r as ConnectionError, s as SerializationError, t as AuthenticationError, u as StatementTimeoutError, v as defaultTransforms, y as fragment } from "./SqlError-CezTFGvr.mjs";
10
+ import { a as make$51, i as keys, n as getOption, o as makeWith$1, r as invalidate, s as set$4, t as get$8 } from "./Cache-D57GWb9-.mjs";
11
+ import { S as layer$9, _ as defaultEscape, a as DeadlockError, b as makeCompiler$1, c as SqlError, d as UnknownError, g as custom, h as make$52, i as ConstraintError, l as SqlSyntaxError, m as SqlClient, n as AuthorizationError, o as LockTimeoutError, r as ConnectionError, s as SerializationError, t as AuthenticationError, u as StatementTimeoutError, v as defaultTransforms, y as fragment } from "./SqlError-BMBkezUO.mjs";
12
12
  import { r as wrapper_default, t as Mime_default } from "./Mime-Dq9IoNiV.mjs";
13
13
  import { a as diag, c as SpanKind$1, d as TraceFlags, f as ROOT_CONTEXT, i as propagation, l as isSpanContextValid, m as DiagLogLevel, n as init_esm$1, o as context$1, p as createContextKey, r as trace, s as SpanStatusCode, t as esm_exports$1 } from "./esm-wLIF-u3N.mjs";
14
- import { a as verifyCmmdAuthRuntimeTicket, c as verifyCmmdRuntimeTicket, d as setRuntimeGrantToken, f as createReadOnceFileTokenAccessor, i as sendRuntimeControlHeartbeat, l as verifyCmmdRuntimeTicketRefreshProof, n as bootstrapRuntimeControlAuthorityFromNarrowGrant, o as verifyCmmdClaudeReadinessTicket, r as readRuntimeControlAuthority, s as verifyCmmdClaudeUsageTicket, t as bootstrapRuntimeControlAuthority, u as runtimeGrantToken } from "./runtimeControlAuthority-D_zKr_eI.mjs";
14
+ import { a as verifyCmmdAuthRuntimeTicket, c as verifyCmmdRuntimeTicket, d as runtimeGrantToken, f as setRuntimeGrantToken, i as sendRuntimeControlHeartbeat, l as verifyCmmdRuntimeTicketRefreshProof, n as bootstrapRuntimeControlAuthorityFromNarrowGrant, o as verifyCmmdClaudeReadinessTicket, p as createReadOnceFileTokenAccessor, r as readRuntimeControlAuthority, s as verifyCmmdClaudeUsageTicket, t as bootstrapRuntimeControlAuthority, u as verifyCmmdScopedRuntimePayload } from "./runtimeControlAuthority-E_kdeD_P.mjs";
15
15
  import { n as PtySpawnError, t as PtyAdapter } from "./PTY-BzdbZ0Ym.mjs";
16
16
  import { createRequire } from "node:module";
17
17
  import * as HE from "node:child_process";
@@ -22,7 +22,7 @@ import { createCipheriv, createDecipheriv, createHash, createHmac, createPublicK
22
22
  import * as FS$1 from "node:fs";
23
23
  import fs, { accessSync, chmodSync, constants, createReadStream, existsSync, mkdirSync, mkdtempSync, promises, readFile, readFileSync, readdir, readdirSync, realpathSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
24
24
  import * as NodeOS from "node:os";
25
- import $U, { homedir, networkInterfaces, tmpdir } from "node:os";
25
+ import os, { homedir, networkInterfaces, tmpdir } from "node:os";
26
26
  import * as Path$1 from "node:path";
27
27
  import path, { basename, delimiter, dirname, extname, isAbsolute, join, posix, relative, resolve, sep } from "node:path";
28
28
  import { pathToFileURL } from "node:url";
@@ -65,12 +65,12 @@ import * as tls from "node:tls";
65
65
  import { performance as performance$1 } from "perf_hooks";
66
66
  import { execFile as execFile$1, spawn as spawn$1 } from "child_process";
67
67
  import * as NodeFSP from "node:fs/promises";
68
- import fs$1, { link, lstat, mkdir, mkdtemp, readFile as readFile$1, realpath, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
68
+ import fs$1, { chmod, link, lstat, mkdir, mkdtemp, readFile as readFile$1, readdir as readdir$1, realpath, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
69
69
  import { StringDecoder } from "string_decoder";
70
70
  import * as timers from "node:timers/promises";
71
71
  import { pipeline } from "node:stream/promises";
72
72
  import z$ from "http2";
73
- import { access, appendFile, chmod, copyFile, link as link$1, lstat as lstat$1, mkdir as mkdir$1, open, readFile as readFile$2, readdir as readdir$1, readlink, realpath as realpath$1, rename as rename$1, rm as rm$1, rmdir, stat as stat$1, symlink, unlink as unlink$1, writeFile as writeFile$1 } from "fs/promises";
73
+ import { access, appendFile, chmod as chmod$1, copyFile, link as link$1, lstat as lstat$1, mkdir as mkdir$1, open, readFile as readFile$2, readdir as readdir$2, readlink, realpath as realpath$1, rename as rename$1, rm as rm$1, rmdir, stat as stat$1, symlink, unlink as unlink$1, writeFile as writeFile$1 } from "fs/promises";
74
74
  import { createInterface as createInterface$1 } from "readline";
75
75
  import { AsyncLocalStorage as AsyncLocalStorage$1 } from "async_hooks";
76
76
  import { chromium } from "playwright";
@@ -17358,7 +17358,8 @@ const ORCHESTRATION_WS_METHODS = {
17358
17358
  getArchivedShellSnapshot: "orchestration.getArchivedShellSnapshot",
17359
17359
  subscribeShell: "orchestration.subscribeShell",
17360
17360
  subscribeThread: "orchestration.subscribeThread",
17361
- searchThreads: "orchestration.searchThreads"
17361
+ searchThreads: "orchestration.searchThreads",
17362
+ getThreadDetailPage: "orchestration.getThreadDetailPage"
17362
17363
  };
17363
17364
  const ProviderKind = Literals([
17364
17365
  "codex",
@@ -17868,10 +17869,39 @@ const OrchestrationShellStreamItem = Union([Struct({
17868
17869
  kind: Literal("snapshot"),
17869
17870
  snapshot: OrchestrationShellSnapshot
17870
17871
  }), OrchestrationShellStreamEvent]);
17871
- const OrchestrationSubscribeThreadInput = Struct({ threadId: ThreadId });
17872
+ const OrchestrationSubscribeThreadInput = Struct({
17873
+ threadId: ThreadId,
17874
+ afterSequence: optionalKey(NonNegativeInt),
17875
+ requestCompletionMarker: optionalKey(Boolean$1),
17876
+ turnLimit: optionalKey(PositiveInt)
17877
+ });
17878
+ Struct({
17879
+ turnLimit: optionalKey(PositiveInt),
17880
+ beforeCursor: optionalKey(TrimmedNonEmptyString)
17881
+ });
17882
+ /**
17883
+ * Reads one windowed page of a thread's detail directly over the RPC socket.
17884
+ *
17885
+ * The HTTP snapshot route and `subscribeThread`'s fallback snapshot both take
17886
+ * the same `turnLimit`/`beforeCursor` window, but neither can serve "load
17887
+ * earlier" on a connection with no HTTP endpoint: the fallback snapshot only
17888
+ * fires once, on subscribe. This method exists for exactly that case (a
17889
+ * relay-only connection), gated on the server's `threadDetailPageRpc`
17890
+ * capability so older servers are never sent a method they do not know.
17891
+ */
17892
+ const OrchestrationGetThreadDetailPageInput = Struct({
17893
+ threadId: ThreadId,
17894
+ turnLimit: optionalKey(PositiveInt),
17895
+ beforeCursor: optionalKey(TrimmedNonEmptyString)
17896
+ });
17872
17897
  const OrchestrationThreadDetailSnapshot = Struct({
17873
17898
  snapshotSequence: NonNegativeInt,
17874
- thread: OrchestrationThread
17899
+ thread: OrchestrationThread,
17900
+ page: optional$4(Struct({
17901
+ beforeCursor: NullOr(TrimmedNonEmptyString),
17902
+ hasMore: Boolean$1,
17903
+ snapshotSequence: NonNegativeInt
17904
+ }))
17875
17905
  });
17876
17906
  const ProjectCreateCommand = Struct({
17877
17907
  type: Literal("project.create"),
@@ -18675,13 +18705,17 @@ const OrchestrationEvent = Union([
18675
18705
  payload: ThreadRateLimitSetPayload$1
18676
18706
  })
18677
18707
  ]);
18678
- const OrchestrationThreadStreamItem = Union([Struct({
18679
- kind: Literal("snapshot"),
18680
- snapshot: OrchestrationThreadDetailSnapshot
18681
- }), Struct({
18682
- kind: Literal("event"),
18683
- event: OrchestrationEvent
18684
- })]);
18708
+ const OrchestrationThreadStreamItem = Union([
18709
+ Struct({ kind: Literal("synchronized") }),
18710
+ Struct({
18711
+ kind: Literal("snapshot"),
18712
+ snapshot: OrchestrationThreadDetailSnapshot
18713
+ }),
18714
+ Struct({
18715
+ kind: Literal("event"),
18716
+ event: OrchestrationEvent
18717
+ })
18718
+ ]);
18685
18719
  const OrchestrationCommandReceiptStatus = Literals(["accepted", "rejected"]);
18686
18720
  const TurnCountRange = Struct({
18687
18721
  fromTurnCount: NonNegativeInt,
@@ -18824,6 +18858,10 @@ const OrchestrationRpcSchemas = {
18824
18858
  searchThreads: {
18825
18859
  input: OrchestrationSearchThreadsInput,
18826
18860
  output: OrchestrationSearchThreadsResult
18861
+ },
18862
+ getThreadDetailPage: {
18863
+ input: OrchestrationGetThreadDetailPageInput,
18864
+ output: OrchestrationThreadDetailSnapshot
18827
18865
  }
18828
18866
  };
18829
18867
  var OrchestrationGetSnapshotError = class extends TaggedErrorClass()("OrchestrationGetSnapshotError", {
@@ -22792,6 +22830,9 @@ const ServerConfig$1 = Struct({
22792
22830
  availableEditors: ArraySchema(EditorId),
22793
22831
  shellRevealInFileManager: optionalKey(Boolean$1),
22794
22832
  shellRevealInFileManagerKind: optionalKey(FileManagerRevealKind),
22833
+ threadResumeCompletionMarker: optionalKey(Boolean$1),
22834
+ threadSnapshotPagination: optionalKey(Boolean$1),
22835
+ threadDetailPageRpc: optionalKey(Boolean$1),
22795
22836
  observability: ServerObservability,
22796
22837
  settings: ServerSettings
22797
22838
  });
@@ -26658,6 +26699,17 @@ const WsOrchestrationSearchThreadsRpc = make$30(ORCHESTRATION_WS_METHODS.searchT
26658
26699
  error: OrchestrationSearchThreadsError
26659
26700
  });
26660
26701
  /**
26702
+ * The socket-only path for "load earlier turns": a relay-only connection has
26703
+ * no HTTP endpoint, so this reads the same windowed page the HTTP snapshot
26704
+ * route and `subscribeThread`'s fallback snapshot serve, over the RPC socket
26705
+ * instead.
26706
+ */
26707
+ const WsOrchestrationGetThreadDetailPageRpc = make$30(ORCHESTRATION_WS_METHODS.getThreadDetailPage, {
26708
+ payload: OrchestrationRpcSchemas.getThreadDetailPage.input,
26709
+ success: OrchestrationRpcSchemas.getThreadDetailPage.output,
26710
+ error: OrchestrationGetSnapshotError
26711
+ });
26712
+ /**
26661
26713
  * Resolve `owner/repo` to the repository the caller is allowed to clone.
26662
26714
  *
26663
26715
  * Both of these run against the same CMMD-brokered GitHub credential the web
@@ -26770,7 +26822,7 @@ const WsRpcGroup = make$29(WsServerGetConfigRpc, WsServerRefreshProvidersRpc, Ws
26770
26822
  payload: ProviderUploadFeedbackInput,
26771
26823
  success: ProviderUploadFeedbackResult,
26772
26824
  error: ProviderUploadFeedbackError
26773
- }), WsOrchestrationSearchThreadsRpc, WsSourceControlLookupRepositoryRpc, WsSourceControlCloneRepositoryRpc);
26825
+ }), WsOrchestrationSearchThreadsRpc, WsSourceControlLookupRepositoryRpc, WsSourceControlCloneRepositoryRpc, WsOrchestrationGetThreadDetailPageRpc);
26774
26826
  const RelayWsRpcGroup = WsRpcGroup.middleware(RelayAuthorizationMiddleware).middleware(RelayDeliveryMiddleware);
26775
26827
  /**
26776
26828
  * The direct-WebSocket mount of the same group, with per-method scope
@@ -26843,6 +26895,32 @@ function encodeCmmdRuntimeToolActivityPayload(kind, payload) {
26843
26895
  }
26844
26896
  };
26845
26897
  }
26898
+ const WORKTREE_LAUNCH_ACTIVITY_KINDS = new Set([
26899
+ "worktree.requested",
26900
+ "worktree.stage",
26901
+ "worktree.output",
26902
+ "worktree.created",
26903
+ "worktree.failed",
26904
+ "setup-script.requested",
26905
+ "setup-script.started",
26906
+ "setup-script.skipped",
26907
+ "setup-script.failed",
26908
+ "launch.agent-starting",
26909
+ "launch.cancelled"
26910
+ ]);
26911
+ function encodeCmmdRuntimeWorktreeLaunchActivityPayload(kind, payload) {
26912
+ return {
26913
+ ...isRecord$23(payload) ? payload : { sourcePayload: payload },
26914
+ forgeWorktreeLaunchActivity: {
26915
+ schemaVersion: 1,
26916
+ source: "native",
26917
+ kind
26918
+ }
26919
+ };
26920
+ }
26921
+ function isCmmdRuntimeWorktreeLaunchActivityKind(kind) {
26922
+ return WORKTREE_LAUNCH_ACTIVITY_KINDS.has(kind);
26923
+ }
26846
26924
  //#endregion
26847
26925
  //#region ../../packages/contracts/src/cmmdProjectPatch.ts
26848
26926
  const CMMD_PROJECT_PATCH_FIELDS = {
@@ -31945,6 +32023,11 @@ const RELAY_METHOD_POLICY_BASES = [
31945
32023
  requiredScope: "thread.read",
31946
32024
  deliveryClass: "subscription"
31947
32025
  },
32026
+ {
32027
+ method: ORCHESTRATION_WS_METHODS.getThreadDetailPage,
32028
+ requiredScope: "thread.read",
32029
+ deliveryClass: "read"
32030
+ },
31948
32031
  {
31949
32032
  method: WS_METHODS.assetsCreateUrl,
31950
32033
  requiredScope: "thread.read",
@@ -50614,7 +50697,7 @@ function _supportsColor(haveStream, { streamIsTTY, sniffFlags = true } = {}) {
50614
50697
  const min = forceColor || 0;
50615
50698
  if (env.TERM === "dumb") return min;
50616
50699
  if (process$1.platform === "win32") {
50617
- const osRelease = $U.release().split(".");
50700
+ const osRelease = os.release().split(".");
50618
50701
  if (Number(osRelease[0]) >= 10 && Number(osRelease[2]) >= 10586) return Number(osRelease[2]) >= 14931 ? 3 : 2;
50619
50702
  return 1;
50620
50703
  }
@@ -53057,7 +53140,7 @@ var require_ProcessDetector = /* @__PURE__ */ __commonJSMin(((exports) => {
53057
53140
  exports.processDetector = void 0;
53058
53141
  const api_1 = (init_esm$1(), __toCommonJS(esm_exports$1));
53059
53142
  const semconv_1 = require_semconv$1();
53060
- const os$3 = __require("os");
53143
+ const os$4 = __require("os");
53061
53144
  /**
53062
53145
  * ProcessDetector will be used to detect the resources related current process running
53063
53146
  * and being instrumented from the NodeJS Process module.
@@ -53079,7 +53162,7 @@ var require_ProcessDetector = /* @__PURE__ */ __commonJSMin(((exports) => {
53079
53162
  };
53080
53163
  if (process.argv.length > 1) attributes[semconv_1.ATTR_PROCESS_COMMAND] = process.argv[1];
53081
53164
  try {
53082
- const userInfo = os$3.userInfo();
53165
+ const userInfo = os$4.userInfo();
53083
53166
  attributes[semconv_1.ATTR_PROCESS_OWNER] = userInfo.username;
53084
53167
  } catch (e) {
53085
53168
  api_1.diag.debug(`error obtaining process owner: ${e}`);
@@ -66002,7 +66085,7 @@ function normalizeNumberish(value) {
66002
66085
  }
66003
66086
  //#endregion
66004
66087
  //#region package.json
66005
- var version$1 = "0.13.11";
66088
+ var version$1 = "0.13.13";
66006
66089
  //#endregion
66007
66090
  //#region src/sentry.ts
66008
66091
  const SERVER_APP_NAME = "forge-server";
@@ -66404,6 +66487,129 @@ const respondToAuthError = (error) => gen(function* () {
66404
66487
  }));
66405
66488
  return response;
66406
66489
  });
66490
+ //#endregion
66491
+ //#region src/http/rateLimit.ts
66492
+ /**
66493
+ * Per-client, per-route-class rate limiting for Forge's own HTTP surface.
66494
+ *
66495
+ * Scoped to the routes a broken or looping client can hammer before it ever
66496
+ * has an authenticated session — the CMMD device-code poll, the bootstrap and
66497
+ * pairing-token exchanges — plus the authenticated /api/cmmd proxy, which
66498
+ * every desktop UI interaction runs through. None of these had any limit
66499
+ * before FOR-36706: a stuck pairing loop or a poll that never saw `slow_down`
66500
+ * could retry as fast as the network allowed, with nothing to stop it and
66501
+ * nothing telling the client to back off.
66502
+ *
66503
+ * Defaults are generous on purpose. This exists to bound a runaway client,
66504
+ * not to throttle a working one — see `defaultRateLimitPolicies` for the
66505
+ * reasoning behind each number and how to override it.
66506
+ *
66507
+ * Deliberately plain module state, not an Effect service: every route this
66508
+ * guards is a synchronous decision (count, compare, maybe reset a window),
66509
+ * and threading a new Context requirement through the whole server's layer
66510
+ * graph for that would touch every test file that stands up any of these
66511
+ * routes. `cmmdJwtResolver.ts`'s in-flight-refresh map is the same call.
66512
+ */
66513
+ function envInt(name, fallback) {
66514
+ const raw = process.env[name];
66515
+ if (!raw) return fallback;
66516
+ const parsed = Number(raw);
66517
+ return Number.isFinite(parsed) && parsed > 0 ? Math.floor(parsed) : fallback;
66518
+ }
66519
+ /**
66520
+ * One reasoning line per class, so a future change to a default has to
66521
+ * confront why the old one was picked:
66522
+ *
66523
+ * - `auth-device-poll` (`/api/auth/cmmd-oauth/device` and `/token`): RFC 8628
66524
+ * polls every `interval` seconds, 5 by default, so a well-behaved client
66525
+ * sits under 12/min. 120/min leaves room for several concurrent sign-in
66526
+ * attempts and CMMD's own `slow_down` handling without ever tripping on
66527
+ * normal use, while still bounding a client that ignores `slow_down`.
66528
+ * - `auth-bootstrap` (`/api/auth/bootstrap*`, the bearer bootstrap routes):
66529
+ * a working client bootstraps once per sign-in. 30/min is generous headroom
66530
+ * for retries after a network blip, not a budget anyone should approach.
66531
+ * - `auth-token-exchange` (`/oauth/token`, the vendored pairing exchange):
66532
+ * same shape as bootstrap — one pairing attempt, occasionally retried.
66533
+ * - `cmmd-proxy` (every `/api/cmmd/*` route): this is the one real desktop
66534
+ * traffic actually leans on — task lists, project browsing, comments, all
66535
+ * day. 1200/min (20/sec) is set high enough that no normal session should
66536
+ * ever see it; it exists for a retry loop or a runaway resync, not for
66537
+ * ordinary use.
66538
+ */
66539
+ function buildPoliciesFromEnv() {
66540
+ return {
66541
+ "auth-device-poll": {
66542
+ maxRequests: envInt("FORGE_RATE_LIMIT_AUTH_DEVICE_POLL_MAX", 120),
66543
+ windowMs: envInt("FORGE_RATE_LIMIT_AUTH_DEVICE_POLL_WINDOW_SECONDS", 60) * 1e3
66544
+ },
66545
+ "auth-bootstrap": {
66546
+ maxRequests: envInt("FORGE_RATE_LIMIT_AUTH_BOOTSTRAP_MAX", 30),
66547
+ windowMs: envInt("FORGE_RATE_LIMIT_AUTH_BOOTSTRAP_WINDOW_SECONDS", 60) * 1e3
66548
+ },
66549
+ "auth-token-exchange": {
66550
+ maxRequests: envInt("FORGE_RATE_LIMIT_AUTH_TOKEN_EXCHANGE_MAX", 30),
66551
+ windowMs: envInt("FORGE_RATE_LIMIT_AUTH_TOKEN_EXCHANGE_WINDOW_SECONDS", 60) * 1e3
66552
+ },
66553
+ "cmmd-proxy": {
66554
+ maxRequests: envInt("FORGE_RATE_LIMIT_CMMD_PROXY_MAX", 1200),
66555
+ windowMs: envInt("FORGE_RATE_LIMIT_CMMD_PROXY_WINDOW_SECONDS", 60) * 1e3
66556
+ }
66557
+ };
66558
+ }
66559
+ let policies = buildPoliciesFromEnv();
66560
+ const windows = /* @__PURE__ */ new Map();
66561
+ function windowKey(routeClass, clientKey) {
66562
+ return `${routeClass}\0${clientKey}`;
66563
+ }
66564
+ /**
66565
+ * Fixed-window counting: cheap, and precise enough for a loop-guard rather
66566
+ * than a security boundary. The one imprecision this trades away — up to 2x
66567
+ * the configured rate right at a window boundary — matters for an attacker
66568
+ * budgeting requests against the limiter's clock, not for the runaway
66569
+ * clients this exists to catch.
66570
+ */
66571
+ function checkHttpRateLimit(input) {
66572
+ const policy = policies[input.routeClass];
66573
+ const nowMs = Date.now();
66574
+ const key = windowKey(input.routeClass, input.clientKey);
66575
+ const existing = windows.get(key);
66576
+ const windowIsLive = existing !== void 0 && nowMs - existing.windowStartMs < policy.windowMs;
66577
+ const windowStartMs = windowIsLive ? existing.windowStartMs : nowMs;
66578
+ const count = (windowIsLive ? existing.count : 0) + 1;
66579
+ windows.set(key, {
66580
+ windowStartMs,
66581
+ count
66582
+ });
66583
+ return {
66584
+ allowed: count <= policy.maxRequests,
66585
+ retryAfterSeconds: Math.max(0, Math.ceil((windowStartMs + policy.windowMs - nowMs) / 1e3))
66586
+ };
66587
+ }
66588
+ /**
66589
+ * Checks the limiter for one request and, when the caller is over budget,
66590
+ * returns the 429 to send instead of running the route — `null` means
66591
+ * proceed. Every route this guards is expected to call this first and return
66592
+ * early on a non-null result, before doing any other work.
66593
+ */
66594
+ function rateLimitOrRespond(input) {
66595
+ const decision = checkHttpRateLimit(input);
66596
+ if (decision.allowed) return null;
66597
+ return jsonUnsafe({ error: "Too many requests. Try again later." }, {
66598
+ status: 429,
66599
+ headers: { "retry-after": String(decision.retryAfterSeconds) }
66600
+ });
66601
+ }
66602
+ /**
66603
+ * The client identity for a route with no session yet — the device-code
66604
+ * poll, bootstrap and pairing-token exchange all run before Forge has issued
66605
+ * one. IP address is the only stable-enough signal available at that point;
66606
+ * the authenticated `/api/cmmd` proxy keys on the session instead (see its
66607
+ * call site), so one desktop and one phone sharing a NAT never share a
66608
+ * budget there.
66609
+ */
66610
+ function clientKeyFromRequest(request) {
66611
+ return normalizeIpAddress(getOrNull(request.remoteAddress)) ?? "unknown";
66612
+ }
66407
66613
  const OAUTH_ACCESS_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token";
66408
66614
  /**
66409
66615
  * The one exchange this endpoint performs, or the reason it will not.
@@ -66429,6 +66635,29 @@ function accessTokenExpiresInSeconds(remainingMillis) {
66429
66635
  return Math.max(0, Math.floor(remainingMillis / 1e3));
66430
66636
  }
66431
66637
  //#endregion
66638
+ //#region src/auth/commandBootstrapClient.ts
66639
+ const BOOTSTRAP_CLIENT_LABELS = {
66640
+ desktop: "Forge Desktop Cloud",
66641
+ mobile: "Forge Mobile Cloud"
66642
+ };
66643
+ /**
66644
+ * Client metadata for a bearer bootstrap session.
66645
+ *
66646
+ * The declared device type wins over the one inferred from the user agent. A
66647
+ * React Native fetch sends whatever the platform's networking stack sends, and
66648
+ * on iOS that is a CFNetwork string no user-agent heuristic reads as a phone.
66649
+ */
66650
+ function resolveCommandBootstrapClient(input) {
66651
+ const deviceType = input.clientDeviceType ?? "desktop";
66652
+ return {
66653
+ ...deriveAuthClientMetadata({
66654
+ request: input.request,
66655
+ label: BOOTSTRAP_CLIENT_LABELS[deviceType]
66656
+ }),
66657
+ deviceType
66658
+ };
66659
+ }
66660
+ //#endregion
66432
66661
  //#region ../../packages/shared/src/cmmdIdentity.ts
66433
66662
  const MAX_CMMD_AVATAR_URL_LENGTH = 256 * 1024;
66434
66663
  const DATA_IMAGE_AVATAR_PATTERN = /^data:image\/(?:avif|gif|jpeg|jpg|png|webp);base64,[a-z0-9+/=]+$/i;
@@ -66882,14 +67111,26 @@ function extractJwtExpiresAt(jwtToken) {
66882
67111
  }
66883
67112
  }
66884
67113
  /**
66885
- * Returns true when a stored JWT is past its expiry or has no decodable expiry.
66886
- * CMMD REST JWTs are short-lived bearer tokens, so unknown expiry is not safe.
67114
+ * How long before `exp` a stored JWT is already treated as spent.
67115
+ *
67116
+ * Comparing against `exp` exactly leaves a window where Forge sends a token it
67117
+ * believes is good and CMMD has already stopped accepting it: the clocks differ by a
67118
+ * little, the request spends time in flight, and the answer comes back 401 on a
67119
+ * session that had a perfectly good refresh token sitting next to it. Two
67120
+ * minutes is far inside the JWT's own lifetime, so this costs one early
67121
+ * rotation and removes the race.
67122
+ */
67123
+ const JWT_EXPIRY_SKEW_MS = 2 * 6e4;
67124
+ /**
67125
+ * Returns true when a stored JWT is at or near its expiry, or has no decodable
67126
+ * expiry. CMMD REST JWTs are short-lived bearer tokens, so unknown expiry is
67127
+ * not safe.
66887
67128
  */
66888
67129
  function isJwtExpiredForSession(sessionId) {
66889
67130
  if (!jwtCache.has(sessionId)) return false;
66890
67131
  const expiresAt = jwtExpiryCache.get(sessionId);
66891
67132
  if (!expiresAt) return true;
66892
- return new Date(expiresAt) <= /* @__PURE__ */ new Date();
67133
+ return new Date(expiresAt).getTime() - JWT_EXPIRY_SKEW_MS <= Date.now();
66893
67134
  }
66894
67135
  /**
66895
67136
  * Hydrate the in-memory cache from the configured persistence layer. Call once at server startup
@@ -67445,10 +67686,27 @@ const clearUnrecoverableCmmdAuthForSession = (sessionId) => gen(function* () {
67445
67686
  * Retire refresh credentials copied into historical desktop bearer sessions.
67446
67687
  * Electron owns rotation for native desktop OAuth; Forge Local keeps only the
67447
67688
  * derived access authority needed by the current bearer session.
67448
- */
67449
- const clearDesktopBearerRefreshTokensForSubject = (subject) => gen(function* () {
67450
- const normalizedSubject = subject.trim();
67451
- if (!normalizedSubject) return;
67689
+ *
67690
+ * Scoped to the bootstrapping client, not to the whole subject. The phone and
67691
+ * Electron sign in as the same `user:<id>`, so a subject-wide sweep let one
67692
+ * desktop launch null out a phone's refresh token and end its session a day
67693
+ * later. Two clauses keep that from happening:
67694
+ *
67695
+ * - `client_device_type = 'desktop'`, which now excludes phones outright,
67696
+ * because the mobile app declares itself at bootstrap.
67697
+ * - the same recorded `client_user_agent` as the bootstrap doing the
67698
+ * retirement, which is the only thing that separates a phone row written
67699
+ * before that stamp existed from a real Electron row. Both are stamped
67700
+ * `desktop`; neither carries any other mark. An empty or unmatched user agent
67701
+ * retires nothing, which is the direction that keeps a live credential.
67702
+ *
67703
+ * Electron re-bootstraps under a stable user agent, so its own historical rows
67704
+ * still match and still get swept.
67705
+ */
67706
+ const clearDesktopBearerRefreshTokensForSubject = (input) => gen(function* () {
67707
+ const normalizedSubject = input.subject.trim();
67708
+ const normalizedUserAgent = input.clientUserAgent?.trim() ?? "";
67709
+ if (!normalizedSubject || !normalizedUserAgent) return;
67452
67710
  const rows = yield* (yield* SqlClient)`
67453
67711
  UPDATE command_access_tokens
67454
67712
  SET
@@ -67461,6 +67719,7 @@ const clearDesktopBearerRefreshTokensForSubject = (subject) => gen(function* ()
67461
67719
  AND role = 'owner'
67462
67720
  AND method = 'bearer-session-token'
67463
67721
  AND client_device_type = 'desktop'
67722
+ AND client_user_agent = ${normalizedUserAgent}
67464
67723
  )
67465
67724
  RETURNING session_id AS "sessionId"
67466
67725
  `.pipe(mapError(toPersistenceSqlError("CommandAccessTokenStore.clearDesktopBearerRefresh:query")));
@@ -67483,6 +67742,17 @@ const OAUTH_STATE_COOKIE = "forge_oauth_state";
67483
67742
  const OAUTH_VERIFIER_COOKIE = "forge_oauth_verifier";
67484
67743
  //#endregion
67485
67744
  //#region src/auth/http/cloudOwnerOAuth.ts
67745
+ const FORGE_CLOUD_SESSION_GRANT_COOKIE = "forge_cloud_session_grant";
67746
+ function resolveCloudOwnerSessionGrantCookie(input) {
67747
+ const token = input.token?.trim();
67748
+ if (!token || !input.expiresAt) return null;
67749
+ const expiresAt = new Date(input.expiresAt);
67750
+ if (!Number.isFinite(expiresAt.getTime()) || expiresAt.getTime() <= Date.now()) return null;
67751
+ return {
67752
+ token,
67753
+ expiresAt
67754
+ };
67755
+ }
67486
67756
  function validateCloudOwnerCallbackAssertion(input) {
67487
67757
  if (input.serverAuth.isForgeCloudShell?.() !== true) return succeed(null);
67488
67758
  if (!input.assertionToken || !input.serverAuth.verifyForgeCloudOwnerAssertion) return fail(new AuthError({
@@ -67546,8 +67816,19 @@ function cloudOwnerAssertionCookieOptions(assertion) {
67546
67816
  secure: true
67547
67817
  };
67548
67818
  }
67549
- function cloudOwnerOAuthResponse(homeUrl, assertion) {
67550
- return redirect(homeUrl, { status: 302 }).pipe(setCookie(FORGE_CLOUD_OWNER_ASSERTION_COOKIE, assertion.token, cloudOwnerAssertionCookieOptions(assertion)), flatMap(setCookie(OAUTH_STATE_COOKIE, "", expiredCallbackCookie)), flatMap(setCookie(OAUTH_VERIFIER_COOKIE, "", expiredCallbackCookie)));
67819
+ function cloudOwnerSessionGrantCookieOptions(grant) {
67820
+ return {
67821
+ expires: grant.expiresAt,
67822
+ httpOnly: true,
67823
+ path: "/api/auth",
67824
+ sameSite: "lax",
67825
+ secure: true
67826
+ };
67827
+ }
67828
+ function cloudOwnerOAuthResponse(homeUrl, assertion, sessionGrant) {
67829
+ let response = redirect(homeUrl, { status: 302 }).pipe(setCookie(FORGE_CLOUD_OWNER_ASSERTION_COOKIE, assertion.token, cloudOwnerAssertionCookieOptions(assertion)));
67830
+ if (sessionGrant) response = response.pipe(flatMap(setCookie(FORGE_CLOUD_SESSION_GRANT_COOKIE, sessionGrant.token, cloudOwnerSessionGrantCookieOptions(sessionGrant))));
67831
+ return response.pipe(flatMap(setCookie(OAUTH_STATE_COOKIE, "", expiredCallbackCookie)), flatMap(setCookie(OAUTH_VERIFIER_COOKIE, "", expiredCallbackCookie)));
67551
67832
  }
67552
67833
  //#endregion
67553
67834
  //#region src/auth/http.ts
@@ -67597,6 +67878,56 @@ const authSessionRouteLayer = add$1("GET", "/api/auth/session", gen(function* ()
67597
67878
  }));
67598
67879
  return response;
67599
67880
  }));
67881
+ const authSessionRenewRouteLayer = add$1("POST", "/api/auth/session/renew", gen(function* () {
67882
+ const request = yield* HttpServerRequest;
67883
+ const serverAuth = yield* ServerAuth;
67884
+ if (serverAuth.isForgeCloudShell?.() !== true) return yield* new AuthError({
67885
+ message: "Session renewal is unavailable.",
67886
+ status: 401
67887
+ });
67888
+ const sessionGrant = request.cookies[FORGE_CLOUD_SESSION_GRANT_COOKIE]?.trim();
67889
+ if (!sessionGrant) return yield* new AuthError({
67890
+ message: "Session renewal is unavailable.",
67891
+ status: 401
67892
+ });
67893
+ const credentials = yield* resolveCmmdTokenBundleCredentials(yield* readCmmdJsonResponse(yield* tryPromise({
67894
+ try: () => fetch(resolveCommandAppEndpoint("/api/mcp-server/oauth/token"), {
67895
+ method: "POST",
67896
+ headers: { "content-type": "application/json" },
67897
+ body: JSON.stringify({
67898
+ grant_type: "forge_session_grant",
67899
+ grant_token: sessionGrant,
67900
+ client_id: "cmmd-forge-web"
67901
+ })
67902
+ }),
67903
+ catch: (cause) => new AuthError({
67904
+ message: "Failed to renew the Forge Cloud session with CMMD.",
67905
+ status: 500,
67906
+ cause
67907
+ })
67908
+ }), "Failed to renew the Forge Cloud session with CMMD."));
67909
+ const { cmmdIdentity, userId, patToStore } = credentials;
67910
+ if (!cmmdIdentity || !userId || !patToStore) return yield* new AuthError({
67911
+ message: "CMMD did not return a complete renewed Forge session.",
67912
+ status: 500
67913
+ });
67914
+ const assertion = yield* validateCloudOwnerCallbackAssertion({
67915
+ serverAuth,
67916
+ assertionToken: credentials.forgeOwnerAssertion,
67917
+ authenticatedUserId: userId
67918
+ });
67919
+ if (!assertion) return yield* die$1(/* @__PURE__ */ new Error("Expected a renewed Forge Cloud owner assertion."));
67920
+ yield* persistCloudOwnerArtifacts({
67921
+ assertion,
67922
+ accessToken: patToStore,
67923
+ identity: cmmdIdentity,
67924
+ jwtToken: credentials.jwtToStore,
67925
+ refreshToken: credentials.refreshToken,
67926
+ sessionGrant,
67927
+ sessionGrantExpiresAt: void 0
67928
+ });
67929
+ return yield* jsonUnsafe({ renewed: true }, { status: 200 }).pipe(setCookie(FORGE_CLOUD_OWNER_ASSERTION_COOKIE, assertion.token, cloudOwnerAssertionCookieOptions(assertion)));
67930
+ }).pipe(catchTag("AuthError", (error) => respondToAuthError(error))));
67600
67931
  const authLogoutRouteLayer = add$1("POST", "/api/auth/logout", gen(function* () {
67601
67932
  const request = yield* HttpServerRequest;
67602
67933
  const serverAuth = yield* ServerAuth;
@@ -67619,14 +67950,24 @@ const authLogoutRouteLayer = add$1("POST", "/api/auth/logout", gen(function* ()
67619
67950
  sameSite: "lax",
67620
67951
  secure
67621
67952
  }));
67622
- if (cloudShell) response = yield* response.pipe(setCookie(FORGE_CLOUD_OWNER_ASSERTION_COOKIE, "", {
67623
- expires: /* @__PURE__ */ new Date(0),
67624
- httpOnly: true,
67625
- maxAge: 0,
67626
- path: "/",
67627
- sameSite: "lax",
67628
- secure: true
67629
- }));
67953
+ if (cloudShell) {
67954
+ response = yield* response.pipe(setCookie(FORGE_CLOUD_OWNER_ASSERTION_COOKIE, "", {
67955
+ expires: /* @__PURE__ */ new Date(0),
67956
+ httpOnly: true,
67957
+ maxAge: 0,
67958
+ path: "/",
67959
+ sameSite: "lax",
67960
+ secure: true
67961
+ }));
67962
+ response = yield* response.pipe(setCookie(FORGE_CLOUD_SESSION_GRANT_COOKIE, "", {
67963
+ expires: /* @__PURE__ */ new Date(0),
67964
+ httpOnly: true,
67965
+ maxAge: 0,
67966
+ path: "/api/auth",
67967
+ sameSite: "lax",
67968
+ secure: true
67969
+ }));
67970
+ }
67630
67971
  response = yield* pipeExpireLegacySessionCookie(sessions.cookieName, succeed(response), { secure });
67631
67972
  if (sessions.cookieName !== "forge_session") response = yield* response.pipe(setCookie(BASE_SESSION_COOKIE_NAME, "", {
67632
67973
  expires: /* @__PURE__ */ new Date(0),
@@ -67649,7 +67990,8 @@ const CommandBootstrapInput = Struct({
67649
67990
  refreshToken: optionalKey(String$1),
67650
67991
  userId: optionalKey(Number$1),
67651
67992
  displayName: optionalKey(String$1),
67652
- user: optionalKey(Unknown)
67993
+ user: optionalKey(Unknown),
67994
+ clientDeviceType: optionalKey(Literals(["desktop", "mobile"]))
67653
67995
  });
67654
67996
  const CommandOAuthDeviceInput = Struct({
67655
67997
  client_id: String$1,
@@ -68115,6 +68457,11 @@ function commandOAuthProxyStatus(path, status, body) {
68115
68457
  }
68116
68458
  const authBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap", gen(function* () {
68117
68459
  const request = yield* HttpServerRequest;
68460
+ const limited = rateLimitOrRespond({
68461
+ routeClass: "auth-bootstrap",
68462
+ clientKey: clientKeyFromRequest(request)
68463
+ });
68464
+ if (limited) return limited;
68118
68465
  const serverAuth = yield* ServerAuth;
68119
68466
  const sessions = yield* SessionCredentialService;
68120
68467
  const payload = yield* schemaBodyJson(AuthBootstrapInput).pipe(mapError((cause) => new AuthError({
@@ -68132,6 +68479,11 @@ const authBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap", gen(functio
68132
68479
  }).pipe(catchTag("AuthError", (error) => respondToAuthError(error))));
68133
68480
  const authCommandBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap/command", gen(function* () {
68134
68481
  const request = yield* HttpServerRequest;
68482
+ const limited = rateLimitOrRespond({
68483
+ routeClass: "auth-bootstrap",
68484
+ clientKey: clientKeyFromRequest(request)
68485
+ });
68486
+ if (limited) return limited;
68135
68487
  const serverAuth = yield* ServerAuth;
68136
68488
  const sessions = yield* SessionCredentialService;
68137
68489
  if (serverAuth.isForgeCloudShell?.() === true) return yield* new AuthError({
@@ -68180,6 +68532,11 @@ const authCommandBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap/comman
68180
68532
  }).pipe(catchTag("AuthError", (error) => respondToAuthError(error))));
68181
68533
  const authCommandBearerBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap/command/bearer", gen(function* () {
68182
68534
  const request = yield* HttpServerRequest;
68535
+ const limited = rateLimitOrRespond({
68536
+ routeClass: "auth-bootstrap",
68537
+ clientKey: clientKeyFromRequest(request)
68538
+ });
68539
+ if (limited) return limited;
68183
68540
  const serverAuth = yield* ServerAuth;
68184
68541
  const sessions = yield* SessionCredentialService;
68185
68542
  if (serverAuth.isForgeCloudShell?.() === true) return yield* new AuthError({
@@ -68197,13 +68554,10 @@ const authCommandBearerBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap/
68197
68554
  method: "bearer-session-token",
68198
68555
  role: "owner",
68199
68556
  subject: sessionSubject,
68200
- client: {
68201
- ...deriveAuthClientMetadata({
68202
- request,
68203
- label: "Forge Desktop Cloud"
68204
- }),
68205
- deviceType: "desktop"
68206
- }
68557
+ client: resolveCommandBootstrapClient({
68558
+ request,
68559
+ clientDeviceType: payload.clientDeviceType
68560
+ })
68207
68561
  }).pipe(mapError((cause) => new AuthError({
68208
68562
  message: "Failed to issue authenticated bearer session.",
68209
68563
  cause
@@ -68213,7 +68567,10 @@ const authCommandBearerBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap/
68213
68567
  payload,
68214
68568
  cmmdIdentity: validated.cmmdIdentity
68215
68569
  });
68216
- if (!payload.refreshToken?.trim()) yield* clearDesktopBearerRefreshTokensForSubject(sessionSubject).pipe(mapError((cause) => new AuthError({
68570
+ if ((payload.clientDeviceType ?? "desktop") === "desktop" && !payload.refreshToken?.trim()) yield* clearDesktopBearerRefreshTokensForSubject({
68571
+ subject: sessionSubject,
68572
+ clientUserAgent: request.headers["user-agent"]
68573
+ }).pipe(mapError((cause) => new AuthError({
68217
68574
  message: "Failed to retire legacy desktop refresh credentials.",
68218
68575
  status: 500,
68219
68576
  cause
@@ -68228,6 +68585,11 @@ const authCommandBearerBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap/
68228
68585
  }, { status: 200 });
68229
68586
  }).pipe(catchTag("AuthError", (error) => respondToAuthError(error))));
68230
68587
  const authCmmdOAuthDeviceRouteLayer = add$1("POST", "/api/auth/cmmd-oauth/device", gen(function* () {
68588
+ const limited = rateLimitOrRespond({
68589
+ routeClass: "auth-device-poll",
68590
+ clientKey: clientKeyFromRequest(yield* HttpServerRequest)
68591
+ });
68592
+ if (limited) return limited;
68231
68593
  const payload = yield* schemaBodyJson(CommandOAuthDeviceInput).pipe(mapError((cause) => new AuthError({
68232
68594
  message: "Invalid CMMD OAuth device payload.",
68233
68595
  status: 400,
@@ -68239,7 +68601,17 @@ const authCmmdOAuthDeviceRouteLayer = add$1("POST", "/api/auth/cmmd-oauth/device
68239
68601
  ...payload.scope ? { scope: payload.scope } : {}
68240
68602
  });
68241
68603
  }).pipe(catchTag("AuthError", (error) => respondToAuthError(error))));
68604
+ /**
68605
+ * The device-code poll. Called every `interval` seconds by design (RFC
68606
+ * 8628), which is exactly why FOR-36706 puts a limiter on it: a client that
68607
+ * ignores `slow_down` — a bug, not a person — has no other backstop here.
68608
+ */
68242
68609
  const authCmmdOAuthTokenRouteLayer = add$1("POST", "/api/auth/cmmd-oauth/token", gen(function* () {
68610
+ const limited = rateLimitOrRespond({
68611
+ routeClass: "auth-device-poll",
68612
+ clientKey: clientKeyFromRequest(yield* HttpServerRequest)
68613
+ });
68614
+ if (limited) return limited;
68243
68615
  const payload = yield* schemaBodyJson(CommandOAuthTokenInput).pipe(mapError((cause) => new AuthError({
68244
68616
  message: "Invalid CMMD OAuth token payload.",
68245
68617
  status: 400,
@@ -68281,6 +68653,14 @@ function completeCloudOwnerPasswordLogin(input) {
68281
68653
  authenticatedUserId: userId
68282
68654
  });
68283
68655
  if (!assertion) return yield* die$1(/* @__PURE__ */ new Error("Expected a Forge Cloud owner assertion in cloud shell mode."));
68656
+ const sessionGrant = resolveCloudOwnerSessionGrantCookie({
68657
+ token: credentials.sessionGrant,
68658
+ expiresAt: credentials.sessionGrantExpiresAt
68659
+ });
68660
+ if (!sessionGrant) return yield* new AuthError({
68661
+ message: "CMMD did not return a durable Forge Cloud session grant.",
68662
+ status: 500
68663
+ });
68284
68664
  yield* persistCloudOwnerArtifacts({
68285
68665
  assertion,
68286
68666
  accessToken: patToStore,
@@ -68290,10 +68670,12 @@ function completeCloudOwnerPasswordLogin(input) {
68290
68670
  sessionGrant: credentials.sessionGrant,
68291
68671
  sessionGrantExpiresAt: credentials.sessionGrantExpiresAt
68292
68672
  });
68293
- return yield* jsonUnsafe({
68673
+ let response = jsonUnsafe({
68294
68674
  authenticated: true,
68295
68675
  cmmdIdentity
68296
68676
  }, { status: 200 }).pipe(setCookie(FORGE_CLOUD_OWNER_ASSERTION_COOKIE, assertion.token, cloudOwnerAssertionCookieOptions(assertion)));
68677
+ if (sessionGrant) response = response.pipe(flatMap(setCookie(FORGE_CLOUD_SESSION_GRANT_COOKIE, sessionGrant.token, cloudOwnerSessionGrantCookieOptions(sessionGrant))));
68678
+ return yield* response;
68297
68679
  });
68298
68680
  }
68299
68681
  const authCmmdPasswordTokenRouteLayer = add$1("POST", "/api/auth/cmmd-password/token", gen(function* () {
@@ -68467,6 +68849,14 @@ const authWebCallbackRouteLayer = add$1("GET", "/api/auth/callback", gen(functio
68467
68849
  assertionToken: forgeOwnerAssertion,
68468
68850
  authenticatedUserId: userId
68469
68851
  });
68852
+ const cloudSessionGrant = cloudOwnerAssertion ? resolveCloudOwnerSessionGrantCookie({
68853
+ token: sessionGrant,
68854
+ expiresAt: sessionGrantExpiresAt
68855
+ }) : null;
68856
+ if (cloudOwnerAssertion && !cloudSessionGrant) return yield* new AuthError({
68857
+ message: "CMMD did not return a durable Forge Cloud session grant.",
68858
+ status: 500
68859
+ });
68470
68860
  const sessionSubject = resolveCommandOAuthSubject(userId);
68471
68861
  const issued = cloudOwnerAssertion ? null : yield* sessions.issue({
68472
68862
  method: "browser-session-cookie",
@@ -68498,7 +68888,7 @@ const authWebCallbackRouteLayer = add$1("GET", "/api/auth/callback", gen(functio
68498
68888
  else yield* persistLocalOAuthArtifacts(persistenceInput);
68499
68889
  const homeUrl = `${publicUrl}/`;
68500
68890
  const secureCookie = shouldUseSecureCookie(publicUrl);
68501
- if (cloudOwnerAssertion) return yield* cloudOwnerOAuthResponse(homeUrl, cloudOwnerAssertion);
68891
+ if (cloudOwnerAssertion) return yield* cloudOwnerOAuthResponse(homeUrl, cloudOwnerAssertion, cloudSessionGrant);
68502
68892
  if (!issued) return yield* die$1(/* @__PURE__ */ new Error("Expected a local session outside Forge Cloud."));
68503
68893
  return yield* pipeExpireLegacySessionCookie(sessions.cookieName, redirect(homeUrl, { status: 302 }).pipe(setCookie(sessions.cookieName, issued.token, {
68504
68894
  expires: toDate(issued.expiresAt),
@@ -68516,6 +68906,11 @@ const authWebCallbackRouteLayer = add$1("GET", "/api/auth/callback", gen(functio
68516
68906
  }).pipe(catchTag("AuthError", (error) => respondToAuthError(error))));
68517
68907
  const authBearerBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap/bearer", gen(function* () {
68518
68908
  const request = yield* HttpServerRequest;
68909
+ const limited = rateLimitOrRespond({
68910
+ routeClass: "auth-bootstrap",
68911
+ clientKey: clientKeyFromRequest(request)
68912
+ });
68913
+ if (limited) return limited;
68519
68914
  const serverAuth = yield* ServerAuth;
68520
68915
  const payload = yield* schemaBodyJson(AuthBootstrapInput).pipe(mapError((cause) => new AuthError({
68521
68916
  message: "Invalid bootstrap payload.",
@@ -68547,6 +68942,11 @@ const authBearerBootstrapRouteLayer = add$1("POST", "/api/auth/bootstrap/bearer"
68547
68942
  */
68548
68943
  const authOAuthTokenExchangeRouteLayer = add$1("POST", "/oauth/token", gen(function* () {
68549
68944
  const request = yield* HttpServerRequest;
68945
+ const limited = rateLimitOrRespond({
68946
+ routeClass: "auth-token-exchange",
68947
+ clientKey: clientKeyFromRequest(request)
68948
+ });
68949
+ if (limited) return limited;
68550
68950
  const serverAuth = yield* ServerAuth;
68551
68951
  const payload = yield* schemaBodyUrlParams(OAuthTokenExchangeInput).pipe(mapError((cause) => new AuthError({
68552
68952
  message: "Invalid token exchange payload.",
@@ -70518,6 +70918,11 @@ function redeemCmmdSessionGrant(grantToken) {
70518
70918
  };
70519
70919
  }).catch(() => null);
70520
70920
  }
70921
+ const REJECTED_OAUTH_ERROR_CODES = new Set([
70922
+ "invalid_grant",
70923
+ "invalid_client",
70924
+ "invalid_request"
70925
+ ]);
70521
70926
  /**
70522
70927
  * Rotate a CMMD refresh token into a fresh access JWT.
70523
70928
  *
@@ -70530,7 +70935,7 @@ function redeemCmmdSessionGrant(grantToken) {
70530
70935
  * code are what identify the failure, and `invalid_grant` in particular means
70531
70936
  * the token was already spent, which is a different fix from a 500.
70532
70937
  */
70533
- function refreshCmmdJwt(refreshToken) {
70938
+ function refreshCmmdJwt(refreshToken, observer) {
70534
70939
  return fetch(new URL("/api/mcp-server/oauth/token", resolveCommandAppUrl()).toString(), {
70535
70940
  method: "POST",
70536
70941
  headers: { "Content-Type": "application/json" },
@@ -70541,7 +70946,9 @@ function refreshCmmdJwt(refreshToken) {
70541
70946
  })
70542
70947
  }).then(async (response) => {
70543
70948
  if (!response.ok) {
70544
- console.warn(`[cmmd-auth] CMMD refused a JWT rotation with ${response.status}${await readOAuthError(response)}`);
70949
+ const errorCode = await readOAuthErrorCode(response);
70950
+ if (errorCode !== null && REJECTED_OAUTH_ERROR_CODES.has(errorCode)) observer?.onCredentialRejected?.();
70951
+ console.warn(`[cmmd-auth] CMMD refused a JWT rotation with ${response.status}${errorCode === null ? "" : ` (${errorCode})`}`);
70545
70952
  return null;
70546
70953
  }
70547
70954
  const parsed = parseCmmdOAuthRefreshResponse(await response.json());
@@ -70553,20 +70960,73 @@ function refreshCmmdJwt(refreshToken) {
70553
70960
  });
70554
70961
  }
70555
70962
  /**
70556
- * The OAuth `error` code from a refusal, as a loggable suffix.
70963
+ * The OAuth `error` code from a refusal, lowercased, or null when there is none.
70557
70964
  *
70558
70965
  * Best effort by design: a body that is not JSON, or is already consumed, must
70559
- * not turn a logged failure into an unlogged one.
70966
+ * not turn a logged failure into an unlogged one. Null also reads correctly as
70967
+ * "no evidence the credential is dead", which is the safe answer for a caller
70968
+ * deciding whether to keep a refresh token.
70560
70969
  */
70561
- async function readOAuthError(response) {
70970
+ async function readOAuthErrorCode(response) {
70562
70971
  try {
70563
70972
  const code = (await response.json())["error"];
70564
- return typeof code === "string" && code.trim().length > 0 ? ` (${code.trim()})` : "";
70973
+ return typeof code === "string" && code.trim().length > 0 ? code.trim().toLowerCase() : null;
70565
70974
  } catch {
70566
- return "";
70975
+ return null;
70567
70976
  }
70568
70977
  }
70569
70978
  //#endregion
70979
+ //#region src/command/cmmdRotationFence.ts
70980
+ /**
70981
+ * True when Forge must not rotate this session's CMMD credentials.
70982
+ *
70983
+ * Fails closed: if the session row cannot be read, assume the fence applies.
70984
+ * Spending a token Forge had no right to spend is worse than one extra
70985
+ * reconnect.
70986
+ */
70987
+ function isRotationFencedSession(sessionId) {
70988
+ if (readCommandRefreshTokenForSession(sessionId)) return succeed(false);
70989
+ return isDesktopBearerAuthSession(sessionId).pipe(orElseSucceed(() => true));
70990
+ }
70991
+ //#endregion
70992
+ //#region src/cmmdConnection/cmmdCredentialRejection.ts
70993
+ /**
70994
+ * How long a recorded rejection stays usable.
70995
+ *
70996
+ * Long enough to span the resolver ladder and the response that follows it,
70997
+ * short enough that it cannot outlive the credential it describes.
70998
+ */
70999
+ const REJECTION_TTL_MS = 6e4;
71000
+ const rejectedAt = /* @__PURE__ */ new Map();
71001
+ /** Record that CMMD refused this session's credential outright. */
71002
+ function noteCmmdCredentialRejected(sessionId) {
71003
+ rejectedAt.set(sessionId, Date.now());
71004
+ }
71005
+ /** Whether CMMD refused this session's credential recently enough to act on. */
71006
+ function hasCmmdCredentialRejection(sessionId) {
71007
+ const observedAt = rejectedAt.get(sessionId);
71008
+ if (observedAt === void 0) return false;
71009
+ if (Date.now() - observedAt <= REJECTION_TTL_MS) return true;
71010
+ rejectedAt.delete(sessionId);
71011
+ return false;
71012
+ }
71013
+ /** Drop a recorded rejection, once it has been acted on or superseded. */
71014
+ function forgetCmmdCredentialRejection(sessionId) {
71015
+ rejectedAt.delete(sessionId);
71016
+ }
71017
+ /**
71018
+ * Clear a session's CMMD credentials, but only if CMMD rejected them.
71019
+ *
71020
+ * This is the only door the 401 path may use. A resolver that returned null
71021
+ * because it was not allowed to try is a policy refusal and leaves the stored
71022
+ * credential exactly where it is, so the next request can rotate it.
71023
+ */
71024
+ const clearCmmdAuthAfterCmmdRejection = (sessionId) => gen(function* () {
71025
+ if (!hasCmmdCredentialRejection(sessionId)) return;
71026
+ yield* clearUnrecoverableCmmdAuthForSession(sessionId);
71027
+ forgetCmmdCredentialRejection(sessionId);
71028
+ });
71029
+ //#endregion
70570
71030
  //#region src/command/cmmdJwtResolver.ts
70571
71031
  function usableCmmdUserJwt(token) {
70572
71032
  return isCmmdUserAccessJwt(token) ? token : null;
@@ -70579,7 +71039,7 @@ function refreshOnceForSession(sessionId, refreshToken) {
70579
71039
  return promise(() => {
70580
71040
  const existing = inFlightRefreshes.get(sessionId);
70581
71041
  if (existing) return existing;
70582
- const pending = refreshCmmdJwt(refreshToken).catch(() => null).finally(() => {
71042
+ const pending = refreshCmmdJwt(refreshToken, { onCredentialRejected: () => noteCmmdCredentialRejected(sessionId) }).catch(() => null).finally(() => {
70583
71043
  inFlightRefreshes.delete(sessionId);
70584
71044
  });
70585
71045
  inFlightRefreshes.set(sessionId, pending);
@@ -70689,13 +71149,16 @@ function trySubjectFallback(sessionId, subject, logPrefix) {
70689
71149
  * Fail-closed: never returns a non-user / malformed Bearer. Callers must map
70690
71150
  * null to CMMD_AUTH_REQUIRED and must not call CMMD upstream with a bad token.
70691
71151
  */
70692
- function resolveCmmdJwtForSession({ sessionId, subject, logPrefix = "cmmd-auth", forceRefresh = false }) {
71152
+ function resolveCmmdJwtForSession({ sessionId, subject, logPrefix = "cmmd-auth", forceRefresh = false, staleAccessToken = null }) {
70693
71153
  const cached = usableCmmdUserJwt(readCommandJwtForSession(sessionId));
70694
- if (!forceRefresh && cached && !isJwtExpiredForSession(sessionId)) return succeed(cached);
71154
+ if (cached && !isJwtExpiredForSession(sessionId)) {
71155
+ if (!forceRefresh) return succeed(cached);
71156
+ if (staleAccessToken && cached !== staleAccessToken) return succeed(cached);
71157
+ }
70695
71158
  const normalizedSubject = subject?.trim() || null;
70696
71159
  if (!normalizedSubject && !forceRefresh) return succeed(null);
70697
71160
  return gen(function* () {
70698
- if (yield* isDesktopBearerAuthSession(sessionId).pipe(orElseSucceed(() => true))) return null;
71161
+ if (yield* isRotationFencedSession(sessionId)) return null;
70699
71162
  if (forceRefresh) {
70700
71163
  const forced = yield* tryOwnCredentialRecovery(sessionId, logPrefix);
70701
71164
  if (forced) return forced;
@@ -71454,15 +71917,25 @@ function clearCmmdAuthAfterFailure(sessionId) {
71454
71917
  })));
71455
71918
  }
71456
71919
  /**
71457
- * The single "no usable CMMD credential" response: clears whatever
71458
- * unrecoverable auth state this session is holding, then returns the
71459
- * canonical `CMMD_AUTH_REQUIRED` body. Every prologue path that decides the
71460
- * session has no way to reach CMMD — a missing PAT, a JWT that fails to
71461
- * resolve, a bespoke gate elsewhere in http.ts — emits this same response so
71462
- * the Shell only ever has one `CMMD_AUTH_REQUIRED` body to branch on.
71920
+ * The single "no usable CMMD credential" response: returns the canonical
71921
+ * `CMMD_AUTH_REQUIRED` body. Every prologue path that decides the session has
71922
+ * no way to reach CMMD — a missing PAT, a JWT that fails to resolve, a bespoke
71923
+ * gate elsewhere in http.ts — emits this same response so the Shell only ever
71924
+ * has one `CMMD_AUTH_REQUIRED` body to branch on.
71925
+ *
71926
+ * It used to delete the session's stored credentials on the way out, and that
71927
+ * was the wrong reading of its own argument. Most of the paths that reach here
71928
+ * are refusals, not rejections: the resolver was fenced, the subject was
71929
+ * missing, a gate said no. A phone would arrive holding a refresh token good
71930
+ * for the next three months, be refused by a rule, and have that token deleted
71931
+ * as "unrecoverable". The clear now happens only where CMMD has actually
71932
+ * refused the credential, which `clearCmmdAuthAfterCmmdRejection` checks.
71463
71933
  */
71464
71934
  function respondCmmdConnectionRequired(sessionId) {
71465
- return clearCmmdAuthAfterFailure(sessionId).pipe(as$3(buildProxyErrorResponse(new CmmdProxyAuthenticationRequiredError("CMMD connection required. Reconnect CMMD to continue."))));
71935
+ return clearCmmdAuthAfterCmmdRejection(sessionId).pipe(catch_((error) => logWarning$1("Failed to clear CMMD auth that CMMD itself rejected.", {
71936
+ cause: error,
71937
+ sessionId
71938
+ })), as$3(buildProxyErrorResponse(new CmmdProxyAuthenticationRequiredError("CMMD connection required. Reconnect CMMD to continue."))));
71466
71939
  }
71467
71940
  async function dispatchToProxy(method, jwt, cmmdPath, rawBody, orgId, searchParams) {
71468
71941
  return cmmdGateway.request({
@@ -71518,6 +71991,11 @@ function buildProxyRouteLayer(route) {
71518
71991
  const handler = gen(function* () {
71519
71992
  const req = yield* HttpServerRequest;
71520
71993
  const session = yield* (yield* ServerAuth).authenticateHttpRequest(req);
71994
+ const limited = rateLimitOrRespond({
71995
+ routeClass: "cmmd-proxy",
71996
+ clientKey: session.sessionId
71997
+ });
71998
+ if (limited) return limited;
71521
71999
  const pathParams = yield* params;
71522
72000
  const cmmdPath = substitutePath(route.cmmdPath, pathParams);
71523
72001
  if (!cmmdPath) return jsonUnsafe({ error: "Missing required path parameter." }, { status: 400 });
@@ -71577,6 +72055,11 @@ function buildProxyHandlerEffect(route) {
71577
72055
  return gen(function* () {
71578
72056
  const req = yield* HttpServerRequest;
71579
72057
  const session = yield* (yield* ServerAuth).authenticateHttpRequest(req);
72058
+ const limited = rateLimitOrRespond({
72059
+ routeClass: "cmmd-proxy",
72060
+ clientKey: session.sessionId
72061
+ });
72062
+ if (limited) return limited;
71580
72063
  if (!readCommandAccessTokenForSession(session.sessionId)) return yield* respondCmmdConnectionRequired(session.sessionId);
71581
72064
  const parsed = yield* route.parse(req);
71582
72065
  if (!parsed.ok) return jsonUnsafe(parsed.body, { status: parsed.status });
@@ -75215,7 +75698,7 @@ var require_node_gyp_build = /* @__PURE__ */ __commonJSMin(((exports, module) =>
75215
75698
  var fs$6 = __require("fs");
75216
75699
  var path$5 = __require("path");
75217
75700
  var url$1 = __require("url");
75218
- var os$2 = __require("os");
75701
+ var os$3 = __require("os");
75219
75702
  var runtimeRequire = typeof __webpack_require__ === "function" ? __non_webpack_require__ : __require;
75220
75703
  var vars = process.config && process.config.variables || {};
75221
75704
  var prebuildsOnly = !!process.env.PREBUILDS_ONLY;
@@ -75223,8 +75706,8 @@ var require_node_gyp_build = /* @__PURE__ */ __commonJSMin(((exports, module) =>
75223
75706
  var abi = versions.modules;
75224
75707
  if (versions.deno || process.isBun) abi = "unsupported";
75225
75708
  var runtime = isElectron() ? "electron" : isNwjs() ? "node-webkit" : "node";
75226
- var arch = process.env.npm_config_arch || os$2.arch();
75227
- var platform = process.env.npm_config_platform || os$2.platform();
75709
+ var arch = process.env.npm_config_arch || os$3.arch();
75710
+ var platform = process.env.npm_config_platform || os$3.platform();
75228
75711
  var libc = process.env.LIBC || (isMusl(platform) ? "musl" : "glibc");
75229
75712
  var armv = process.env.ARM_VERSION || (arch === "arm64" ? "8" : vars.arm_version) || "";
75230
75713
  var uv = (versions.uv || "").split(".")[0];
@@ -83346,12 +83829,12 @@ const makePullRequestService = gen(function* () {
83346
83829
  }));
83347
83830
  const submitReview = (input) => requireProject(input).pipe(flatMap((project) => {
83348
83831
  const review = project.api.capabilities.review;
83349
- if (!review.verdicts.includes(input.verdict)) return refuse(`This host cannot ${VERDICT_LABELS[input.verdict]} a change request.`);
83350
- if (input.comments.length > 0 && !review.inlineComment) return refuse("This host cannot comment on a line of a change request.");
83351
- if (input.verdict !== "approve" && input.body.trim().length === 0 && input.comments.length === 0) return refuse("A review needs a summary or at least one comment.");
83832
+ if (!review.verdicts.includes(input.verdict)) return refuse$1(`This host cannot ${VERDICT_LABELS[input.verdict]} a change request.`);
83833
+ if (input.comments.length > 0 && !review.inlineComment) return refuse$1("This host cannot comment on a line of a change request.");
83834
+ if (input.verdict !== "approve" && input.body.trim().length === 0 && input.comments.length === 0) return refuse$1("A review needs a summary or at least one comment.");
83352
83835
  return viewerPermissionsOf(project, input, "submitReview").pipe(flatMap((viewer) => {
83353
- if (!viewer.verdicts.includes(input.verdict)) return refuse(`You need write access on this repository to ${VERDICT_LABELS[input.verdict]} a change request.`);
83354
- if (input.comments.length > 0 && !viewer.comment) return refuse("You need write access on this repository to comment on a line of a change request.");
83836
+ if (!viewer.verdicts.includes(input.verdict)) return refuse$1(`You need write access on this repository to ${VERDICT_LABELS[input.verdict]} a change request.`);
83837
+ if (input.comments.length > 0 && !viewer.comment) return refuse$1("You need write access on this repository to comment on a line of a change request.");
83355
83838
  return project.api.submitReview({
83356
83839
  projectId: project.project.id,
83357
83840
  cwd: project.project.workspaceRoot,
@@ -83748,7 +84231,7 @@ const toEntry = (input) => {
83748
84231
  ...input.item.reviewDecision === void 0 || input.item.reviewDecision === null ? {} : { reviewDecision: input.item.reviewDecision }
83749
84232
  };
83750
84233
  };
83751
- const refuse = (detail) => fail(new PullRequestOperationError({
84234
+ const refuse$1 = (detail) => fail(new PullRequestOperationError({
83752
84235
  operation: "submitReview",
83753
84236
  detail
83754
84237
  }));
@@ -93406,7 +93889,7 @@ const launchDetached = (launch) => gen(function* () {
93406
93889
  });
93407
93890
  const OpenLive = effect(Open, gen(function* () {
93408
93891
  const open = yield* tryPromise({
93409
- try: () => import("./open-D8mGyWmt.mjs"),
93892
+ try: () => import("./open-FIgTDosf.mjs"),
93410
93893
  catch: (cause) => new OpenError$1({
93411
93894
  message: "failed to load browser opener",
93412
93895
  cause
@@ -93590,6 +94073,166 @@ const admitNormalizedDispatchCommand = (command, admit) => uninterruptibleMask$1
93590
94073
  };
93591
94074
  }));
93592
94075
  //#endregion
94076
+ //#region src/orchestration/threadDetailWindow.ts
94077
+ function earliest(current, candidate) {
94078
+ return current === void 0 || candidate < current ? candidate : current;
94079
+ }
94080
+ /**
94081
+ * Every turn in the thread, oldest first.
94082
+ *
94083
+ * Ordering is by the earliest timestamp across all four windowed collections
94084
+ * rather than by message order alone: a turn can open with an activity (a tool
94085
+ * call before any text) and a checkpoint-only turn has no message at all.
94086
+ * Ties break on turn id so the order is total and a cursor means the same thing
94087
+ * on every read.
94088
+ */
94089
+ function orderThreadTurns(thread) {
94090
+ const anchors = /* @__PURE__ */ new Map();
94091
+ const touch = (turnId, at, isUserMessage) => {
94092
+ if (turnId === null) return;
94093
+ const existing = anchors.get(turnId);
94094
+ anchors.set(turnId, {
94095
+ at: earliest(existing?.at, at),
94096
+ hasUserMessage: (existing?.hasUserMessage ?? false) || isUserMessage
94097
+ });
94098
+ };
94099
+ for (const message of thread.messages) touch(message.turnId, message.createdAt, message.role === "user");
94100
+ for (const activity of thread.activities) touch(activity.turnId, activity.createdAt, false);
94101
+ for (const plan of thread.proposedPlans) touch(plan.turnId, plan.createdAt, false);
94102
+ for (const checkpoint of thread.checkpoints) touch(checkpoint.turnId, checkpoint.completedAt, false);
94103
+ return Array.from(anchors, ([turnId, anchor]) => ({
94104
+ turnId,
94105
+ ...anchor
94106
+ })).toSorted((left, right) => left.at === right.at ? left.turnId.localeCompare(right.turnId) : left.at < right.at ? -1 : 1);
94107
+ }
94108
+ /**
94109
+ * Index of the first turn to include, counting back `turnLimit` user-anchored
94110
+ * turns from the end of `ordered`. Returns 0 when the thread holds fewer user
94111
+ * turns than the limit, which is how a short thread comes back whole.
94112
+ */
94113
+ function windowStartIndex(ordered, turnLimit) {
94114
+ let userTurns = 0;
94115
+ for (let index = ordered.length - 1; index >= 0; index -= 1) {
94116
+ const anchor = ordered[index];
94117
+ if (anchor !== void 0 && anchor.hasUserMessage) {
94118
+ userTurns += 1;
94119
+ if (userTurns === turnLimit) return index;
94120
+ }
94121
+ }
94122
+ return 0;
94123
+ }
94124
+ /**
94125
+ * The turns a windowed read returns.
94126
+ *
94127
+ * `beforeCursor` bounds the read to turns strictly older than a cursor an
94128
+ * earlier read handed out, which is what "load earlier" sends back. A cursor
94129
+ * naming a turn this thread no longer holds, after a revert, say, yields an
94130
+ * empty page rather than the whole thread: returning everything would be a
94131
+ * silent full download at the exact moment the client asked for less.
94132
+ */
94133
+ function selectWindowTurnIds(ordered, window) {
94134
+ const cursorIndex = window.beforeCursor === void 0 ? ordered.length : ordered.findIndex((anchor) => anchor.turnId === window.beforeCursor);
94135
+ if (cursorIndex < 0) return {
94136
+ turnIds: /* @__PURE__ */ new Set(),
94137
+ beforeCursor: null,
94138
+ hasMore: false,
94139
+ includeUnanchored: false
94140
+ };
94141
+ const candidates = ordered.slice(0, cursorIndex);
94142
+ const startIndex = window.turnLimit === void 0 ? 0 : windowStartIndex(candidates, window.turnLimit);
94143
+ const included = candidates.slice(startIndex);
94144
+ const oldest = included[0];
94145
+ return {
94146
+ turnIds: new Set(included.map((anchor) => anchor.turnId)),
94147
+ beforeCursor: oldest?.turnId ?? null,
94148
+ hasMore: startIndex > 0,
94149
+ includeUnanchored: startIndex === 0
94150
+ };
94151
+ }
94152
+ function keepsTurn(selection, turnId) {
94153
+ return turnId === null ? selection.includeUnanchored : selection.turnIds.has(turnId);
94154
+ }
94155
+ /**
94156
+ * Applies a turn window to a full thread detail read.
94157
+ *
94158
+ * A request with neither field is a full read and comes back untouched with no
94159
+ * page, which is exactly what a pre-pagination client already receives. Only
94160
+ * the four turn-scoped collections are filtered; thread metadata, the session,
94161
+ * and `latestTurn` describe the thread as a whole and stay as they are, so a
94162
+ * windowed snapshot still renders a correct header and a correct running state.
94163
+ */
94164
+ function windowThreadDetail(input) {
94165
+ const { thread, window, snapshotSequence } = input;
94166
+ if (window.turnLimit === void 0 && window.beforeCursor === void 0) return {
94167
+ thread,
94168
+ page: void 0
94169
+ };
94170
+ const selection = selectWindowTurnIds(orderThreadTurns(thread), window);
94171
+ return {
94172
+ thread: {
94173
+ ...thread,
94174
+ messages: thread.messages.filter((row) => keepsTurn(selection, row.turnId)),
94175
+ activities: thread.activities.filter((row) => keepsTurn(selection, row.turnId)),
94176
+ proposedPlans: thread.proposedPlans.filter((row) => keepsTurn(selection, row.turnId)),
94177
+ checkpoints: thread.checkpoints.filter((row) => keepsTurn(selection, row.turnId)),
94178
+ ...thread.turnModelSelections === void 0 ? {} : { turnModelSelections: thread.turnModelSelections.filter((row) => keepsTurn(selection, row.turnId)) }
94179
+ },
94180
+ page: {
94181
+ beforeCursor: selection.hasMore ? selection.beforeCursor : null,
94182
+ hasMore: selection.hasMore,
94183
+ snapshotSequence
94184
+ }
94185
+ };
94186
+ }
94187
+ /** Whether a subscriber's cursor can be served by replay rather than a snapshot. */
94188
+ function canResumeFromSequence(input) {
94189
+ if (input.afterSequence > input.currentSequence) return false;
94190
+ return input.currentSequence - input.afterSequence <= (input.maxGap ?? 2e3);
94191
+ }
94192
+ function snapshotCatchUp(snapshot) {
94193
+ return {
94194
+ items: [{
94195
+ kind: "snapshot",
94196
+ snapshot
94197
+ }],
94198
+ watermark: snapshot.snapshotSequence
94199
+ };
94200
+ }
94201
+ function replayCatchUp(events, afterSequence) {
94202
+ return {
94203
+ items: events.map((event) => ({
94204
+ kind: "event",
94205
+ event
94206
+ })),
94207
+ watermark: events.at(-1)?.sequence ?? afterSequence
94208
+ };
94209
+ }
94210
+ /**
94211
+ * Builds the frames that precede live events: either the replayed catch-up or
94212
+ * the snapshot, plus the completion marker when one was asked for.
94213
+ */
94214
+ const buildCatchUp = fn$1("threadSubscription.buildCatchUp")(function* (input, sources) {
94215
+ const afterSequence = input.afterSequence;
94216
+ if (afterSequence === void 0) return snapshotCatchUp(yield* sources.loadSnapshot);
94217
+ if (!canResumeFromSequence({
94218
+ afterSequence,
94219
+ currentSequence: yield* sources.currentSequence
94220
+ })) return snapshotCatchUp(yield* sources.loadSnapshot);
94221
+ const replayed = yield* runCollect(sources.replayEvents(afterSequence));
94222
+ return replayCatchUp(Array.from(replayed), afterSequence);
94223
+ });
94224
+ function makeThreadSubscriptionStream(input, sources) {
94225
+ return unwrap$1(gen(function* () {
94226
+ const liveEvents = yield* sources.subscribeLiveEvents;
94227
+ const catchUp = yield* buildCatchUp(input, sources);
94228
+ const marker = input.requestCompletionMarker === true ? [{ kind: "synchronized" }] : [];
94229
+ return concat$2(fromArray([...catchUp.items, ...marker]), liveEvents.pipe(filter$2((event) => event.sequence > catchUp.watermark), map$7((event) => ({
94230
+ kind: "event",
94231
+ event
94232
+ }))));
94233
+ }));
94234
+ }
94235
+ //#endregion
93593
94236
  //#region src/orchestration/bootstrapCommandCoordinator.ts
93594
94237
  const MAX_COMPLETED_COMMANDS_PER_ENGINE = 256;
93595
94238
  const commandsByEngine = /* @__PURE__ */ new WeakMap();
@@ -97243,6 +97886,26 @@ function serializeNativeToolActivity(activity) {
97243
97886
  }
97244
97887
  };
97245
97888
  }
97889
+ function serializeWorktreeLaunchActivity(activity) {
97890
+ if (decodeWorktreeLaunchActivityKind(activity.kind) === null) return null;
97891
+ const summary = text(activity.summary, 4e3);
97892
+ if (summary === null) return null;
97893
+ return {
97894
+ sourceSequence: null,
97895
+ event: {
97896
+ eventId: `activity:${activity.id}`,
97897
+ occurredAt: canonicalRuntimeEventTimestamp(activity.createdAt),
97898
+ ...eventTurn(activity.turnId),
97899
+ kind: "tool_call",
97900
+ summary,
97901
+ status: "completed",
97902
+ payload: encodeCmmdRuntimeWorktreeLaunchActivityPayload(activity.kind, activity.payload)
97903
+ }
97904
+ };
97905
+ }
97906
+ function decodeWorktreeLaunchActivityKind(kind) {
97907
+ return isCmmdRuntimeWorktreeLaunchActivityKind(kind) ? kind : null;
97908
+ }
97246
97909
  function isReplicatedFailureActivity(activity) {
97247
97910
  return activity.kind === "runtime.error" || activity.kind.startsWith("provider.") && activity.kind.endsWith(".failed");
97248
97911
  }
@@ -97623,12 +98286,17 @@ function serializeRuntimeThreadEvents(thread) {
97623
98286
  const event = serializeNativeToolActivity(activity);
97624
98287
  return event === null ? [] : [event];
97625
98288
  });
98289
+ const worktreeLaunchActivities = thread.activities.flatMap((activity) => {
98290
+ const event = serializeWorktreeLaunchActivity(activity);
98291
+ return event === null ? [] : [event];
98292
+ });
97626
98293
  const recordedTurns = serializeRecordedTurns(thread);
97627
98294
  const textGenerations = serializeTextGenerations(thread);
97628
98295
  return [
97629
98296
  ...messages,
97630
98297
  ...replicaActivities,
97631
98298
  ...toolActivities,
98299
+ ...worktreeLaunchActivities,
97632
98300
  ...failureActivities,
97633
98301
  ...recordedTurns,
97634
98302
  ...textGenerations
@@ -101904,7 +102572,7 @@ function claudeReadinessReasonCode(reason) {
101904
102572
  //#endregion
101905
102573
  //#region src/egress/providerDeliveryReceipt.ts
101906
102574
  const PROVIDER_ACCOUNT_IDENTITY$2 = /^pai_v1_[A-Za-z0-9_-]{43}$/;
101907
- const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
102575
+ const UUID$1 = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
101908
102576
  const RFC3339$1 = /^(\d{4})-(\d{2})-(\d{2})T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/;
101909
102577
  const RECEIPT_KEYS = new Set([
101910
102578
  "receiptId",
@@ -101987,7 +102655,7 @@ function claudeAccountFields(value) {
101987
102655
  }
101988
102656
  function parseClaudeDeliveryIdentity(value) {
101989
102657
  const account = claudeAccountFields(value.providerAccountIdentity);
101990
- if (!account || !hasAllowedKeys(value, CLAUDE_IDENTITY_KEYS, CLAUDE_IDENTITY_OPTIONAL_KEYS) || value.kind !== "credential_generation" || value.provider !== "claude" || typeof value.credentialGenerationId !== "string" || !UUID.test(value.credentialGenerationId)) return null;
102658
+ if (!account || !hasAllowedKeys(value, CLAUDE_IDENTITY_KEYS, CLAUDE_IDENTITY_OPTIONAL_KEYS) || value.kind !== "credential_generation" || value.provider !== "claude" || typeof value.credentialGenerationId !== "string" || !UUID$1.test(value.credentialGenerationId)) return null;
101991
102659
  return {
101992
102660
  kind: "credential_generation",
101993
102661
  provider: "claude",
@@ -102754,7 +103422,10 @@ function extractClaudeProviderAccountId(response) {
102754
103422
  if (typeof response !== "object" || response === null || !("data" in response)) return null;
102755
103423
  const data = response.data;
102756
103424
  if (typeof data !== "object" || data === null) return null;
102757
- if ("connected" in data && data.connected === true) return extractClaudeCredentialGenerationId(response);
103425
+ if ("connected" in data && data.connected === true) {
103426
+ if ("providerAccountId" in data) return asNonEmptyString$1(data.providerAccountId) ?? null;
103427
+ return extractClaudeCredentialGenerationId(response);
103428
+ }
102758
103429
  if (!("account" in data)) return null;
102759
103430
  const account = data.account;
102760
103431
  if (typeof account !== "object" || account === null || !("providerAccountId" in account) || typeof account.providerAccountId !== "string") return null;
@@ -105726,6 +106397,14 @@ function makeRelayCommandInboxRepository(sql) {
105726
106397
  function isThreadDetailEvent(event) {
105727
106398
  return event.type === "thread.message-sent" || event.type === "thread.proposed-plan-upserted" || event.type === "thread.activity-appended" || event.type === "thread.turn-diff-completed" || event.type === "thread.reverted" || event.type === "thread.session-set" || event.type === "thread.rate-limit-set";
105728
106399
  }
106400
+ /**
106401
+ * Narrows the domain-event feed to one thread's stream-visible events. The same
106402
+ * predicate guards the live feed and the resume replay, so a subscriber cannot
106403
+ * be handed a different set of events depending on which path served it.
106404
+ */
106405
+ function isThreadDetailEventFor(threadId) {
106406
+ return (event) => event.aggregateKind === "thread" && event.aggregateId === threadId && isThreadDetailEvent(event);
106407
+ }
105729
106408
  const PROVIDER_STATUS_DEBOUNCE_MS = 200;
105730
106409
  function streamBoundedText(contents) {
105731
106410
  if (Buffer.byteLength(contents, "utf8") > 16777216) return fail(new BulkTransferError({
@@ -105771,6 +106450,39 @@ function loadInitialShellSnapshot(input) {
105771
106450
  return yield* input.projectionSnapshotQuery.getShellSnapshot();
105772
106451
  });
105773
106452
  }
106453
+ /**
106454
+ * The thread snapshot a subscription falls back to, windowed when the
106455
+ * subscriber asked for a window. A missing thread is a failure rather than an
106456
+ * empty snapshot: the subscriber asked about a thread this server does not
106457
+ * have, and saying so is the only honest answer.
106458
+ *
106459
+ * Also the read behind `getThreadDetailPage`, the socket RPC a relay-only
106460
+ * connection uses for "load earlier turns" in place of the HTTP snapshot
106461
+ * route: same window, same pure `windowThreadDetail`, so the two reads cannot
106462
+ * disagree about what a page is.
106463
+ */
106464
+ function loadThreadDetailSnapshot(input) {
106465
+ return gen(function* () {
106466
+ const [threadDetail, snapshotSequence] = yield* all([input.projectionSnapshotQuery.getThreadDetailById(input.threadId).pipe(mapError((cause) => new OrchestrationGetSnapshotError({
106467
+ message: `Failed to load thread ${input.threadId}`,
106468
+ cause
106469
+ }))), input.orchestrationEngine.getReadModel().pipe(map$3((readModel) => readModel.snapshotSequence))]);
106470
+ if (isNone(threadDetail)) return yield* new OrchestrationGetSnapshotError({
106471
+ message: `Thread ${input.threadId} was not found`,
106472
+ cause: input.threadId
106473
+ });
106474
+ const windowed = windowThreadDetail({
106475
+ thread: threadDetail.value,
106476
+ window: input.window,
106477
+ snapshotSequence
106478
+ });
106479
+ return {
106480
+ snapshotSequence,
106481
+ thread: windowed.thread,
106482
+ ...windowed.page === void 0 ? {} : { page: windowed.page }
106483
+ };
106484
+ });
106485
+ }
105774
106486
  function makeSnapshotThenLiveStream(input) {
105775
106487
  return unwrap$1(gen(function* () {
105776
106488
  const liveEvents = yield* input.subscribeLiveEvents;
@@ -106017,6 +106729,9 @@ const makeWsRpcLayer = (currentSessionId, currentUserId) => WsRpcGroup.toLayer(g
106017
106729
  shellRevealInFileManager: true,
106018
106730
  shellRevealInFileManagerKind: fileManagerRevealKind
106019
106731
  },
106732
+ threadResumeCompletionMarker: true,
106733
+ threadSnapshotPagination: true,
106734
+ threadDetailPageRpc: true,
106020
106735
  observability: {
106021
106736
  logsDirectoryPath: config.logsDir,
106022
106737
  localTracingEnabled: true,
@@ -106114,26 +106829,28 @@ const makeWsRpcLayer = (currentSessionId, currentUserId) => WsRpcGroup.toLayer(g
106114
106829
  projectionSnapshotQuery
106115
106830
  })), flatMap$3((event) => isSome(event) ? succeed$4(event.value) : empty$5))))
106116
106831
  })), { "rpc.aggregate": "orchestration" }),
106117
- [ORCHESTRATION_WS_METHODS.subscribeThread]: (input) => observeRpcStreamEffect(ORCHESTRATION_WS_METHODS.subscribeThread, gen(function* () {
106118
- const [threadDetail, snapshotSequence] = yield* all([projectionSnapshotQuery.getThreadDetailById(input.threadId).pipe(mapError((cause) => new OrchestrationGetSnapshotError({
106119
- message: `Failed to load thread ${input.threadId}`,
106832
+ [ORCHESTRATION_WS_METHODS.subscribeThread]: (input) => observeRpcStreamEffect(ORCHESTRATION_WS_METHODS.subscribeThread, succeed(makeThreadSubscriptionStream(input, {
106833
+ subscribeLiveEvents: orchestrationEngine.subscribeDomainEvents().pipe(map$3((events) => events.pipe(filter$2(isThreadDetailEventFor(input.threadId))))),
106834
+ currentSequence: orchestrationEngine.getReadModel().pipe(map$3((readModel) => readModel.snapshotSequence)),
106835
+ loadSnapshot: loadThreadDetailSnapshot({
106836
+ threadId: input.threadId,
106837
+ window: input.turnLimit === void 0 ? {} : { turnLimit: input.turnLimit },
106838
+ projectionSnapshotQuery,
106839
+ orchestrationEngine
106840
+ }),
106841
+ replayEvents: (afterSequence) => orchestrationEngine.readEvents(afterSequence).pipe(filter$2(isThreadDetailEventFor(input.threadId)), mapError$1((cause) => new OrchestrationGetSnapshotError({
106842
+ message: `Failed to replay thread ${input.threadId}`,
106120
106843
  cause
106121
- }))), orchestrationEngine.getReadModel().pipe(map$3((readModel) => readModel.snapshotSequence))]);
106122
- if (isNone(threadDetail)) return yield* new OrchestrationGetSnapshotError({
106123
- message: `Thread ${input.threadId} was not found`,
106124
- cause: input.threadId
106125
- });
106126
- const liveStream = orchestrationEngine.streamDomainEvents.pipe(filter$2((event) => event.aggregateKind === "thread" && event.aggregateId === input.threadId && isThreadDetailEvent(event)), map$7((event) => ({
106127
- kind: "event",
106128
- event
106129
- })));
106130
- return concat$2(make$43({
106131
- kind: "snapshot",
106132
- snapshot: {
106133
- snapshotSequence,
106134
- thread: threadDetail.value
106135
- }
106136
- }), liveStream);
106844
+ })))
106845
+ })), { "rpc.aggregate": "orchestration" }),
106846
+ [ORCHESTRATION_WS_METHODS.getThreadDetailPage]: (input) => observeRpcEffect(ORCHESTRATION_WS_METHODS.getThreadDetailPage, loadThreadDetailSnapshot({
106847
+ threadId: input.threadId,
106848
+ window: {
106849
+ ...input.turnLimit === void 0 ? {} : { turnLimit: input.turnLimit },
106850
+ ...input.beforeCursor === void 0 ? {} : { beforeCursor: input.beforeCursor }
106851
+ },
106852
+ projectionSnapshotQuery,
106853
+ orchestrationEngine
106137
106854
  }), { "rpc.aggregate": "orchestration" }),
106138
106855
  [WS_METHODS.serverGetConfig]: (_input) => observeRpcEffect(WS_METHODS.serverGetConfig, loadServerConfig, { "rpc.aggregate": "server" }),
106139
106856
  [WS_METHODS.serverRefreshProviders]: (_input) => observeRpcEffect(WS_METHODS.serverRefreshProviders, providerRegistry.refresh().pipe(map$3((providers) => ({ providers }))), { "rpc.aggregate": "server" }),
@@ -117598,8 +118315,8 @@ effectDiscard(runMigrations());
117598
118315
  //#endregion
117599
118316
  //#region src/persistence/Layers/Sqlite.ts
117600
118317
  const defaultSqliteClientLoaders = {
117601
- bun: () => import("./SqliteClient-D3Yd3ZHn.mjs"),
117602
- node: () => import("./NodeSqliteClient-BrzqdOsF.mjs")
118318
+ bun: () => import("./SqliteClient-BKB695RF.mjs"),
118319
+ node: () => import("./NodeSqliteClient-md8xd9hY.mjs")
117603
118320
  };
117604
118321
  const makeRuntimeSqliteLayer = fn$1("makeRuntimeSqliteLayer")(function* (config) {
117605
118322
  const loader = defaultSqliteClientLoaders[process.versions.bun !== void 0 ? "bun" : "node"];
@@ -183040,7 +183757,7 @@ var AM = class {
183040
183757
  return unlink$1(e);
183041
183758
  }
183042
183759
  list(e) {
183043
- return readdir$1(e);
183760
+ return readdir$2(e);
183044
183761
  }
183045
183762
  append(e, t, n) {
183046
183763
  return appendFile(e, t, {
@@ -183065,7 +183782,7 @@ var AM = class {
183065
183782
  return { mtimeMs: (await stat$1(e)).mtimeMs };
183066
183783
  }
183067
183784
  async listEntries(e) {
183068
- return (await readdir$1(e, { withFileTypes: !0 })).map((n) => ({
183785
+ return (await readdir$2(e, { withFileTypes: !0 })).map((n) => ({
183069
183786
  name: n.name,
183070
183787
  isDirectory: n.isDirectory(),
183071
183788
  isFile: n.isFile()
@@ -190295,7 +191012,7 @@ var Vce = {
190295
191012
  return lstat$1(e);
190296
191013
  },
190297
191014
  async readdir(e) {
190298
- return readdir$1(e, { withFileTypes: !0 });
191015
+ return readdir$2(e, { withFileTypes: !0 });
190299
191016
  },
190300
191017
  async unlink(e) {
190301
191018
  return unlink$1(e);
@@ -190352,7 +191069,7 @@ var Vce = {
190352
191069
  return link$1(e, t);
190353
191070
  },
190354
191071
  async chmod(e, t) {
190355
- return chmod(e, t);
191072
+ return chmod$1(e, t);
190356
191073
  },
190357
191074
  statSync(e) {
190358
191075
  let n = [];
@@ -190535,7 +191252,7 @@ var Vce = {
190535
191252
  function Ve() {
190536
191253
  return Vce;
190537
191254
  }
190538
- var Us = $U.homedir(), lk = $U.tmpdir(), { env: Vc } = process$1, Yce = (e) => {
191255
+ var Us = os.homedir(), lk = os.tmpdir(), { env: Vc } = process$1, Yce = (e) => {
190539
191256
  let t = path.join(Us, "Library");
190540
191257
  return {
190541
191258
  data: path.join(t, "Application Support", e),
@@ -190871,7 +191588,7 @@ async function hme(e) {
190871
191588
  await Promise.all(o.map(async (s) => {
190872
191589
  let i;
190873
191590
  try {
190874
- i = await readdir$1(s, { withFileTypes: !0 });
191591
+ i = await readdir$2(s, { withFileTypes: !0 });
190875
191592
  } catch {
190876
191593
  return;
190877
191594
  }
@@ -200735,12 +201452,12 @@ function qZ(e, t) {
200735
201452
  }
200736
201453
  return n;
200737
201454
  }
200738
- var os$1 = "https://code.claude.com/docs/en", kKe = [
201455
+ var os$2 = "https://code.claude.com/docs/en", kKe = [
200739
201456
  {
200740
201457
  matches: (e) => e.path === "permissions.defaultMode" && e.code === "invalid_value",
200741
201458
  tip: {
200742
201459
  suggestion: "Valid modes: \"acceptEdits\" (ask before file changes), \"plan\" (analysis only), \"bypassPermissions\" (auto-accept all), or \"default\" (standard behavior)",
200743
- docLink: `${os$1}/iam#permission-modes`
201460
+ docLink: `${os$2}/iam#permission-modes`
200744
201461
  }
200745
201462
  },
200746
201463
  {
@@ -200755,7 +201472,7 @@ var os$1 = "https://code.claude.com/docs/en", kKe = [
200755
201472
  matches: (e) => e.path.startsWith("env.") && e.code === "invalid_type",
200756
201473
  tip: {
200757
201474
  suggestion: "Environment variables must be strings. Wrap numbers and booleans in quotes. Example: \"DEBUG\": \"true\", \"PORT\": \"3000\"",
200758
- docLink: `${os$1}/settings#environment-variables`
201475
+ docLink: `${os$2}/settings#environment-variables`
200759
201476
  }
200760
201477
  },
200761
201478
  {
@@ -200766,14 +201483,14 @@ var os$1 = "https://code.claude.com/docs/en", kKe = [
200766
201483
  matches: (e) => e.path.startsWith("hooks.") && e.code === "invalid_key",
200767
201484
  tip: {
200768
201485
  suggestion: "Not a recognized hook event. Common events: PreToolUse, PostToolUse, UserPromptSubmit, SessionStart, SessionEnd, Stop. Check spelling and capitalization.",
200769
- docLink: `${os$1}/hooks`
201486
+ docLink: `${os$2}/hooks`
200770
201487
  }
200771
201488
  },
200772
201489
  {
200773
201490
  matches: (e) => /\.hooks\.\d+\.command$/.test(e.path) && e.code === "invalid_type" && e.received === "undefined",
200774
201491
  tip: {
200775
201492
  suggestion: "Command hooks require `command`. For exec form (no shell), set `command` to the executable and `args` to its arguments: {\"type\": \"command\", \"command\": \"echo\", \"args\": [\"hi\"]}. For shell form, set `command` to the full shell string: {\"type\": \"command\", \"command\": \"echo hi\"}.",
200776
- docLink: `${os$1}/hooks#exec-form-and-shell-form`
201493
+ docLink: `${os$2}/hooks#exec-form-and-shell-form`
200777
201494
  }
200778
201495
  },
200779
201496
  {
@@ -200788,7 +201505,7 @@ var os$1 = "https://code.claude.com/docs/en", kKe = [
200788
201505
  matches: (e) => e.code === "unrecognized_keys",
200789
201506
  tip: {
200790
201507
  suggestion: "Check for typos or refer to the documentation for valid fields",
200791
- docLink: `${os$1}/settings`
201508
+ docLink: `${os$2}/settings`
200792
201509
  }
200793
201510
  },
200794
201511
  {
@@ -200803,13 +201520,13 @@ var os$1 = "https://code.claude.com/docs/en", kKe = [
200803
201520
  matches: (e) => e.path === "permissions.additionalDirectories" && e.code === "invalid_type",
200804
201521
  tip: {
200805
201522
  suggestion: "Must be an array of directory paths. Example: [\"~/projects\", \"/tmp/workspace\"]. You can also use --add-dir flag or /add-dir command",
200806
- docLink: `${os$1}/iam#working-directories`
201523
+ docLink: `${os$2}/iam#working-directories`
200807
201524
  }
200808
201525
  }
200809
201526
  ], AKe = {
200810
- permissions: `${os$1}/iam#configuring-permissions`,
200811
- env: `${os$1}/settings#environment-variables`,
200812
- hooks: `${os$1}/hooks`
201527
+ permissions: `${os$2}/iam#configuring-permissions`,
201528
+ env: `${os$2}/settings#environment-variables`,
201529
+ hooks: `${os$2}/hooks`
200813
201530
  };
200814
201531
  function GZ(e) {
200815
201532
  let t = kKe.find((r) => r.matches(e));
@@ -212181,6 +212898,8 @@ const PUBLIC_PROVIDER_LEAK_PATTERNS = [
212181
212898
  /\bCURSOR_API_KEY\b/,
212182
212899
  /\bOPENCODE_API_KEY\b/,
212183
212900
  /\bforge_proxy_[A-Za-z0-9._-]+\b/,
212901
+ /\bProviderValidationError\b/,
212902
+ /\/forge-server\/node_modules\//,
212184
212903
  /https?:\/\/[^\s'"<>)]*\/api\/provider-egress\/[^\s'"<>)]*/i,
212185
212904
  /https?:\/\/[^\s'"<>)]*(?:apex|deepseek|moonshot|kimi)[^\s'"<>)]*/i
212186
212905
  ];
@@ -212190,6 +212909,12 @@ function isProviderRateOrQuotaError(message) {
212190
212909
  const normalized = message.toLowerCase();
212191
212910
  return /\b(rate[\s-]?limit|too\s+many\s+requests|usage[ _-]limit|usage_limit_reached|quota|insufficient_quota)\b/.test(normalized) || /\b429\b/.test(normalized);
212192
212911
  }
212912
+ function providerLifecycleOrAccountCopy(message) {
212913
+ if (/\bbound to provider account\b/.test(message) && /\bcannot use provider account\b/.test(message)) return "Forge could not switch this conversation to the selected AI account. Retry the account switch.";
212914
+ if (/\bforge_runtime_repairing\b/.test(message) || /\bthis turn was interrupted because the environment is being repaired\b/.test(message)) return "This turn was interrupted because the environment is being repaired. Retry after the repair finishes.";
212915
+ if (/\bforge_runtime_stopped\b/.test(message) || /\bthis turn was interrupted because the environment was stopped\b/.test(message)) return "This turn was interrupted because the environment was stopped. Start the environment and retry.";
212916
+ return null;
212917
+ }
212193
212918
  /**
212194
212919
  * Infer a SAFE, clearer failure category from a (possibly leaky) provider error.
212195
212920
  *
@@ -212202,7 +212927,8 @@ function isProviderRateOrQuotaError(message) {
212202
212927
  function categorizePublicProviderError(message) {
212203
212928
  if (!message) return null;
212204
212929
  const m = message.toLowerCase();
212205
- if (/\bforge_runtime_stopped\b/.test(m) || /\bthis turn was interrupted because the environment was stopped\b/.test(m)) return "This turn was interrupted because the environment was stopped. Start the environment and retry.";
212930
+ const lifecycleOrAccountCopy = providerLifecycleOrAccountCopy(m);
212931
+ if (lifecycleOrAccountCopy !== null) return lifecycleOrAccountCopy;
212206
212932
  if (/\bforge_provider_model_not_allowed\b/.test(m) || /\bnot\s+scoped\s+to\s+the\s+requested\s+model\b/.test(m)) return "The workspace AI credential does not cover the model this turn asked for. Retry, or pick that model for the thread.";
212207
212933
  if (/\bmodel\b[\s\S]*\b(not\s+(found|available|supported)|does\s+not\s+exist|unsupported|invalid|unknown)\b/.test(m) || /\b(unsupported|unknown|invalid)\b[\s\S]*\bmodel\b/.test(m) || /\b(model_not_found|invalid_model|model_not_supported)\b/.test(m)) return "The selected model isn't available right now. Pick a different model and retry.";
212208
212934
  if (isProviderRateOrQuotaError(m)) return "The AI service is rate-limited or out of quota. Wait a moment and retry.";
@@ -212268,60 +212994,14 @@ function hasConfiguredApexCredential(profiles) {
212268
212994
  }
212269
212995
  //#endregion
212270
212996
  //#region src/provider/claudeCredentialDirectory.ts
212271
- /**
212272
- * Per-account credential directories for Claude turns.
212273
- *
212274
- * Claude Code keeps conversation transcripts under
212275
- * `${CLAUDE_CONFIG_DIR}/projects/<cwd-slug>/<sessionId>.jsonl` and its login
212276
- * under `.credentials.json`. `CLAUDE_SECURESTORAGE_CONFIG_DIR` moves only the
212277
- * credential file, so each Provider Account can hold its own login while every
212278
- * session file stays in one place and `--resume` keeps finding its target. See
212279
- * `docs/adr/claude-accounts-split-credentials-not-sessions.md`.
212280
- *
212281
- * That variable is not documented. Under
212282
- * `docs/adr/unpublished-provider-surface-needs-a-fallback.md` the dependency is
212283
- * only allowed because absence degrades to a named behaviour that already
212284
- * works: a thread with no account runs on the Environment's own login. This
212285
- * module sets the variable only when the resolved directory holds a credential
212286
- * the CLI can use, because a missing file, or one whose access token a refusal
212287
- * has blanked, makes the CLI report logged out and a live turn would fail.
212288
- *
212289
- * Nothing writes these directories yet. #3049 adds the writer and keys each
212290
- * directory on the account identity CMMD writes on the delivery receipt it
212291
- * signs (`docs/adr/provider-account-identity-is-issued-by-cmmd.md`). Until then
212292
- * every runtime takes the `no-account` skip.
212293
- *
212294
- * Free of Effect services on purpose, for the same reason as
212295
- * `claudeCredentialContainment.ts`: ClaudeAdapter drags in Effect platform
212296
- * services, the browser manager and the server config, so a unit test that
212297
- * imports it cannot run in a worktree. This rule decides which account pays for
212298
- * a turn, so it has to stay directly testable.
212299
- */
212997
+ /** Per-account credentials share the existing Claude transcript directory. */
212300
212998
  /** Repoints `.credentials.json` without moving the transcripts. */
212301
212999
  const CLAUDE_SECURESTORAGE_CONFIG_DIR_ENV = "CLAUDE_SECURESTORAGE_CONFIG_DIR";
212302
213000
  /** The one file Claude Code writes into the credential directory. */
212303
213001
  const CLAUDE_CREDENTIAL_FILE_NAME = ".credentials.json";
212304
- /**
212305
- * Runtime env var naming the Provider Account this runtime's Claude turns
212306
- * belong to. Nothing in Forge or CMMD sets it, so no runtime names an account
212307
- * and each keeps its one login.
212308
- *
212309
- * It is a bridge that will not get a writer. #3073 settled the key in
212310
- * `docs/adr/provider-account-identity-is-issued-by-cmmd.md`: the directory name
212311
- * comes from the identity CMMD issues and writes on the delivery receipt it
212312
- * signs, and this variable goes away. #3049 makes that change. The credential
212313
- * generation was ruled out as a key: it rotates on every reconnect, and CMMD
212314
- * uses it as the primary account's id, so keying on it would strand every
212315
- * thread pinned to that account on a directory that stops receiving deliveries.
212316
- *
212317
- * Tests reach the `credentialDirectory` parameter on
212318
- * `ClaudeAdapter.buildClaudeQueryEnv` directly.
212319
- */
212320
- const CLAUDE_PROVIDER_ACCOUNT_IDENTITY_ENV = "FORGE_CLAUDE_PROVIDER_ACCOUNT_IDENTITY";
212321
213002
  /** Directory under the server's base directory that holds one directory per account. */
212322
213003
  const CREDENTIAL_ROOT_DIRECTORY_NAME = "claude-credentials";
212323
213004
  const DEFAULT_CLAUDE_CONFIG_DIRECTORY_NAME = ".claude";
212324
- const MAX_ACCOUNT_IDENTITY_LENGTH = 256;
212325
213005
  const DIRECTORY_NAME_HASH_LENGTH = 32;
212326
213006
  /**
212327
213007
  * Directory name for an account. A hash, so no email or display name reaches a
@@ -212330,11 +213010,12 @@ const DIRECTORY_NAME_HASH_LENGTH = 32;
212330
213010
  function claudeCredentialDirectoryName(accountIdentity) {
212331
213011
  return `account-${createHash("sha256").update(accountIdentity, "utf8").digest("hex").slice(0, DIRECTORY_NAME_HASH_LENGTH)}`;
212332
213012
  }
212333
- /** The account this runtime's Claude turns belong to, or null while the variable is unset. */
212334
- function resolveClaudeProviderAccountIdentity(env) {
212335
- const identity = env[CLAUDE_PROVIDER_ACCOUNT_IDENTITY_ENV]?.trim();
212336
- if (!identity || identity.length > MAX_ACCOUNT_IDENTITY_LENGTH) return null;
212337
- return identity;
213013
+ /** Only a matching CMMD delivery can select a credential directory. */
213014
+ function resolveSelectedClaudeAccountIdentity(selectedAccountId, binding) {
213015
+ if (selectedAccountId === void 0) return null;
213016
+ const identity = binding?.identity;
213017
+ if (identity?.kind !== "credential_generation" || identity.provider !== "claude" || identity.providerAccountIdentity !== selectedAccountId) throw new Error("The selected Claude account has no matching credential delivery. Reconnect that account and try again.");
213018
+ return identity.providerAccountIdentity;
212338
213019
  }
212339
213020
  /**
212340
213021
  * Where per-account credential directories live: `claude-credentials` under the
@@ -212361,29 +213042,19 @@ function resolveClaudeCredentialDirectory(input) {
212361
213042
  if (!root) return null;
212362
213043
  return join(root, claudeCredentialDirectoryName(input.accountIdentity));
212363
213044
  }
212364
- /**
212365
- * Decide whether this turn may point at a per-account credential directory.
212366
- *
212367
- * The cheap checks run first and the capability probe runs last, so a runtime
212368
- * with no usable credential never spawns the CLI. The probe's error channel is
212369
- * empty, so nothing here catches.
212370
- *
212371
- * A skip is not a fallback onto the default account. With no account the turn
212372
- * runs on the Environment's own login, unchanged. A thread pinned to an account
212373
- * whose credential is missing is refused by name under
212374
- * `docs/adr/provider-account-identity-is-issued-by-cmmd.md`, and #3049 turns
212375
- * `no-usable-credential`, and `capability-not-supported` for a pinned account,
212376
- * into those refusals. Until #3049 nothing names an account, so `no-account` is
212377
- * the only skip a runtime sees.
212378
- */
213045
+ /** Refuse selected accounts that cannot use their own credential directory. */
212379
213046
  function decideClaudeCredentialDirectory(input) {
212380
213047
  return gen(function* () {
212381
213048
  if (!input.accountIdentity) return skip("no-account");
212382
- if (input.env["CLAUDE_SECURESTORAGE_CONFIG_DIR"]?.trim()) return skip("operator-override");
213049
+ if (input.env["CLAUDE_SECURESTORAGE_CONFIG_DIR"]?.trim()) return refuse("operator-override");
212383
213050
  const directory = resolveClaudeCredentialDirectory(input);
212384
- if (!directory) return skip("no-root");
212385
- if (!holdsUsableCredential(directory)) return skip("no-usable-credential");
212386
- return (yield* input.probeSupport) === "supported" ? set(directory) : skip("capability-not-supported");
213051
+ if (!directory) return refuse("no-root");
213052
+ if (!holdsUsableCredential(directory)) return refuse("no-usable-credential");
213053
+ if ((yield* input.probeSupport) === "supported") return set(directory);
213054
+ return input.primaryCredentialMatchesSelected === true ? {
213055
+ outcome: "skipped",
213056
+ reason: "same-primary-account"
213057
+ } : refuse("capability-not-supported");
212387
213058
  });
212388
213059
  }
212389
213060
  /**
@@ -212422,6 +213093,12 @@ function skip(reason) {
212422
213093
  reason
212423
213094
  };
212424
213095
  }
213096
+ function refuse(reason) {
213097
+ return {
213098
+ outcome: "refused",
213099
+ reason
213100
+ };
213101
+ }
212425
213102
  function claudeConfigDirectory(env) {
212426
213103
  const configured = env.CLAUDE_CONFIG_DIR?.trim();
212427
213104
  if (configured) return configured;
@@ -212431,6 +213108,24 @@ function claudeConfigDirectory(env) {
212431
213108
  return null;
212432
213109
  }
212433
213110
  }
213111
+ /**
213112
+ * The fallback is allowed only when the primary on-disk credential is
213113
+ * byte-identical to what CMMD prepared for the selected account. A ticket's
213114
+ * `credentialRevisionHash` cannot express that: it hashes the encrypted row
213115
+ * CMMD stores, not the decrypted bytes this process ever sees. Comparing
213116
+ * files directly is the only check that means what the fallback needs.
213117
+ */
213118
+ function primaryClaudeCredentialMatchesPrepared(env, preparedDirectory) {
213119
+ const directory = claudeConfigDirectory(env);
213120
+ if (!directory) return false;
213121
+ try {
213122
+ const primary = readFileSync(join(directory, CLAUDE_CREDENTIAL_FILE_NAME), "utf8");
213123
+ const prepared = readFileSync(join(preparedDirectory, CLAUDE_CREDENTIAL_FILE_NAME), "utf8");
213124
+ return hasAccessToken(JSON.parse(primary)) && createHash("sha256").update(primary).digest("hex") === createHash("sha256").update(prepared).digest("hex");
213125
+ } catch {
213126
+ return false;
213127
+ }
213128
+ }
212434
213129
  //#endregion
212435
213130
  //#region src/provider/personalClaudeInVm.ts
212436
213131
  /**
@@ -212563,13 +213258,106 @@ function withoutInheritedAnthropicCredentials(env, options) {
212563
213258
  * marker CMMD writes on disk (#3081), so a Sprite switched on in place, which
212564
213259
  * has the marker and no env var until it restarts, drops the override too.
212565
213260
  */
212566
- function claudeSubscriptionChildEnv(processEnv, credentialDirectory) {
213261
+ function claudeSubscriptionChildEnv(processEnv, credentialDirectory, selectedSubscription = false) {
212567
213262
  return {
212568
- ...withoutInheritedAnthropicCredentials(scrubForgeProviderChildEnvSecrets(processEnv), { stripEndpointOverride: personalClaudeInVmEnabled(processEnv) }),
213263
+ ...withoutInheritedAnthropicCredentials(scrubForgeProviderChildEnvSecrets(processEnv), { stripEndpointOverride: selectedSubscription || credentialDirectory !== null || personalClaudeInVmEnabled(processEnv) }),
212569
213264
  ...credentialDirectory ? { [CLAUDE_SECURESTORAGE_CONFIG_DIR_ENV]: credentialDirectory } : {}
212570
213265
  };
212571
213266
  }
212572
213267
  //#endregion
213268
+ //#region src/provider/claudeSelectedCredentialStore.ts
213269
+ async function ensurePrivateDirectory(path) {
213270
+ await mkdir(path, {
213271
+ recursive: true,
213272
+ mode: 448
213273
+ });
213274
+ const info = await lstat(path);
213275
+ if (!info.isDirectory() || info.isSymbolicLink()) throw new Error("Unsafe credential directory.");
213276
+ await chmod(path, 448);
213277
+ }
213278
+ async function makeCapacity(root, target, active, maximum) {
213279
+ const entries = (await readdir$1(root, { withFileTypes: true })).filter((entry) => entry.isDirectory() && /^account-[a-f0-9]{32}$/.test(entry.name));
213280
+ if (entries.some((entry) => join(root, entry.name) === target)) return;
213281
+ const candidates = await Promise.all(entries.filter((entry) => !active.has(join(root, entry.name))).map(async (entry) => ({
213282
+ path: join(root, entry.name),
213283
+ modified: (await lstat(join(root, entry.name))).mtimeMs
213284
+ })));
213285
+ candidates.sort((a, b) => a.modified - b.modified);
213286
+ const required = entries.length - maximum + 1;
213287
+ if (required > candidates.length) throw new Error("All Claude credential slots are active. Stop a session before choosing another account.");
213288
+ for (const candidate of candidates.slice(0, Math.max(0, required))) await rm(candidate.path, { recursive: true });
213289
+ }
213290
+ function createClaudeSelectedCredentialStore(baseDir, options = {}) {
213291
+ const sessions = /* @__PURE__ */ new Map();
213292
+ const maximum = options.maxAccounts ?? 8;
213293
+ if (!Number.isSafeInteger(maximum) || maximum < 1) throw new Error("Invalid credential capacity.");
213294
+ let queue = Promise.resolve();
213295
+ async function install(threadId, acquired) {
213296
+ if (acquired.claims.threadId !== threadId) throw new Error("Credential thread mismatch.");
213297
+ const directory = resolveClaudeCredentialDirectory({
213298
+ env: process.env,
213299
+ baseDir,
213300
+ accountIdentity: acquired.claims.providerAccountId
213301
+ });
213302
+ if (!directory) throw new Error("Unsafe credential directory.");
213303
+ const binding = {
213304
+ runtimeId: acquired.claims.runtimeId,
213305
+ machineId: acquired.claims.machineId,
213306
+ credentialEpoch: acquired.claims.credentialEpoch,
213307
+ identity: {
213308
+ kind: "credential_generation",
213309
+ provider: "claude",
213310
+ credentialGenerationId: acquired.claims.generationId,
213311
+ providerAccountIdentity: acquired.claims.providerAccountId
213312
+ }
213313
+ };
213314
+ for (const prepared of sessions.values()) if (prepared.directory === directory && (!providerDeliveryBindingsEqual(prepared.binding, binding) || prepared.credentialRevisionHash !== acquired.claims.credentialRevisionHash)) throw new Error("The Claude account has an active session. Stop it before replacing its credentials.");
213315
+ const root = dirname(directory);
213316
+ await ensurePrivateDirectory(root);
213317
+ await makeCapacity(root, directory, new Set([...sessions.values()].map((entry) => entry.directory)), maximum);
213318
+ await ensurePrivateDirectory(directory);
213319
+ const temporary = join(directory, `.credential-${randomUUID()}.tmp`);
213320
+ try {
213321
+ await writeFile(temporary, acquired.credentialJson, {
213322
+ mode: 384,
213323
+ flag: "wx"
213324
+ });
213325
+ await rename(temporary, join(directory, ".credentials.json"));
213326
+ } finally {
213327
+ await rm(temporary, { force: true });
213328
+ }
213329
+ sessions.set(threadId, {
213330
+ directory,
213331
+ binding,
213332
+ accountKind: acquired.claims.accountKind,
213333
+ credentialRevisionHash: acquired.claims.credentialRevisionHash
213334
+ });
213335
+ return binding;
213336
+ }
213337
+ return {
213338
+ prepare(threadId, acquired) {
213339
+ const operation = queue.then(() => install(threadId, acquired));
213340
+ queue = operation.then(() => void 0, () => void 0);
213341
+ return operation;
213342
+ },
213343
+ read(threadId, binding) {
213344
+ const prepared = sessions.get(threadId);
213345
+ return prepared && providerDeliveryBindingsEqual(prepared.binding, binding) ? prepared : null;
213346
+ },
213347
+ release(threadId) {
213348
+ sessions.delete(threadId);
213349
+ }
213350
+ };
213351
+ }
213352
+ const stores = /* @__PURE__ */ new Map();
213353
+ function selectedClaudeCredentialStore(baseDir) {
213354
+ const existing = stores.get(baseDir);
213355
+ if (existing) return existing;
213356
+ const store = createClaudeSelectedCredentialStore(baseDir);
213357
+ stores.set(baseDir, store);
213358
+ return store;
213359
+ }
213360
+ //#endregion
212573
213361
  //#region src/provider/claudeCredentialDirectoryProbe.ts
212574
213362
  /**
212575
213363
  * Capability probe for `CLAUDE_SECURESTORAGE_CONFIG_DIR`.
@@ -214611,7 +215399,8 @@ function claudeForgeAttributionPrompt() {
214611
215399
  * turn, an API key is, so pointing the variable there would add a second source
214612
215400
  * of truth that a later reader has to rule out.
214613
215401
  */
214614
- function buildClaudeQueryEnv(baseProcessEnv, endpointProfile, claudeSettings, providerProxyCredential, credentialDirectory) {
215402
+ function buildClaudeQueryEnv(baseProcessEnv, endpointProfile, claudeSettings, providerProxyCredential, credentialDirectory, selectedSubscription = false) {
215403
+ if (selectedSubscription) return claudeSubscriptionChildEnv(baseProcessEnv, credentialDirectory, true);
214615
215404
  const baseEnv = scrubForgeProviderChildEnvSecrets(baseProcessEnv);
214616
215405
  const effectiveBaseUrl = resolveClaudeQueryBaseUrl(endpointProfile, claudeSettings, providerProxyCredential);
214617
215406
  if (!effectiveBaseUrl) return claudeSubscriptionChildEnv(baseProcessEnv, credentialDirectory);
@@ -216524,6 +217313,7 @@ const makeClaudeAdapter = fn$1("makeClaudeAdapter")(function* (options) {
216524
217313
  if (streamFiber && streamFiber.pollUnsafe() === void 0) yield* interrupt$4(streamFiber);
216525
217314
  try {
216526
217315
  context.query.close();
217316
+ selectedClaudeCredentialStore(serverConfig.baseDir).release(context.session.threadId);
216527
217317
  } catch (cause) {
216528
217318
  yield* emitRuntimeError(context, "Failed to close Claude runtime query.", cause);
216529
217319
  }
@@ -216835,6 +217625,43 @@ const makeClaudeAdapter = fn$1("makeClaudeAdapter")(function* (options) {
216835
217625
  sessionId: existingResumeSessionId ?? newSessionId
216836
217626
  };
216837
217627
  });
217628
+ const resolveSelectedCredentialDirectory = fn$1("resolveSelectedCredentialDirectory")(function* (input, resumeState, claudeBinaryPath) {
217629
+ const requestedAccountId = input.modelSelection?.provider === "claudeAgent" ? input.modelSelection.providerAccountId : void 0;
217630
+ const selectedAccountIdentity = yield* try_({
217631
+ try: () => resolveSelectedClaudeAccountIdentity(requestedAccountId, resumeState?.providerDeliveryBinding),
217632
+ catch: () => new ProviderAdapterValidationError({
217633
+ provider: PROVIDER$11,
217634
+ operation: "startSession",
217635
+ issue: `The selected Claude account ${requestedAccountId} has no matching credential delivery. Reconnect that account and try again.`
217636
+ })
217637
+ });
217638
+ const preparedCredential = resumeState?.providerDeliveryBinding ? selectedClaudeCredentialStore(serverConfig.baseDir).read(input.threadId, resumeState.providerDeliveryBinding) : null;
217639
+ if (selectedAccountIdentity !== null && preparedCredential === null) return yield* new ProviderAdapterValidationError({
217640
+ provider: PROVIDER$11,
217641
+ operation: "startSession",
217642
+ issue: `The selected Claude account ${selectedAccountIdentity} has no prepared credential. Try again to reconnect it.`
217643
+ });
217644
+ const credentialDirectoryDecision = yield* decideClaudeCredentialDirectory({
217645
+ env: process.env,
217646
+ baseDir: serverConfig.baseDir,
217647
+ accountIdentity: selectedAccountIdentity,
217648
+ probeSupport: claudeCredentialDirectoryProbeFor(claudeBinaryPath).support,
217649
+ primaryCredentialMatchesSelected: preparedCredential?.accountKind === "primary" && primaryClaudeCredentialMatchesPrepared(process.env, preparedCredential.directory)
217650
+ });
217651
+ if (credentialDirectoryDecision.outcome === "refused") return yield* new ProviderAdapterValidationError({
217652
+ provider: PROVIDER$11,
217653
+ operation: "startSession",
217654
+ issue: `The selected Claude account ${selectedAccountIdentity} cannot run on this environment: ${credentialDirectoryDecision.reason}. Reconnect the account or choose another environment.`
217655
+ });
217656
+ if (credentialDirectoryDecision.outcome === "skipped") {
217657
+ const skipLine = `Claude credential directory skipped: ${credentialDirectoryDecision.reason}`;
217658
+ yield* credentialDirectoryDecision.reason === "no-account" ? logDebug(skipLine) : logWarning$1(skipLine);
217659
+ }
217660
+ return {
217661
+ selectedAccountIdentity,
217662
+ claudeCredentialDirectory: credentialDirectoryDecision.outcome === "set" ? credentialDirectoryDecision.directory : null
217663
+ };
217664
+ });
216838
217665
  const startSession = fn$1("startSession")(function* (input) {
216839
217666
  const sessionProvider = yield* validateClaudeStartSessionProvider(input.provider);
216840
217667
  const startedAt = yield* nowIso;
@@ -216895,19 +217722,14 @@ const makeClaudeAdapter = fn$1("makeClaudeAdapter")(function* (options) {
216895
217722
  })));
216896
217723
  const claudeSettings = serverSettings.providers.claudeAgent;
216897
217724
  const claudeBinaryPath = claudeSettings.binaryPath;
216898
- const credentialDirectoryDecision = yield* decideClaudeCredentialDirectory({
216899
- env: process.env,
216900
- baseDir: serverConfig.baseDir,
216901
- accountIdentity: resolveClaudeProviderAccountIdentity(process.env),
216902
- probeSupport: claudeCredentialDirectoryProbeFor(claudeBinaryPath).support
216903
- });
216904
- if (credentialDirectoryDecision.outcome === "skipped") {
216905
- const skipLine = `Claude credential directory skipped: ${credentialDirectoryDecision.reason}`;
216906
- yield* credentialDirectoryDecision.reason === "no-account" ? logDebug(skipLine) : logWarning$1(skipLine);
216907
- }
216908
- const claudeCredentialDirectory = credentialDirectoryDecision.outcome === "set" ? credentialDirectoryDecision.directory : null;
217725
+ const { selectedAccountIdentity, claudeCredentialDirectory } = yield* resolveSelectedCredentialDirectory(input, resumeState, claudeBinaryPath);
216909
217726
  const modelConfig = resolveClaudeSessionModelConfig(input.modelSelection, claudeSettings.endpointProfiles);
216910
- const providerProxyCredential = yield* issueClaudeProviderProxyCredential(modelConfig.endpointProfile, modelConfig.modelSlug ?? modelConfig.effectiveApiModelId ?? "unknown", threadId);
217727
+ if (selectedAccountIdentity !== null && modelConfig.endpointProfile !== void 0) return yield* new ProviderAdapterValidationError({
217728
+ provider: PROVIDER$11,
217729
+ operation: "startSession",
217730
+ issue: "The selected Claude account cannot be used with a custom API endpoint. Choose a subscription model."
217731
+ });
217732
+ const providerProxyCredential = selectedAccountIdentity !== null ? null : yield* issueClaudeProviderProxyCredential(modelConfig.endpointProfile, modelConfig.modelSlug ?? modelConfig.effectiveApiModelId ?? "unknown", threadId);
216911
217733
  const permissionMode = permissionModeForRuntimeMode(input.runtimeMode);
216912
217734
  const settings = claudeQuerySettings(modelConfig.thinking, modelConfig.fastMode);
216913
217735
  const workspacePreflight = input.cwd ? formatWorkspaceEnvironmentPreflightForAgent(runWorkspaceEnvironmentPreflight({ cwd: input.cwd })) : void 0;
@@ -216930,7 +217752,7 @@ const makeClaudeAdapter = fn$1("makeClaudeAdapter")(function* (options) {
216930
217752
  existingResumeSessionId,
216931
217753
  newSessionId,
216932
217754
  canUseTool,
216933
- env: buildClaudeQueryEnv(process.env, modelConfig.endpointProfile, claudeSettings, providerProxyCredential, claudeCredentialDirectory),
217755
+ env: buildClaudeQueryEnv(process.env, modelConfig.endpointProfile, claudeSettings, providerProxyCredential, claudeCredentialDirectory, selectedAccountIdentity !== null),
216934
217756
  browserManager,
216935
217757
  threadId,
216936
217758
  browserToolEnvironmentId,
@@ -216982,7 +217804,9 @@ const makeClaudeAdapter = fn$1("makeClaudeAdapter")(function* (options) {
216982
217804
  });
216983
217805
  attachClaudeStreamFiber(context$4, runFork);
216984
217806
  return { ...session };
216985
- });
217807
+ }, (effect, input) => effect.pipe(onExit((exit) => sync$1(() => {
217808
+ if (exit._tag === "Failure") selectedClaudeCredentialStore(serverConfig.baseDir).release(input.threadId);
217809
+ }))));
216986
217810
  const sendTurn = fn$1("sendTurn")(function* (input) {
216987
217811
  const context = yield* requireSession(input.threadId);
216988
217812
  const modelSelection = input.modelSelection?.provider === "claudeAgent" || input.modelSelection?.provider === "apex" || input.modelSelection?.provider === "cmmd" ? input.modelSelection : void 0;
@@ -231264,6 +232088,9 @@ const ProviderRollbackConversationInput = Struct({
231264
232088
  threadId: ThreadId,
231265
232089
  numTurns: NonNegativeInt
231266
232090
  });
232091
+ function selectedAccountGuardFields(providerAccountId) {
232092
+ return providerAccountId === void 0 ? {} : { providerAccountId };
232093
+ }
231267
232094
  function cursorRecord(value) {
231268
232095
  return typeof value === "object" && value !== null && !Array.isArray(value) ? { ...value } : {};
231269
232096
  }
@@ -231356,9 +232183,13 @@ const makeProviderService = fn$1("makeProviderService")(function* (options) {
231356
232183
  const runtimeEventPubSub = yield* unbounded$1();
231357
232184
  const assertProviderExecutable = options?.assertProviderExecutable ?? (() => succeed(null));
231358
232185
  const assertCurrentProviderDelivery = fn$1("assertCurrentProviderDelivery")(function* (input) {
231359
- const currentBinding = yield* assertProviderExecutable(input);
231360
- if (!currentBinding) return;
231361
232186
  const persistedBinding = getOrUndefined(yield* directory.getBinding(input.threadId));
232187
+ const providerAccountId = readPersistedProviderAccountId(persistedBinding?.runtimePayload);
232188
+ const currentBinding = yield* assertProviderExecutable({
232189
+ ...input,
232190
+ ...selectedAccountGuardFields(providerAccountId)
232191
+ });
232192
+ if (!currentBinding) return;
231362
232193
  if (!persistedBinding || persistedBinding.provider !== input.provider) return yield* toValidationError(input.operation, `Cannot verify the delivered provider identity for thread '${input.threadId}'.`);
231363
232194
  yield* bindProviderDeliveryToResumeCursor({
231364
232195
  operation: input.operation,
@@ -231413,10 +232244,12 @@ const makeProviderService = fn$1("makeProviderService")(function* (options) {
231413
232244
  if (!hasResumeCursor) return yield* toValidationError(input.operation, `Cannot recover thread '${input.binding.threadId}' because no provider resume state is persisted.`);
231414
232245
  const persistedCwd = readPersistedCwd(input.binding.runtimePayload);
231415
232246
  const persistedModelSelection = readPersistedModelSelection(input.binding.runtimePayload);
232247
+ const persistedProviderAccountId = readProviderAccountIdFromModelSelection(persistedModelSelection);
231416
232248
  const currentDeliveryBinding = yield* assertProviderExecutable({
231417
232249
  threadId: input.binding.threadId,
231418
232250
  provider: input.binding.provider,
231419
- operation: input.operation
232251
+ operation: input.operation,
232252
+ ...selectedAccountGuardFields(persistedProviderAccountId)
231420
232253
  });
231421
232254
  const boundResumeCursor = yield* bindProviderDeliveryToResumeCursor({
231422
232255
  operation: input.operation,
@@ -231541,7 +232374,8 @@ const makeProviderService = fn$1("makeProviderService")(function* (options) {
231541
232374
  currentBinding: yield* assertProviderExecutable({
231542
232375
  threadId,
231543
232376
  provider: effectiveProvider,
231544
- operation: "ProviderService.startSession"
232377
+ operation: "ProviderService.startSession",
232378
+ ...selectedAccountGuardFields(requestedAccountId)
231545
232379
  })
231546
232380
  });
231547
232381
  const session = yield* adapter.startSession({
@@ -231607,6 +232441,8 @@ const makeProviderService = fn$1("makeProviderService")(function* (options) {
231607
232441
  allowRecovery: true
231608
232442
  });
231609
232443
  const persistedBinding = getOrUndefined(yield* directory.getBinding(input.threadId));
232444
+ const persistedModelSelection = readPersistedModelSelection(persistedBinding?.runtimePayload);
232445
+ const nextModelSelection = input.modelSelection ?? persistedModelSelection;
231610
232446
  yield* assertProviderAccountBoundary({
231611
232447
  operation: "ProviderService.sendTurn",
231612
232448
  threadId: input.threadId,
@@ -231631,7 +232467,7 @@ const makeProviderService = fn$1("makeProviderService")(function* (options) {
231631
232467
  status: "running",
231632
232468
  ...turn.resumeCursor !== void 0 ? { resumeCursor: turn.resumeCursor } : {},
231633
232469
  runtimePayload: {
231634
- ...input.modelSelection !== void 0 ? { modelSelection: input.modelSelection } : {},
232470
+ ...nextModelSelection !== void 0 ? { modelSelection: nextModelSelection } : {},
231635
232471
  activeTurnId: turn.turnId,
231636
232472
  lastRuntimeEvent: "provider.sendTurn",
231637
232473
  lastRuntimeEventAt: (/* @__PURE__ */ new Date()).toISOString()
@@ -231887,6 +232723,201 @@ function makeProviderServiceLive(options) {
231887
232723
  return effect(ProviderService, makeProviderService(options));
231888
232724
  }
231889
232725
  //#endregion
232726
+ //#region src/runtimeAuth/cmmdClaudeSelectedTicket.ts
232727
+ const ACCOUNT = /^pai_v1_[A-Za-z0-9_-]{43}$/;
232728
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
232729
+ const REVISION_HASH = /^[a-f0-9]{64}$/;
232730
+ function hasValidTicketLifetime(claims, payload) {
232731
+ const now = Math.floor(Date.now() / 1e3);
232732
+ const issuedAt = payload.iat;
232733
+ const notBefore = payload.nbf;
232734
+ return typeof issuedAt === "number" && Number.isSafeInteger(issuedAt) && issuedAt <= now + 5 && typeof notBefore === "number" && Number.isSafeInteger(notBefore) && notBefore <= now && Number.isSafeInteger(claims.expiresAtSeconds) && claims.expiresAtSeconds - issuedAt <= 900;
232735
+ }
232736
+ function hasSelectedCredentialScope(claims) {
232737
+ return claims.scope.length === 1 && claims.scope[0] === "claude-credential-read";
232738
+ }
232739
+ function hasExpectedSelectedBinding(payload, expected) {
232740
+ return payload.provider_account_id === expected.providerAccountId && ACCOUNT.test(expected.providerAccountId) && payload.thread_id === expected.threadId && payload.operation_id === expected.operationId && UUID.test(expected.operationId);
232741
+ }
232742
+ function selectedCredentialClaims(payload) {
232743
+ const jti = payload.jti;
232744
+ const generationId = payload.credential_generation_id;
232745
+ const credentialRevisionHash = payload.credential_revision_hash;
232746
+ const accountKind = payload.account_kind;
232747
+ if (typeof jti !== "string" || !jti.trim() || typeof generationId !== "string" || !UUID.test(generationId) || typeof credentialRevisionHash !== "string" || !REVISION_HASH.test(credentialRevisionHash) || accountKind !== "primary" && accountKind !== "linked") return null;
232748
+ return {
232749
+ jti,
232750
+ generationId,
232751
+ credentialRevisionHash,
232752
+ accountKind
232753
+ };
232754
+ }
232755
+ function verifyCmmdClaudeSelectedTicket(token, expected) {
232756
+ const verified = verifyCmmdScopedRuntimePayload(token, expected, "forge-claude-credential-read");
232757
+ if (!verified) return null;
232758
+ const { claims, payload } = verified;
232759
+ if (!hasValidTicketLifetime(claims, payload)) return null;
232760
+ if (!hasSelectedCredentialScope(claims)) return null;
232761
+ if (!hasExpectedSelectedBinding(payload, expected)) return null;
232762
+ const selected = selectedCredentialClaims(payload);
232763
+ if (!selected) return null;
232764
+ return {
232765
+ ...claims,
232766
+ jti: selected.jti,
232767
+ providerAccountId: expected.providerAccountId,
232768
+ generationId: selected.generationId,
232769
+ credentialRevisionHash: selected.credentialRevisionHash,
232770
+ accountKind: selected.accountKind,
232771
+ threadId: expected.threadId,
232772
+ operationId: expected.operationId
232773
+ };
232774
+ }
232775
+ //#endregion
232776
+ //#region src/provider/claudeSelectedCredentialAcquisition.ts
232777
+ function readToken(response) {
232778
+ if (typeof response !== "object" || response === null || !("data" in response)) return null;
232779
+ const data = response.data;
232780
+ return typeof data === "object" && data !== null && "token" in data && typeof data.token === "string" ? data.token : null;
232781
+ }
232782
+ async function acquireSelectedClaudeCredential(expected, transport) {
232783
+ try {
232784
+ const token = readToken(await transport.requestTicket());
232785
+ const claims = token === null ? null : verifyCmmdClaudeSelectedTicket(token, expected);
232786
+ if (!token || !claims) throw new Error("invalid_ticket");
232787
+ const credentialJson = await transport.readCredential(token);
232788
+ if (Buffer.byteLength(credentialJson, "utf8") > 512e3 || !hasUsableClaudeCredentialBlob(credentialJson)) throw new Error("invalid_credential");
232789
+ return {
232790
+ claims,
232791
+ credentialJson
232792
+ };
232793
+ } catch {
232794
+ throw new Error("The selected Claude account could not be prepared. Reconnect the account and try again.");
232795
+ }
232796
+ }
232797
+ //#endregion
232798
+ //#region src/provider/claudeSelectedCredentialPreparer.ts
232799
+ const REQUEST_TIMEOUT_MS = 1e4;
232800
+ const OWNER_TICKET_PATH = "/api/forge/context/providers/claude/runtime-read-ticket";
232801
+ const CREDENTIAL_READ_PATH = "/api/forge/context/auth-runtime-claude-credential";
232802
+ function isRuntimeTicketSession$1(sessionId) {
232803
+ return String(sessionId).startsWith("cmmd-runtime:");
232804
+ }
232805
+ function nonEmptyConfigString(value) {
232806
+ return value?.trim() || null;
232807
+ }
232808
+ function selectedRuntimeStrings(config) {
232809
+ const values = [
232810
+ nonEmptyConfigString(config.forgeCmmdRuntimeId),
232811
+ nonEmptyConfigString(config.forgeCmmdRuntimeMachineId),
232812
+ nonEmptyConfigString(config.forgeCmmdRuntimeOrgId),
232813
+ nonEmptyConfigString(config.forgeCmmdRuntimeUserId),
232814
+ nonEmptyConfigString(config.forgeCmmdRuntimePublicKey)
232815
+ ];
232816
+ if (values.includes(null)) return null;
232817
+ return {
232818
+ runtimeId: values[0],
232819
+ machineId: values[1],
232820
+ orgId: values[2],
232821
+ userId: values[3],
232822
+ publicKeyPem: values[4]
232823
+ };
232824
+ }
232825
+ function selectedCredentialEpoch(config) {
232826
+ const credentialEpoch = config.forgeCmmdRuntimeCredentialEpoch;
232827
+ return typeof credentialEpoch === "number" && Number.isSafeInteger(credentialEpoch) && credentialEpoch >= 0 ? credentialEpoch : null;
232828
+ }
232829
+ function selectedConfig(config) {
232830
+ if (config.forgeCmmdRuntimeMode !== true) return null;
232831
+ const strings = selectedRuntimeStrings(config);
232832
+ const credentialEpoch = selectedCredentialEpoch(config);
232833
+ if (strings === null || credentialEpoch === null) return null;
232834
+ return {
232835
+ ...strings,
232836
+ credentialEpoch
232837
+ };
232838
+ }
232839
+ async function boundedRuntimeTicketRequest(grant, runtimeId, request) {
232840
+ const response = await fetch(resolveCommandAppEndpoint(`/api/forge/runtimes/${encodeURIComponent(runtimeId)}/provider/claude/read-ticket`), {
232841
+ method: "POST",
232842
+ headers: {
232843
+ authorization: `Bearer ${grant}`,
232844
+ "content-type": "application/json"
232845
+ },
232846
+ body: JSON.stringify(request),
232847
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
232848
+ });
232849
+ if (!response.ok) throw new Error("selected_ticket_unavailable");
232850
+ return await response.json();
232851
+ }
232852
+ async function boundedCredentialRead(token) {
232853
+ const response = await fetch(resolveCommandAppEndpoint(CREDENTIAL_READ_PATH), {
232854
+ headers: {
232855
+ authorization: `Bearer ${token}`,
232856
+ accept: "application/json"
232857
+ },
232858
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS)
232859
+ });
232860
+ if (!response.ok) throw new Error("selected_credential_unavailable");
232861
+ return await response.text();
232862
+ }
232863
+ function missingAuthority() {
232864
+ throw new Error("selected_authority_unavailable");
232865
+ }
232866
+ /**
232867
+ * Acquires one signed, account-scoped Claude credential for the thread that
232868
+ * selected it. Owner and runtime-ticket sessions use different CMMD lanes.
232869
+ */
232870
+ function makeClaudeSelectedCredentialPreparer(config, dependencies) {
232871
+ const readRuntimeAuthority = dependencies.readRuntimeAuthority ?? readRuntimeBrokerAuthority;
232872
+ const requestOwnerTicket = dependencies.requestOwnerTicket ?? ((jwt, request) => proxyPost(jwt, OWNER_TICKET_PATH, request, void 0, { signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS) }));
232873
+ const requestRuntimeTicket = dependencies.requestRuntimeTicket ?? boundedRuntimeTicketRequest;
232874
+ const readCredential = dependencies.readCredential ?? boundedCredentialRead;
232875
+ const prepare = dependencies.prepare ?? ((threadId, acquired) => {
232876
+ if (!config.baseDir) return Promise.reject(/* @__PURE__ */ new Error("selected_store_unavailable"));
232877
+ return selectedClaudeCredentialStore(config.baseDir).prepare(threadId, acquired);
232878
+ });
232879
+ const createOperationId = dependencies.createOperationId ?? randomUUID;
232880
+ return async (input) => {
232881
+ const sealed = selectedConfig(config);
232882
+ const sessionId = input.executionSessionId ?? dependencies.readSessionId(input.threadId);
232883
+ if (sealed === null || sessionId === null) return missingAuthority();
232884
+ const operationId = createOperationId();
232885
+ const request = {
232886
+ providerAccountId: input.providerAccountId,
232887
+ threadId: String(input.threadId),
232888
+ operationId
232889
+ };
232890
+ const acquired = await acquireSelectedClaudeCredential({
232891
+ ...sealed,
232892
+ providerAccountId: input.providerAccountId,
232893
+ threadId: String(input.threadId),
232894
+ operationId
232895
+ }, {
232896
+ requestTicket: async () => {
232897
+ if (isRuntimeTicketSession$1(sessionId)) {
232898
+ const grant = readRuntimeAuthority({
232899
+ sessionId,
232900
+ runtimeId: sealed.runtimeId,
232901
+ machineId: sealed.machineId,
232902
+ credentialEpoch: sealed.credentialEpoch,
232903
+ requiredScope: "provider"
232904
+ });
232905
+ if (grant === null) return missingAuthority();
232906
+ return { data: await requestRuntimeTicket(grant, sealed.runtimeId, request) };
232907
+ }
232908
+ const jwt = await dependencies.resolveJwt(sessionId, { forceRefresh: false });
232909
+ if (jwt === null) return missingAuthority();
232910
+ return await requestOwnerTicket(jwt, {
232911
+ ...request,
232912
+ runtimeId: sealed.runtimeId
232913
+ });
232914
+ },
232915
+ readCredential
232916
+ });
232917
+ return await prepare(String(input.threadId), acquired);
232918
+ };
232919
+ }
232920
+ //#endregion
231890
232921
  //#region src/provider/claudeRuntimeReadinessGuard.ts
231891
232922
  const GENERATION_HASH_PATTERN = /^[a-f0-9]{64}$/;
231892
232923
  const RUNTIME_READINESS_TIMEOUT_MS$1 = 1e4;
@@ -232155,7 +233186,22 @@ function makeClaudeRuntimeReadinessGuard(config, dependencies) {
232155
233186
  return (input) => claudeReadinessGuardEffect(config, dependencies, resolved, input);
232156
233187
  }
232157
233188
  function claudeReadinessGuardEffect(config, dependencies, resolved, input) {
232158
- if (!requiresNativeClaudeReadiness(config, input.provider)) return succeed(null);
233189
+ if (!requiresNativeClaudeReadiness(config, input.provider, input.providerAccountId)) return succeed(null);
233190
+ if (input.providerAccountId !== void 0) {
233191
+ const prepareSelectedCredential = dependencies.prepareSelectedCredential ?? makeClaudeSelectedCredentialPreparer(config, {
233192
+ resolveJwt: dependencies.resolveJwt,
233193
+ readSessionId: resolved.readSessionId,
233194
+ readRuntimeAuthority: resolved.readRuntimeAuthority
233195
+ });
233196
+ const selectedInput = {
233197
+ ...input,
233198
+ providerAccountId: input.providerAccountId
233199
+ };
233200
+ return tryPromise({
233201
+ try: () => prepareSelectedCredential(selectedInput),
233202
+ catch: () => selectedCredentialValidationError(input.operation, selectedInput.providerAccountId)
233203
+ });
233204
+ }
232159
233205
  const authority = resolveGuardAuthority(config, resolved.readSessionId, input);
232160
233206
  if (authority === null) return invalidOwnerSessionEffect(input.operation);
232161
233207
  return tryPromise({
@@ -232163,8 +233209,9 @@ function claudeReadinessGuardEffect(config, dependencies, resolved, input) {
232163
233209
  catch: (cause) => readinessValidationError(input.operation, cause)
232164
233210
  });
232165
233211
  }
232166
- function requiresNativeClaudeReadiness(config, provider) {
232167
- return provider === "claudeAgent" && config.forgeCmmdRuntimeMode === true && personalClaudeInVmEnabled();
233212
+ function requiresNativeClaudeReadiness(config, provider, providerAccountId) {
233213
+ if (provider !== "claudeAgent" || config.forgeCmmdRuntimeMode !== true) return false;
233214
+ return providerAccountId !== void 0 || personalClaudeInVmEnabled();
232168
233215
  }
232169
233216
  function resolveGuardAuthority(config, readSessionId, input) {
232170
233217
  const runtimeId = config.forgeCmmdRuntimeId?.trim();
@@ -232201,6 +233248,12 @@ async function verifyGuardAuthority(config, dependencies, resolved, authority) {
232201
233248
  fetchReadiness: resolved.fetchReadiness
232202
233249
  });
232203
233250
  }
233251
+ function selectedCredentialValidationError(operation, accountId) {
233252
+ return new ProviderValidationError({
233253
+ operation,
233254
+ issue: `The selected Claude account ${accountId} could not be prepared. Reconnect the account and try again.`
233255
+ });
233256
+ }
232204
233257
  function readinessValidationError(operation, cause) {
232205
233258
  return new ProviderValidationError({
232206
233259
  operation,
@@ -267698,7 +268751,7 @@ var import_multicast_dns = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMi
267698
268751
  var dgram = __require("dgram");
267699
268752
  var thunky = require_thunky();
267700
268753
  var events = __require("events");
267701
- var os = __require("os");
268754
+ var os$1 = __require("os");
267702
268755
  var noop = function() {};
267703
268756
  module.exports = function(opts) {
267704
268757
  if (!opts) opts = {};
@@ -267829,14 +268882,14 @@ var import_multicast_dns = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMi
267829
268882
  return that;
267830
268883
  };
267831
268884
  function defaultInterface() {
267832
- var networks = os.networkInterfaces();
268885
+ var networks = os$1.networkInterfaces();
267833
268886
  var names = Object.keys(networks);
267834
268887
  for (var i = 0; i < names.length; i++) {
267835
268888
  var net = networks[names[i]];
267836
268889
  for (var j = 0; j < net.length; j++) {
267837
268890
  var iface = net[j];
267838
268891
  if (isIPv4(iface.family) && !iface.internal) {
267839
- if (os.platform() === "darwin" && names[i] === "en0") return iface.address;
268892
+ if (os$1.platform() === "darwin" && names[i] === "en0") return iface.address;
267840
268893
  return "0.0.0.0";
267841
268894
  }
267842
268895
  }
@@ -267844,7 +268897,7 @@ var import_multicast_dns = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMi
267844
268897
  return "127.0.0.1";
267845
268898
  }
267846
268899
  function allInterfaces() {
267847
- var networks = os.networkInterfaces();
268900
+ var networks = os$1.networkInterfaces();
267848
268901
  var names = Object.keys(networks);
267849
268902
  var res = [];
267850
268903
  for (var i = 0; i < names.length; i++) {
@@ -290898,6 +291951,10 @@ const ProviderCommandReactorLive = effect(ProviderCommandReactor, gen(function*
290898
291951
  shouldRestartForAccountChange,
290899
291952
  hasResumeCursor: resumeCursor !== void 0
290900
291953
  });
291954
+ if (shouldRestartForAccountChange) yield* providerService.stopSession({
291955
+ threadId,
291956
+ preserveResumeState: true
291957
+ });
290901
291958
  const restartedSession = yield* startProviderSession(resumeCursor !== void 0 ? { resumeCursor } : void 0);
290902
291959
  yield* logInfo("provider command reactor restarted provider session", {
290903
291960
  threadId,
@@ -296833,7 +297890,7 @@ function resolveBuildCommitFromEnv(env) {
296833
297890
  * environment descriptor down instead of reporting an honest "unknown".
296834
297891
  */
296835
297892
  function readBakedBuildCommit() {
296836
- return "05d8568497fdceb5b5d827b142ea4c681bef25ce";
297893
+ return "81ce9360f406bcb5ae9fbdc9f90dd55d33f25f0c";
296837
297894
  }
296838
297895
  async function resolveServerBuildCommit(input) {
296839
297896
  if (isFullCommitSha(input.baked)) return input.baked;
@@ -298246,20 +299303,20 @@ const generatePairingToken = () => {
298246
299303
  const randomBytes = crypto.getRandomValues(new Uint8Array(PAIRING_TOKEN_LENGTH));
298247
299304
  return Array.from(randomBytes, (value) => PAIRING_TOKEN_ALPHABET[value & 31]).join("");
298248
299305
  };
299306
+ const invalidBootstrapCredentialError = (message) => new BootstrapCredentialError({
299307
+ message,
299308
+ status: 401
299309
+ });
299310
+ const internalBootstrapCredentialError = (message, cause) => new BootstrapCredentialError({
299311
+ message,
299312
+ status: 500,
299313
+ cause
299314
+ });
298249
299315
  const BootstrapCredentialServiceLive = effect(BootstrapCredentialService, gen(function* () {
298250
299316
  const config = yield* ServerConfig;
298251
299317
  const pairingLinks = yield* AuthPairingLinkRepository;
298252
299318
  const seededGrantsRef = yield* make$50(/* @__PURE__ */ new Map());
298253
299319
  const changesPubSub = yield* unbounded$1();
298254
- const invalidBootstrapCredentialError = (message) => new BootstrapCredentialError({
298255
- message,
298256
- status: 401
298257
- });
298258
- const internalBootstrapCredentialError = (message, cause) => new BootstrapCredentialError({
298259
- message,
298260
- status: 500,
298261
- cause
298262
- });
298263
299320
  const seedGrant = (credential, grant) => update$4(seededGrantsRef, (current) => {
298264
299321
  const next = new Map(current);
298265
299322
  next.set(credential, grant);
@@ -298274,19 +299331,19 @@ const BootstrapCredentialServiceLive = effect(BootstrapCredentialService, gen(fu
298274
299331
  id
298275
299332
  }).pipe(asVoid);
298276
299333
  if (config.desktopBootstrapToken) {
298277
- const now$2 = yield* now;
299334
+ const now$3 = yield* now;
298278
299335
  yield* seedGrant(config.desktopBootstrapToken, {
298279
299336
  method: "desktop-bootstrap",
298280
299337
  role: "owner",
298281
299338
  subject: "desktop-bootstrap",
298282
- expiresAt: add$3(now$2, { milliseconds: toMillis(DESKTOP_BOOTSTRAP_TOKEN_TTL) }),
299339
+ expiresAt: add$3(now$3, { milliseconds: toMillis(DESKTOP_BOOTSTRAP_TOKEN_TTL) }),
298283
299340
  remainingUses: "unbounded"
298284
299341
  });
298285
299342
  }
298286
299343
  const toBootstrapCredentialError = (message) => (cause) => internalBootstrapCredentialError(message, cause);
298287
299344
  const listActive = () => gen(function* () {
298288
- const now$5 = yield* now;
298289
- return (yield* pairingLinks.listActive({ now: now$5 })).map((row) => row.label ? {
299345
+ const now$2 = yield* now;
299346
+ return (yield* pairingLinks.listActive({ now: now$2 })).map((row) => row.label ? {
298290
299347
  id: row.id,
298291
299348
  credential: row.credential,
298292
299349
  role: row.role,
@@ -298316,8 +299373,8 @@ const BootstrapCredentialServiceLive = effect(BootstrapCredentialService, gen(fu
298316
299373
  const id = crypto.randomUUID();
298317
299374
  const credential = generatePairingToken();
298318
299375
  const ttl = input?.ttl ?? DEFAULT_ONE_TIME_TOKEN_TTL_MINUTES;
298319
- const now$4 = yield* now;
298320
- const expiresAt = add$3(now$4, { milliseconds: toMillis(ttl) });
299376
+ const now$5 = yield* now;
299377
+ const expiresAt = add$3(now$5, { milliseconds: toMillis(ttl) });
298321
299378
  const issued = {
298322
299379
  id,
298323
299380
  credential,
@@ -298331,7 +299388,7 @@ const BootstrapCredentialServiceLive = effect(BootstrapCredentialService, gen(fu
298331
299388
  role: input?.role ?? "client",
298332
299389
  subject: input?.subject ?? "one-time-token",
298333
299390
  label: input?.label ?? null,
298334
- createdAt: now$4,
299391
+ createdAt: now$5,
298335
299392
  expiresAt
298336
299393
  });
298337
299394
  yield* emitUpsert({
@@ -298340,13 +299397,13 @@ const BootstrapCredentialServiceLive = effect(BootstrapCredentialService, gen(fu
298340
299397
  role: input?.role ?? "client",
298341
299398
  subject: input?.subject ?? "one-time-token",
298342
299399
  ...input?.label ? { label: input.label } : {},
298343
- createdAt: now$4,
299400
+ createdAt: now$5,
298344
299401
  expiresAt
298345
299402
  });
298346
299403
  return issued;
298347
299404
  }).pipe(mapError(toBootstrapCredentialError("Failed to issue pairing credential.")));
298348
299405
  const consume = (credential) => gen(function* () {
298349
- const now$3 = yield* now;
299406
+ const now$4 = yield* now;
298350
299407
  const seededResult = yield* modify$3(seededGrantsRef, (current) => {
298351
299408
  const grant = current.get(credential);
298352
299409
  if (!grant) return [{
@@ -298355,7 +299412,7 @@ const BootstrapCredentialServiceLive = effect(BootstrapCredentialService, gen(fu
298355
299412
  error: invalidBootstrapCredentialError("Unknown bootstrap credential.")
298356
299413
  }, current];
298357
299414
  const next = new Map(current);
298358
- if (isGreaterThanOrEqualTo$1(now$3, grant.expiresAt)) {
299415
+ if (isGreaterThanOrEqualTo$1(now$4, grant.expiresAt)) {
298359
299416
  next.delete(credential);
298360
299417
  return [{
298361
299418
  _tag: "error",
@@ -298384,8 +299441,8 @@ const BootstrapCredentialServiceLive = effect(BootstrapCredentialService, gen(fu
298384
299441
  if (seededResult.reason !== "not-found") return yield* seededResult.error;
298385
299442
  const consumed = yield* pairingLinks.consumeAvailable({
298386
299443
  credential,
298387
- consumedAt: now$3,
298388
- now: now$3
299444
+ consumedAt: now$4,
299445
+ now: now$4
298389
299446
  });
298390
299447
  if (isSome(consumed)) {
298391
299448
  yield* emitRemoved(consumed.value.id);
@@ -298401,9 +299458,9 @@ const BootstrapCredentialServiceLive = effect(BootstrapCredentialService, gen(fu
298401
299458
  if (isNone(matching)) return yield* invalidBootstrapCredentialError("Unknown bootstrap credential.");
298402
299459
  if (matching.value.revokedAt !== null) return yield* invalidBootstrapCredentialError("Bootstrap credential is no longer available.");
298403
299460
  if (matching.value.consumedAt !== null) return yield* invalidBootstrapCredentialError("Unknown bootstrap credential.");
298404
- if (isGreaterThanOrEqualTo$1(now$3, matching.value.expiresAt)) return yield* invalidBootstrapCredentialError("Bootstrap credential expired.");
299461
+ if (isGreaterThanOrEqualTo$1(now$4, matching.value.expiresAt)) return yield* invalidBootstrapCredentialError("Bootstrap credential expired.");
298405
299462
  return yield* invalidBootstrapCredentialError("Bootstrap credential is no longer available.");
298406
- }).pipe(mapError((cause) => cause instanceof BootstrapCredentialError ? cause : internalBootstrapCredentialError("Failed to consume bootstrap credential.", cause)));
299463
+ }).pipe(mapError((cause) => cause instanceof BootstrapCredentialError ? cause : internalBootstrapCredentialError("Failed to consume bootstrap credential.", cause)), tapError((error) => error.status === 401 ? logWarning$1("Rejected bootstrap credential exchange.", { reason: error.message }) : void_$1));
298407
299464
  return {
298408
299465
  issueOneTimeToken,
298409
299466
  listActive,
@@ -298458,6 +299515,10 @@ const SetAuthSessionLastConnectedAtInput = Struct({
298458
299515
  sessionId: AuthSessionId,
298459
299516
  lastConnectedAt: DateTimeUtcFromString
298460
299517
  });
299518
+ const ExtendAuthSessionExpiryInput = Struct({
299519
+ sessionId: AuthSessionId,
299520
+ expiresAt: DateTimeUtcFromString
299521
+ });
298461
299522
  var AuthSessionRepository = class extends Service$1()("forge/persistence/Services/AuthSessions/AuthSessionRepository") {};
298462
299523
  //#endregion
298463
299524
  //#region src/persistence/Layers/AuthSessions.ts
@@ -298609,6 +299670,17 @@ const AuthSessionRepositoryLive = effect(AuthSessionRepository, gen(function* ()
298609
299670
  WHERE session_id = ${sessionId}
298610
299671
  AND revoked_at IS NULL
298611
299672
  RETURNING session_id AS "sessionId"
299673
+ `
299674
+ });
299675
+ const extendSessionExpiryRows = findAll({
299676
+ Request: ExtendAuthSessionExpiryInput,
299677
+ Result: Struct({ sessionId: AuthSessionId }),
299678
+ execute: ({ sessionId, expiresAt }) => sql`
299679
+ UPDATE auth_sessions
299680
+ SET expires_at = ${expiresAt}
299681
+ WHERE session_id = ${sessionId}
299682
+ AND revoked_at IS NULL
299683
+ RETURNING session_id AS "sessionId"
298612
299684
  `
298613
299685
  });
298614
299686
  const revokeOtherSessionRows = findAll({
@@ -298631,13 +299703,15 @@ const AuthSessionRepositoryLive = effect(AuthSessionRepository, gen(function* ()
298631
299703
  const revoke = (input) => revokeSessionRows(input).pipe(mapError(toPersistenceSqlOrDecodeError$3("AuthSessionRepository.revoke:query", "AuthSessionRepository.revoke:decodeRows")), map$3((rows) => rows.length > 0));
298632
299704
  const revokeAllExcept = (input) => revokeOtherSessionRows(input).pipe(mapError(toPersistenceSqlOrDecodeError$3("AuthSessionRepository.revokeAllExcept:query", "AuthSessionRepository.revokeAllExcept:decodeRows")), map$3((rows) => rows.map((row) => row.sessionId)));
298633
299705
  const setLastConnectedAt = (input) => setLastConnectedAtRow(input).pipe(mapError(toPersistenceSqlOrDecodeError$3("AuthSessionRepository.setLastConnectedAt:query", "AuthSessionRepository.setLastConnectedAt:encodeRequest")));
299706
+ const extendExpiry = (input) => extendSessionExpiryRows(input).pipe(mapError(toPersistenceSqlOrDecodeError$3("AuthSessionRepository.extendExpiry:query", "AuthSessionRepository.extendExpiry:decodeRows")), map$3((rows) => rows.length > 0));
298634
299707
  return {
298635
299708
  create,
298636
299709
  getById,
298637
299710
  listActive,
298638
299711
  revoke,
298639
299712
  revokeAllExcept,
298640
- setLastConnectedAt
299713
+ setLastConnectedAt,
299714
+ extendExpiry
298641
299715
  };
298642
299716
  }));
298643
299717
  //#endregion
@@ -298645,6 +299719,20 @@ const AuthSessionRepositoryLive = effect(AuthSessionRepository, gen(function* ()
298645
299719
  const SIGNING_SECRET_NAME = "server-signing-key";
298646
299720
  const DEFAULT_SESSION_TTL = days(30);
298647
299721
  const DEFAULT_WEBSOCKET_TOKEN_TTL = minutes(5);
299722
+ /**
299723
+ * Whether a session presented at `nowMs` has used up half its own lifetime.
299724
+ *
299725
+ * A phone that opens the app daily should never hit the 30-day wall, but a
299726
+ * lost phone's session should still die on schedule if it is never presented
299727
+ * again. Sliding the expiry forward only when the session is already more
299728
+ * than half spent gives an active phone many chances to renew before expiry
299729
+ * while keeping a dormant session's remaining lifetime bounded by the last
299730
+ * time it was actually used.
299731
+ */
299732
+ function isPastHalfLife(input) {
299733
+ const halfLifeMarkMs = input.issuedAtMs + (input.expiresAtMs - input.issuedAtMs) / 2;
299734
+ return input.nowMs >= halfLifeMarkMs;
299735
+ }
298648
299736
  const toSessionCredentialError = (message) => (cause) => new SessionCredentialError({
298649
299737
  message,
298650
299738
  cause
@@ -298805,6 +299893,42 @@ const SessionCredentialServiceLive = effect(SessionCredentialService, gen(functi
298805
299893
  role: claims.role
298806
299894
  };
298807
299895
  }).pipe(mapError(toSessionCredentialError("Failed to issue session credential.")));
299896
+ /**
299897
+ * Slide a session's expiry forward when the token it just verified is past
299898
+ * half its own lifetime, so a phone that keeps using the app never runs
299899
+ * into the 30-day wall.
299900
+ *
299901
+ * The session id, subject, role and method carry over unchanged — only the
299902
+ * signature-bearing `iat`/`exp` differ — which is what lets the DB row be
299903
+ * extended in place (`extendExpiry`) rather than replaced: everything keyed
299904
+ * by session id, most importantly the `command_access_tokens` row holding
299905
+ * this session's CMMD credential, needs no migration.
299906
+ */
299907
+ const renewIfPastHalfLife = (input) => gen(function* () {
299908
+ const nowMs = yield* currentTimeMillis;
299909
+ if (!isPastHalfLife({
299910
+ issuedAtMs: input.row.issuedAt.epochMilliseconds,
299911
+ expiresAtMs: input.row.expiresAt.epochMilliseconds,
299912
+ nowMs
299913
+ })) return void 0;
299914
+ const newIssuedAt = yield* now;
299915
+ const newExpiresAt = add$3(newIssuedAt, { milliseconds: toMillis(DEFAULT_SESSION_TTL) });
299916
+ const newClaims = {
299917
+ ...input.claims,
299918
+ iat: newIssuedAt.epochMilliseconds,
299919
+ exp: newExpiresAt.epochMilliseconds
299920
+ };
299921
+ const newEncodedPayload = base64UrlEncode(JSON.stringify(newClaims));
299922
+ const newSignature = signPayload(newEncodedPayload, signingSecret);
299923
+ if (!(yield* authSessions.extendExpiry({
299924
+ sessionId: input.claims.sid,
299925
+ expiresAt: newExpiresAt
299926
+ }).pipe(orElseSucceed(() => false)))) return void 0;
299927
+ return {
299928
+ token: `${newEncodedPayload}.${newSignature}`,
299929
+ expiresAt: newExpiresAt
299930
+ };
299931
+ });
298808
299932
  const verify = (token) => gen(function* () {
298809
299933
  const [encodedPayload, signature] = token.split(".");
298810
299934
  if (!encodedPayload || !signature) return yield* new SessionCredentialError({ message: "Malformed session token." });
@@ -298819,14 +299943,19 @@ const SessionCredentialServiceLive = effect(SessionCredentialService, gen(functi
298819
299943
  if (isSome(row)) {
298820
299944
  if (row.value.revokedAt !== null) return yield* new SessionCredentialError({ message: "Session token revoked." });
298821
299945
  } else if (!(statelessCookieSessions && claims.method === "browser-session-cookie")) return yield* new SessionCredentialError({ message: "Unknown session token." });
299946
+ const renewed = isSome(row) ? yield* renewIfPastHalfLife({
299947
+ claims,
299948
+ row: row.value
299949
+ }) : void 0;
298822
299950
  return {
298823
299951
  sessionId: claims.sid,
298824
299952
  token,
298825
299953
  method: claims.method,
298826
299954
  client: isSome(row) ? toClientMetadata(row.value.client) : createDefaultClientMetadata(),
298827
- expiresAt: makeUnsafe$5(claims.exp),
299955
+ expiresAt: renewed?.expiresAt ?? makeUnsafe$5(claims.exp),
298828
299956
  subject: claims.sub,
298829
- role: claims.role
299957
+ role: claims.role,
299958
+ ...renewed ? { renewed } : {}
298830
299959
  };
298831
299960
  }).pipe(mapError((cause) => cause instanceof SessionCredentialError ? cause : new SessionCredentialError({
298832
299961
  message: "Failed to verify session credential.",
@@ -299318,7 +300447,10 @@ const ServerAuthLive = effect(ServerAuth, gen(function* () {
299318
300447
  subject: session.subject,
299319
300448
  method: session.method,
299320
300449
  role: session.role,
299321
- ...session.expiresAt ? { expiresAt: session.expiresAt } : {}
300450
+ ...session.renewed ? {
300451
+ expiresAt: session.renewed.expiresAt,
300452
+ renewedSessionToken: session.renewed.token
300453
+ } : session.expiresAt ? { expiresAt: session.expiresAt } : {}
299322
300454
  })), mapError((cause) => new AuthError({
299323
300455
  message: "Unauthorized request.",
299324
300456
  status: 401,
@@ -299353,6 +300485,7 @@ const ServerAuthLive = effect(ServerAuth, gen(function* () {
299353
300485
  role: session.role,
299354
300486
  sessionMethod: session.method,
299355
300487
  ...session.expiresAt ? { expiresAt: toUtc(session.expiresAt) } : {},
300488
+ ...session.renewedSessionToken ? { renewedSessionToken: session.renewedSessionToken } : {},
299356
300489
  cmmdIdentity
299357
300490
  })))), tapError((e) => {
299358
300491
  if (e._tag === "AuthError" && (e.status ?? 500) < 500) return void_$1;
@@ -305679,6 +306812,82 @@ const orchestrationAutomationTerminalReconcileRouteLayer = add$1("POST", "/api/o
305679
306812
  }, { status: 200 });
305680
306813
  }).pipe(catchTag("OrchestrationDispatchCommandError", respondToWorkRunEventError)));
305681
306814
  //#endregion
306815
+ //#region src/orchestration/threadSnapshotRoute.ts
306816
+ /**
306817
+ * `GET /api/orchestration/threads/:threadId`, one thread's detail snapshot,
306818
+ * optionally windowed to a page of recent turns.
306819
+ *
306820
+ * Forge never served this route. The mobile client has asked for it on every
306821
+ * thread open since it shipped, fell through to the socket-embedded snapshot on
306822
+ * the 404, and its "load earlier" control had nowhere to fetch from, so a long
306823
+ * thread was only ever loadable in full. Serving it here gives the initial read
306824
+ * a body the transport can gzip, off the RPC socket, and gives older turns a
306825
+ * cursor to arrive on.
306826
+ *
306827
+ * The window is applied by the same pure function the WebSocket fallback
306828
+ * snapshot uses, so the two reads cannot disagree about what a page is.
306829
+ */
306830
+ /**
306831
+ * Reads the window out of the query string.
306832
+ *
306833
+ * Returns null for a malformed value rather than ignoring it. A client that
306834
+ * asked for 10 turns and silently received 4,000 has no way to tell, and the
306835
+ * failure would show up as a slow app rather than as an error.
306836
+ */
306837
+ function parseThreadSnapshotWindow(searchParams) {
306838
+ const rawTurnLimit = searchParams.get("turnLimit");
306839
+ const rawBeforeCursor = searchParams.get("beforeCursor");
306840
+ let turnLimit;
306841
+ if (rawTurnLimit !== null) {
306842
+ const parsed = Number(rawTurnLimit);
306843
+ if (!Number.isSafeInteger(parsed) || parsed < 1) return null;
306844
+ turnLimit = parsed;
306845
+ }
306846
+ let beforeCursor;
306847
+ if (rawBeforeCursor !== null) {
306848
+ const trimmed = rawBeforeCursor.trim();
306849
+ if (trimmed.length === 0) return null;
306850
+ beforeCursor = trimmed;
306851
+ }
306852
+ return {
306853
+ ...turnLimit === void 0 ? {} : { turnLimit },
306854
+ ...beforeCursor === void 0 ? {} : { beforeCursor }
306855
+ };
306856
+ }
306857
+ const orchestrationThreadSnapshotRouteLayer = add$1("GET", "/api/orchestration/threads/:threadId", gen(function* () {
306858
+ const request = yield* HttpServerRequest;
306859
+ yield* (yield* ServerAuth).authenticateHttpRequest(request);
306860
+ const rawThreadId = (yield* params)["threadId"];
306861
+ if (rawThreadId === void 0 || rawThreadId.length === 0) return jsonUnsafe({ error: "Missing thread id." }, { status: 400 });
306862
+ const url = toURL(request);
306863
+ const window = isNone(url) ? {} : parseThreadSnapshotWindow(url.value.searchParams);
306864
+ if (window === null) return jsonUnsafe({ error: "Invalid turnLimit or beforeCursor." }, { status: 400 });
306865
+ const threadId = ThreadId.make(decodeURIComponent(rawThreadId));
306866
+ const projectionSnapshotQuery = yield* ProjectionSnapshotQuery;
306867
+ const orchestrationEngine = yield* OrchestrationEngineService;
306868
+ const [threadDetail, snapshotSequence] = yield* all([projectionSnapshotQuery.getThreadDetailById(threadId).pipe(mapError((cause) => new OrchestrationGetSnapshotError({
306869
+ message: `Failed to load thread ${threadId}`,
306870
+ cause
306871
+ }))), orchestrationEngine.getReadModel().pipe(map$3((readModel) => readModel.snapshotSequence))]);
306872
+ if (isNone(threadDetail)) return jsonUnsafe({ error: "Thread not found." }, { status: 404 });
306873
+ const windowed = windowThreadDetail({
306874
+ thread: threadDetail.value,
306875
+ window,
306876
+ snapshotSequence
306877
+ });
306878
+ return jsonUnsafe({
306879
+ snapshotSequence,
306880
+ thread: windowed.thread,
306881
+ ...windowed.page === void 0 ? {} : { page: windowed.page }
306882
+ }, { status: 200 });
306883
+ }).pipe(catchTag("AuthError", respondToAuthError), catchTag("OrchestrationGetSnapshotError", (error) => gen(function* () {
306884
+ yield* logError("thread snapshot route failed", {
306885
+ message: error.message,
306886
+ cause: error.cause
306887
+ });
306888
+ return jsonUnsafe({ error: error.message }, { status: 500 });
306889
+ }))));
306890
+ //#endregion
305682
306891
  //#region src/localAutomations/scheduling.ts
305683
306892
  const MINUTE_MS = 6e4;
305684
306893
  const HOUR_MS = 60 * MINUTE_MS;
@@ -317547,7 +318756,7 @@ function makeProjectLeadControllerClientLive(options = {}) {
317547
318756
  const sql = yield* SqlClient;
317548
318757
  const fetchFn = options.fetchFn ?? fetch;
317549
318758
  const baseUrl = options.baseUrl ?? resolveCommandAppUrl();
317550
- const resolveStoredJwt = (forceRefresh) => gen(function* () {
318759
+ const resolveStoredJwt = (forceRefresh, staleJwt) => gen(function* () {
317551
318760
  const owner = yield* readMostRecentOwnerCommandAuthSession.pipe(provideService(SqlClient, sql), mapError((cause) => new ProjectLeadControllerClientError({
317552
318761
  kind: "unavailable",
317553
318762
  operation: "resolve-owner-session",
@@ -317561,7 +318770,8 @@ function makeProjectLeadControllerClientLive(options = {}) {
317561
318770
  sessionId: owner.sessionId,
317562
318771
  subject: owner.subject,
317563
318772
  logPrefix: "project-lead-scheduler",
317564
- forceRefresh
318773
+ forceRefresh,
318774
+ staleAccessToken: staleJwt
317565
318775
  }).pipe(provideService(SqlClient, sql));
317566
318776
  if (!jwt) return yield* new ProjectLeadControllerClientError({
317567
318777
  kind: "unauthenticated",
@@ -317604,7 +318814,7 @@ function makeProjectLeadControllerClientLive(options = {}) {
317604
318814
  }));
317605
318815
  if (first._tag === "Success") return first.success;
317606
318816
  if (first.failure.kind !== "unauthenticated") return yield* first.failure;
317607
- const refreshed = yield* resolveJwt(true);
318817
+ const refreshed = yield* resolveJwt(true, jwt);
317608
318818
  return yield* requestAttempt({
317609
318819
  ...input,
317610
318820
  jwt: refreshed
@@ -319998,12 +321208,12 @@ const gitUrlCloneRouteLayer = add$1("POST", "/api/cloud/git/clone-url", gen(func
319998
321208
  //#region src/server.ts
319999
321209
  const PtyAdapterLive = unwrap(gen(function* () {
320000
321210
  if (typeof Bun !== "undefined") return (yield* promise(() => import("./BunPTY-DkAIzLIx.mjs"))).layer;
320001
- else return (yield* promise(() => import("./NodePTY-B1u6RHXk.mjs"))).layer;
321211
+ else return (yield* promise(() => import("./NodePTY-BhezYy9H.mjs"))).layer;
320002
321212
  }));
320003
321213
  const HttpServerLive = unwrap(gen(function* () {
320004
321214
  const config = yield* ServerConfig;
320005
- if (typeof Bun !== "undefined") return (yield* promise(() => import("./BunHttpServer-CVPGkizx.mjs"))).layer(bunHttpServerOptions(config));
320006
- const [NodeHttpServer, NodeHttp] = yield* all([promise(() => import("./NodeHttpServer-UQnvZnmu.mjs")), promise(() => import("node:http"))]);
321215
+ if (typeof Bun !== "undefined") return (yield* promise(() => import("./BunHttpServer-FbbQmsl_.mjs"))).layer(bunHttpServerOptions(config));
321216
+ const [NodeHttpServer, NodeHttp] = yield* all([promise(() => import("./NodeHttpServer-BJ4l15f-.mjs")), promise(() => import("node:http"))]);
320007
321217
  return NodeHttpServer.layer(NodeHttp.createServer, {
320008
321218
  host: config.host,
320009
321219
  port: config.port
@@ -320011,10 +321221,10 @@ const HttpServerLive = unwrap(gen(function* () {
320011
321221
  }));
320012
321222
  const PlatformServicesLive = unwrap(gen(function* () {
320013
321223
  if (typeof Bun !== "undefined") {
320014
- const { layer } = yield* promise(() => import("./BunServices-CPrZSIfE.mjs"));
321224
+ const { layer } = yield* promise(() => import("./BunServices-DC7nhA3w.mjs"));
320015
321225
  return layer;
320016
321226
  } else {
320017
- const { layer } = yield* promise(() => import("./NodeServices-z1Ac4lEO.mjs").then((n) => n.t));
321227
+ const { layer } = yield* promise(() => import("./NodeServices-D3IxOzHl.mjs").then((n) => n.t));
320018
321228
  return layer;
320019
321229
  }
320020
321230
  }));
@@ -320079,7 +321289,7 @@ const MiscRuntimeLayerLive = mergeAll(AnalyticsServiceLayerLive, OpenLive, Serve
320079
321289
  const RuntimeDependenciesWithoutPullRequestsLive = ReactorLayerLive.pipe(provideMerge(CheckpointingLayerLive), provideMerge(GitLayerLive), provideMerge(OrchestrationLayerLive), provideMerge(ProviderLayerLive), provideMerge(TerminalLayerLive), provideMerge(AgentBrowserManagerLive), provideMerge(mergeAll(ThreadDevRuntimeLive, DevTunnelLive.pipe(provide$1(ThreadDevRuntimeLive)))), provideMerge(PersistenceLayerLive), provideMerge(KeybindingsLive), provideMerge(ProviderRegistryLive), provideMerge(mergeAll(ServerSettingsLive, UsageServiceLive.pipe(provide$1(ServerSettingsLive), provide$1(UsageSessionLookupLive), provide$1(UsageRecordedSourceLive.pipe(provide$1(OrchestrationLayerLive), provide$1(RuntimeReplicationOutboxLive), provide$1(ProviderSessionRuntimeRepositoryLive), provide$1(TextGenerationSessionRepositoryLive)))))), provideMerge(WorkspaceLayerLive), provideMerge(WorkspaceReadinessStateLive), provideMerge(ProjectFaviconResolverLive), provideMerge(RepositoryIdentityResolverLive), provideMerge(ServerEnvironmentLive), provideMerge(AuthLayerLive), provideMerge(ServerSecretStoreLayerLive), provideMerge(mergeAll(CloudRedisCoordinationLive, CloudStateSyncLive.pipe(provide$1(PersistenceLayerLive)))), provideMerge(MiscRuntimeLayerLive));
320080
321290
  const RuntimeDependenciesLive = pullRequestServiceFactoryLayer.pipe(provideMerge(RuntimeDependenciesWithoutPullRequestsLive), provideMerge(cmmdProjectIdentityResolverLive.pipe(provide$1(RuntimeDependenciesWithoutPullRequestsLive))), provideMerge(pullRequestProviderRuntimeLive));
320081
321291
  const RuntimeServicesLive = ServerRuntimeStartupLive.pipe(provideMerge(RuntimeDependenciesLive));
320082
- const makeRoutesLayer = mergeAll(attachmentUploadRouteLayer, authBearerBootstrapRouteLayer, authBootstrapRouteLayer, authCommandBearerBootstrapRouteLayer, authCommandBootstrapRouteLayer, authOAuthTokenExchangeRouteLayer, authWebSocketTicketRouteLayer, authCmmdOAuthDeviceRouteLayer, authCmmdOAuthTokenRouteLayer, authCmmdPasswordTokenRouteLayer, authClientsRevokeOthersRouteLayer, authClientsRevokeRouteLayer, authClientsRouteLayer, authLogoutRouteLayer, authPairingLinksRevokeRouteLayer, authPairingLinksRouteLayer, authPairingCredentialRouteLayer, authSessionRouteLayer, authWebSocketTokenRouteLayer, authWebLoginRouteLayer, authWebCallbackRouteLayer, desktopLocalWebBridgePublishRouteLayer, desktopLocalWebBridgeBootstrapRouteLayer, desktopRelayControlPlaneRouteLayer, apiDocsHtmlRouteLayer, apiDocsOpenApiRouteLayer, pulseSignalStateListRouteLayer, pulseSignalStateUpsertRouteLayer, cmmdProfileRouteLayer, ...cmmdProxyRouteLayers, cmmdWorkIntakeRouteLayer, cmmdWsTokenRouteLayer, cmmdGetProjectCommentsRouteLayer, cmmdAddProjectCommentRouteLayer, cmmdUpdateProjectRouteLayer, cmmdUploadTaskAttachmentRouteLayer, cmmdUpdateTaskCommentRouteLayer, cmmdListWorkspaceMembersRouteLayer, cmmdAddProjectMemberRouteLayer, cmmdUpdateProjectAgentMemberAvatarRouteLayer, runtimeReplicationStatusRouteLayer, cmmdRefreshProviderUsageRouteLayer, cmmdForgeCloudProjectsListRouteLayer, cmmdForgeCloudProjectsEventsRouteLayer, cmmdForgeRuntimesEnsureRouteLayer, cmmdForgeRuntimesConnectRouteLayer, ownerRuntimeCodexReadinessRouteLayer, cmmdForgeRuntimesStatusRouteLayer, cmmdForgeRuntimesStartRouteLayer, cmmdForgeRuntimesStopRouteLayer, cmmdForgeRuntimesRecycleRouteLayer, cmmdForgeRuntimesUpdateCommitRouteLayer, cmmdForgeRuntimesDestroyRouteLayer, mobileProviderCatalogRouteLayer, attachmentsRouteLayer, orchestrationDispatchRouteLayer, orchestrationCmmdWorkRunEventRouteLayer, orchestrationAutomationStartThreadRouteLayer, orchestrationAutomationRebindRouteLayer, orchestrationAutomationTerminalReconcileRouteLayer, mobileGitRunStackedActionRouteLayer, pullRequestDiffRouteLayer, orchestrationSnapshotRouteLayer, localAutomationListRouteLayer, localAutomationCreateRouteLayer, localAutomationUpdateRouteLayer, localAutomationDeleteRouteLayer, localAutomationRunRouteLayer, otlpTracesProxyRouteLayer, devTunnelProxyRouteLayer, devTunnelLocalPortProxyRouteLayer, projectFaviconRouteLayer, serverEnvironmentRouteLayer, browserWebRtcReadinessRouteLayer, staticAndDevRouteLayer, githubStatusRouteLayer, githubAuthorizeUrlRouteLayer, githubDisconnectRouteLayer, githubReposRouteLayer, githubCloneRouteLayer, githubCloneJobStartRouteLayer, githubCloneJobStatusRouteLayer, githubRefreshCredentialsRouteLayer, githubToolConfirmationApproveRouteLayer, projectLinkGetRouteLayer, projectLinkListRouteLayer, projectLinkSetRouteLayer, projectLinkDeleteRouteLayer, cmmdWorkspacesRouteLayer, cmmdCampaignsRouteLayer, cmmdProjectLinksRouteLayer, cmmdEnsureGitHubProjectContextRouteLayer, cmmdProjectRepositoryEnsureRouteLayer, cmmdProjectsGetRouteLayer, cmmdProjectsCreateRouteLayer, workspaceReadinessRouteLayer, runtimeLivenessRouteLayer, runtimeFilesystemHealthRouteLayer, claudeNativeGenerationProofRouteLayer, claudeUsageRouteLayer, codexRuntimeProviderReadinessRouteLayer, runtimeDiagnosticsRouteLayer, cloudFoldersRouteLayer, cloudProjectCreateRouteLayer, gitUrlCloneRouteLayer, websocketRpcRouteLayer).pipe(provide$1(browserApiCorsLayer));
321292
+ const makeRoutesLayer = mergeAll(attachmentUploadRouteLayer, authBearerBootstrapRouteLayer, authBootstrapRouteLayer, authCommandBearerBootstrapRouteLayer, authCommandBootstrapRouteLayer, authOAuthTokenExchangeRouteLayer, authWebSocketTicketRouteLayer, authCmmdOAuthDeviceRouteLayer, authCmmdOAuthTokenRouteLayer, authCmmdPasswordTokenRouteLayer, authClientsRevokeOthersRouteLayer, authClientsRevokeRouteLayer, authClientsRouteLayer, authLogoutRouteLayer, authPairingLinksRevokeRouteLayer, authPairingLinksRouteLayer, authPairingCredentialRouteLayer, authSessionRenewRouteLayer, authSessionRouteLayer, authWebSocketTokenRouteLayer, authWebLoginRouteLayer, authWebCallbackRouteLayer, desktopLocalWebBridgePublishRouteLayer, desktopLocalWebBridgeBootstrapRouteLayer, desktopRelayControlPlaneRouteLayer, apiDocsHtmlRouteLayer, apiDocsOpenApiRouteLayer, pulseSignalStateListRouteLayer, pulseSignalStateUpsertRouteLayer, cmmdProfileRouteLayer, ...cmmdProxyRouteLayers, cmmdWorkIntakeRouteLayer, cmmdWsTokenRouteLayer, cmmdGetProjectCommentsRouteLayer, cmmdAddProjectCommentRouteLayer, cmmdUpdateProjectRouteLayer, cmmdUploadTaskAttachmentRouteLayer, cmmdUpdateTaskCommentRouteLayer, cmmdListWorkspaceMembersRouteLayer, cmmdAddProjectMemberRouteLayer, cmmdUpdateProjectAgentMemberAvatarRouteLayer, runtimeReplicationStatusRouteLayer, cmmdRefreshProviderUsageRouteLayer, cmmdForgeCloudProjectsListRouteLayer, cmmdForgeCloudProjectsEventsRouteLayer, cmmdForgeRuntimesEnsureRouteLayer, cmmdForgeRuntimesConnectRouteLayer, ownerRuntimeCodexReadinessRouteLayer, cmmdForgeRuntimesStatusRouteLayer, cmmdForgeRuntimesStartRouteLayer, cmmdForgeRuntimesStopRouteLayer, cmmdForgeRuntimesRecycleRouteLayer, cmmdForgeRuntimesUpdateCommitRouteLayer, cmmdForgeRuntimesDestroyRouteLayer, mobileProviderCatalogRouteLayer, attachmentsRouteLayer, orchestrationDispatchRouteLayer, orchestrationCmmdWorkRunEventRouteLayer, orchestrationAutomationStartThreadRouteLayer, orchestrationAutomationRebindRouteLayer, orchestrationAutomationTerminalReconcileRouteLayer, mobileGitRunStackedActionRouteLayer, pullRequestDiffRouteLayer, orchestrationSnapshotRouteLayer, orchestrationThreadSnapshotRouteLayer, localAutomationListRouteLayer, localAutomationCreateRouteLayer, localAutomationUpdateRouteLayer, localAutomationDeleteRouteLayer, localAutomationRunRouteLayer, otlpTracesProxyRouteLayer, devTunnelProxyRouteLayer, devTunnelLocalPortProxyRouteLayer, projectFaviconRouteLayer, serverEnvironmentRouteLayer, browserWebRtcReadinessRouteLayer, staticAndDevRouteLayer, githubStatusRouteLayer, githubAuthorizeUrlRouteLayer, githubDisconnectRouteLayer, githubReposRouteLayer, githubCloneRouteLayer, githubCloneJobStartRouteLayer, githubCloneJobStatusRouteLayer, githubRefreshCredentialsRouteLayer, githubToolConfirmationApproveRouteLayer, projectLinkGetRouteLayer, projectLinkListRouteLayer, projectLinkSetRouteLayer, projectLinkDeleteRouteLayer, cmmdWorkspacesRouteLayer, cmmdCampaignsRouteLayer, cmmdProjectLinksRouteLayer, cmmdEnsureGitHubProjectContextRouteLayer, cmmdProjectRepositoryEnsureRouteLayer, cmmdProjectsGetRouteLayer, cmmdProjectsCreateRouteLayer, workspaceReadinessRouteLayer, runtimeLivenessRouteLayer, runtimeFilesystemHealthRouteLayer, claudeNativeGenerationProofRouteLayer, claudeUsageRouteLayer, codexRuntimeProviderReadinessRouteLayer, runtimeDiagnosticsRouteLayer, cloudFoldersRouteLayer, cloudProjectCreateRouteLayer, gitUrlCloneRouteLayer, websocketRpcRouteLayer).pipe(provide$1(browserApiCorsLayer));
320083
321293
  const runServer = launch$1(unwrap(gen(function* () {
320084
321294
  const config = yield* ServerConfig;
320085
321295
  fixPath({ preferProcessPath: config.forgeCmmdRuntimeMode === true });
@@ -321009,7 +322219,7 @@ const connectLoginCommand = make$34("login", {
321009
322219
  })).json();
321010
322220
  },
321011
322221
  openBrowser: async (url) => {
321012
- const open = (await import("./open-D8mGyWmt.mjs")).default;
322222
+ const open = (await import("./open-FIgTDosf.mjs")).default;
321013
322223
  await open(url);
321014
322224
  return true;
321015
322225
  },