stagent 0.9.5 → 0.10.0

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 (277) hide show
  1. package/README.md +5 -42
  2. package/dist/cli.js +42 -18
  3. package/docs/.coverage-gaps.json +13 -55
  4. package/docs/.last-generated +1 -1
  5. package/docs/features/provider-runtimes.md +4 -0
  6. package/docs/features/schedules.md +32 -4
  7. package/docs/features/settings.md +28 -5
  8. package/docs/features/tables.md +9 -2
  9. package/docs/features/workflows.md +10 -4
  10. package/docs/journeys/developer.md +15 -1
  11. package/docs/journeys/personal-use.md +21 -4
  12. package/docs/superpowers/plans/2026-04-07-instance-bootstrap.md +1691 -0
  13. package/docs/superpowers/plans/2026-04-08-schedule-orchestration.md +2983 -0
  14. package/docs/superpowers/plans/2026-04-11-schedule-maxturns-api-control.md +551 -0
  15. package/docs/superpowers/plans/2026-04-11-task-create-profile-validation.md +864 -0
  16. package/docs/superpowers/plans/2026-04-11-task-runtime-stagent-mcp-injection.md +739 -0
  17. package/docs/superpowers/specs/2026-04-08-chat-sse-resilience-hotfix-design.md +201 -0
  18. package/docs/superpowers/specs/2026-04-08-schedule-orchestration-design.md +371 -0
  19. package/docs/superpowers/specs/2026-04-08-swarm-visibility-design.md +213 -0
  20. package/package.json +3 -2
  21. package/src/__tests__/instrumentation-smoke.test.ts +15 -0
  22. package/src/app/analytics/page.tsx +1 -21
  23. package/src/app/api/chat/conversations/[id]/messages/route.ts +22 -1
  24. package/src/app/api/diagnostics/chat-streams/route.ts +65 -0
  25. package/src/app/api/instance/config/route.ts +41 -0
  26. package/src/app/api/instance/init/route.ts +34 -0
  27. package/src/app/api/instance/upgrade/check/route.ts +26 -0
  28. package/src/app/api/instance/upgrade/route.ts +96 -0
  29. package/src/app/api/instance/upgrade/status/route.ts +35 -0
  30. package/src/app/api/memory/route.ts +0 -11
  31. package/src/app/api/notifications/route.ts +4 -2
  32. package/src/app/api/projects/[id]/route.ts +5 -155
  33. package/src/app/api/projects/__tests__/delete-project.test.ts +10 -19
  34. package/src/app/api/schedules/[id]/execute/route.ts +111 -0
  35. package/src/app/api/schedules/[id]/route.ts +9 -1
  36. package/src/app/api/schedules/__tests__/execute-route.test.ts +118 -0
  37. package/src/app/api/schedules/route.ts +3 -12
  38. package/src/app/api/settings/openai/login/route.ts +22 -0
  39. package/src/app/api/settings/openai/logout/route.ts +7 -0
  40. package/src/app/api/settings/openai/route.ts +21 -1
  41. package/src/app/api/settings/providers/route.ts +35 -8
  42. package/src/app/api/tables/[id]/enrich/__tests__/route.test.ts +153 -0
  43. package/src/app/api/tables/[id]/enrich/plan/route.ts +98 -0
  44. package/src/app/api/tables/[id]/enrich/route.ts +147 -0
  45. package/src/app/api/tables/[id]/enrich/runs/route.ts +25 -0
  46. package/src/app/api/tasks/[id]/execute/route.ts +0 -21
  47. package/src/app/api/workflows/[id]/resume/route.ts +59 -0
  48. package/src/app/api/workflows/[id]/status/route.ts +22 -8
  49. package/src/app/api/workspace/context/route.ts +2 -0
  50. package/src/app/api/workspace/fix-data-dir/route.ts +81 -0
  51. package/src/app/chat/page.tsx +11 -0
  52. package/src/app/inbox/page.tsx +12 -5
  53. package/src/app/layout.tsx +42 -21
  54. package/src/app/page.tsx +0 -2
  55. package/src/app/settings/page.tsx +6 -9
  56. package/src/components/chat/__tests__/chat-session-provider.test.tsx +408 -0
  57. package/src/components/chat/chat-command-popover.tsx +2 -2
  58. package/src/components/chat/chat-input.tsx +2 -3
  59. package/src/components/chat/chat-session-provider.tsx +720 -0
  60. package/src/components/chat/chat-shell.tsx +92 -401
  61. package/src/components/instance/__tests__/instance-section.test.tsx +125 -0
  62. package/src/components/instance/instance-section.tsx +382 -0
  63. package/src/components/instance/upgrade-badge.tsx +219 -0
  64. package/src/components/notifications/__tests__/batch-proposal-review.test.tsx +95 -0
  65. package/src/components/notifications/__tests__/notification-item.test.tsx +106 -0
  66. package/src/components/notifications/batch-proposal-review.tsx +20 -5
  67. package/src/components/notifications/inbox-list.tsx +11 -2
  68. package/src/components/notifications/notification-item.tsx +56 -2
  69. package/src/components/notifications/pending-approval-host.tsx +56 -37
  70. package/src/components/schedules/schedule-create-sheet.tsx +19 -1
  71. package/src/components/schedules/schedule-edit-sheet.tsx +20 -1
  72. package/src/components/schedules/schedule-form.tsx +31 -0
  73. package/src/components/settings/__tests__/providers-runtimes-section.test.tsx +149 -0
  74. package/src/components/settings/auth-method-selector.tsx +19 -4
  75. package/src/components/settings/auth-status-badge.tsx +28 -3
  76. package/src/components/settings/openai-chatgpt-auth-control.tsx +278 -0
  77. package/src/components/settings/openai-runtime-section.tsx +7 -1
  78. package/src/components/settings/providers-runtimes-section.tsx +138 -19
  79. package/src/components/shared/app-sidebar.tsx +4 -3
  80. package/src/components/shared/command-palette.tsx +4 -5
  81. package/src/components/shared/theme-toggle.tsx +5 -24
  82. package/src/components/shared/workspace-indicator.tsx +61 -2
  83. package/src/components/tables/__tests__/table-enrichment-sheet.test.tsx +130 -0
  84. package/src/components/tables/table-create-sheet.tsx +4 -0
  85. package/src/components/tables/table-enrichment-runs.tsx +103 -0
  86. package/src/components/tables/table-enrichment-sheet.tsx +538 -0
  87. package/src/components/tables/table-spreadsheet.tsx +29 -5
  88. package/src/components/tables/table-toolbar.tsx +10 -1
  89. package/src/components/tasks/kanban-board.tsx +1 -0
  90. package/src/components/tasks/kanban-column.tsx +53 -14
  91. package/src/components/tasks/task-bento-grid.tsx +19 -0
  92. package/src/components/tasks/task-card.tsx +26 -3
  93. package/src/components/tasks/task-chip-bar.tsx +24 -0
  94. package/src/components/tasks/task-result-renderer.tsx +1 -1
  95. package/src/components/workflows/delay-step-body.tsx +109 -0
  96. package/src/components/workflows/hooks/use-workflow-status.ts +50 -0
  97. package/src/components/workflows/loop-status-view.tsx +1 -1
  98. package/src/components/workflows/shared/step-result.tsx +78 -0
  99. package/src/components/workflows/shared/workflow-header.tsx +141 -0
  100. package/src/components/workflows/shared/workflow-loading-skeleton.tsx +36 -0
  101. package/src/components/workflows/swarm-dashboard.tsx +2 -15
  102. package/src/components/workflows/views/loop-pattern-view.tsx +137 -0
  103. package/src/components/workflows/views/sequence-pattern-view.tsx +511 -0
  104. package/src/components/workflows/workflow-form-view.tsx +133 -16
  105. package/src/components/workflows/workflow-status-view.tsx +30 -740
  106. package/src/instrumentation-node.ts +94 -0
  107. package/src/instrumentation.ts +4 -48
  108. package/src/lib/agents/__tests__/claude-agent.test.ts +199 -0
  109. package/src/lib/agents/__tests__/execution-manager.test.ts +1 -27
  110. package/src/lib/agents/__tests__/failure-reason.test.ts +68 -0
  111. package/src/lib/agents/__tests__/learned-context.test.ts +0 -11
  112. package/src/lib/agents/__tests__/learning-session.test.ts +158 -0
  113. package/src/lib/agents/__tests__/pattern-extractor.test.ts +48 -0
  114. package/src/lib/agents/claude-agent.ts +155 -18
  115. package/src/lib/agents/execution-manager.ts +0 -35
  116. package/src/lib/agents/learned-context.ts +0 -12
  117. package/src/lib/agents/learning-session.ts +18 -5
  118. package/src/lib/agents/profiles/__tests__/registry.test.ts +6 -4
  119. package/src/lib/agents/profiles/builtins/upgrade-assistant/SKILL.md +70 -0
  120. package/src/lib/agents/profiles/builtins/upgrade-assistant/profile.yaml +32 -0
  121. package/src/lib/agents/runtime/__tests__/openai-codex-auth.test.ts +118 -0
  122. package/src/lib/agents/runtime/codex-app-server-client.ts +11 -5
  123. package/src/lib/agents/runtime/openai-codex-auth.ts +389 -0
  124. package/src/lib/agents/runtime/openai-codex.ts +29 -60
  125. package/src/lib/agents/runtime/types.ts +8 -0
  126. package/src/lib/book/chapter-mapping.ts +11 -0
  127. package/src/lib/book/content.ts +10 -0
  128. package/src/lib/chat/__tests__/active-streams.test.ts +49 -0
  129. package/src/lib/chat/__tests__/finalize-safety-net.test.ts +139 -0
  130. package/src/lib/chat/__tests__/reconcile.test.ts +137 -0
  131. package/src/lib/chat/__tests__/stream-telemetry.test.ts +151 -0
  132. package/src/lib/chat/active-streams.ts +27 -0
  133. package/src/lib/chat/codex-engine.ts +16 -17
  134. package/src/lib/chat/context-builder.ts +5 -3
  135. package/src/lib/chat/engine.ts +50 -3
  136. package/src/lib/chat/reconcile.ts +117 -0
  137. package/src/lib/chat/stagent-tools.ts +1 -0
  138. package/src/lib/chat/stream-telemetry.ts +132 -0
  139. package/src/lib/chat/suggested-prompts.ts +28 -1
  140. package/src/lib/chat/system-prompt.ts +26 -1
  141. package/src/lib/chat/tool-catalog.ts +2 -1
  142. package/src/lib/chat/tools/__tests__/enrich-table-tool.test.ts +127 -0
  143. package/src/lib/chat/tools/__tests__/schedule-tools.test.ts +261 -0
  144. package/src/lib/chat/tools/__tests__/task-tools.test.ts +352 -0
  145. package/src/lib/chat/tools/__tests__/workflow-tools-dedup.test.ts +217 -0
  146. package/src/lib/chat/tools/document-tools.ts +29 -13
  147. package/src/lib/chat/tools/helpers.ts +39 -0
  148. package/src/lib/chat/tools/notification-tools.ts +9 -5
  149. package/src/lib/chat/tools/project-tools.ts +33 -0
  150. package/src/lib/chat/tools/schedule-tools.ts +44 -11
  151. package/src/lib/chat/tools/table-tools.ts +71 -0
  152. package/src/lib/chat/tools/task-tools.ts +84 -20
  153. package/src/lib/chat/tools/workflow-tools.ts +234 -32
  154. package/src/lib/constants/settings.ts +8 -18
  155. package/src/lib/data/__tests__/clear.test.ts +56 -2
  156. package/src/lib/data/clear.ts +20 -15
  157. package/src/lib/data/delete-project.ts +171 -0
  158. package/src/lib/db/__tests__/bootstrap.test.ts +1 -1
  159. package/src/lib/db/bootstrap.ts +45 -16
  160. package/src/lib/db/index.ts +5 -0
  161. package/src/lib/db/migrations/0009_add_app_instances.sql +25 -0
  162. package/src/lib/db/migrations/0024_add_workflow_resume_at.sql +10 -0
  163. package/src/lib/db/migrations/0025_drop_app_instances.sql +3 -0
  164. package/src/lib/db/migrations/0026_drop_license.sql +3 -0
  165. package/src/lib/db/migrations/meta/_journal.json +21 -0
  166. package/src/lib/db/schema.ts +68 -23
  167. package/src/lib/environment/workspace-context.ts +13 -1
  168. package/src/lib/import/dedup.ts +4 -54
  169. package/src/lib/instance/__tests__/bootstrap.test.ts +362 -0
  170. package/src/lib/instance/__tests__/detect.test.ts +115 -0
  171. package/src/lib/instance/__tests__/fingerprint.test.ts +48 -0
  172. package/src/lib/instance/__tests__/git-ops.test.ts +95 -0
  173. package/src/lib/instance/__tests__/settings.test.ts +83 -0
  174. package/src/lib/instance/__tests__/upgrade-poller.test.ts +131 -0
  175. package/src/lib/instance/bootstrap.ts +270 -0
  176. package/src/lib/instance/detect.ts +49 -0
  177. package/src/lib/instance/fingerprint.ts +78 -0
  178. package/src/lib/instance/git-ops.ts +95 -0
  179. package/src/lib/instance/settings.ts +61 -0
  180. package/src/lib/instance/types.ts +77 -0
  181. package/src/lib/instance/upgrade-poller.ts +153 -0
  182. package/src/lib/notifications/__tests__/visibility.test.ts +51 -0
  183. package/src/lib/notifications/visibility.ts +33 -0
  184. package/src/lib/schedules/__tests__/collision-check.test.ts +93 -0
  185. package/src/lib/schedules/__tests__/config.test.ts +62 -0
  186. package/src/lib/schedules/__tests__/firing-metrics.test.ts +99 -0
  187. package/src/lib/schedules/__tests__/integration.test.ts +82 -0
  188. package/src/lib/schedules/__tests__/slot-claim.test.ts +242 -0
  189. package/src/lib/schedules/__tests__/tick-scheduler.test.ts +102 -0
  190. package/src/lib/schedules/__tests__/turn-budget.test.ts +228 -0
  191. package/src/lib/schedules/collision-check.ts +105 -0
  192. package/src/lib/schedules/config.ts +53 -0
  193. package/src/lib/schedules/scheduler.ts +232 -13
  194. package/src/lib/schedules/slot-claim.ts +105 -0
  195. package/src/lib/settings/__tests__/openai-auth.test.ts +101 -0
  196. package/src/lib/settings/__tests__/openai-login-manager.test.ts +64 -0
  197. package/src/lib/settings/__tests__/runtime-setup.test.ts +33 -0
  198. package/src/lib/settings/openai-auth.ts +105 -10
  199. package/src/lib/settings/openai-login-manager.ts +260 -0
  200. package/src/lib/settings/runtime-setup.ts +14 -4
  201. package/src/lib/tables/__tests__/enrichment-planner.test.ts +124 -0
  202. package/src/lib/tables/__tests__/enrichment.test.ts +147 -0
  203. package/src/lib/tables/enrichment-planner.ts +454 -0
  204. package/src/lib/tables/enrichment.ts +328 -0
  205. package/src/lib/tables/query-builder.ts +5 -2
  206. package/src/lib/tables/trigger-evaluator.ts +3 -2
  207. package/src/lib/theme.ts +71 -0
  208. package/src/lib/usage/ledger.ts +2 -18
  209. package/src/lib/util/__tests__/similarity.test.ts +106 -0
  210. package/src/lib/util/similarity.ts +77 -0
  211. package/src/lib/utils/format-timestamp.ts +24 -0
  212. package/src/lib/utils/stagent-paths.ts +12 -0
  213. package/src/lib/validators/__tests__/blueprint.test.ts +172 -0
  214. package/src/lib/validators/__tests__/settings.test.ts +10 -0
  215. package/src/lib/validators/blueprint.ts +70 -9
  216. package/src/lib/validators/profile.ts +2 -2
  217. package/src/lib/validators/settings.ts +3 -1
  218. package/src/lib/workflows/__tests__/delay.test.ts +196 -0
  219. package/src/lib/workflows/__tests__/engine.test.ts +8 -0
  220. package/src/lib/workflows/__tests__/loop-executor.test.ts +54 -0
  221. package/src/lib/workflows/__tests__/post-action.test.ts +108 -0
  222. package/src/lib/workflows/blueprints/instantiator.ts +22 -1
  223. package/src/lib/workflows/blueprints/types.ts +10 -2
  224. package/src/lib/workflows/delay.ts +106 -0
  225. package/src/lib/workflows/engine.ts +207 -4
  226. package/src/lib/workflows/loop-executor.ts +349 -24
  227. package/src/lib/workflows/post-action.ts +91 -0
  228. package/src/lib/workflows/types.ts +166 -1
  229. package/src/app/api/license/checkout/route.ts +0 -28
  230. package/src/app/api/license/portal/route.ts +0 -26
  231. package/src/app/api/license/route.ts +0 -89
  232. package/src/app/api/license/usage/route.ts +0 -63
  233. package/src/app/api/marketplace/browse/route.ts +0 -15
  234. package/src/app/api/marketplace/import/route.ts +0 -28
  235. package/src/app/api/marketplace/publish/route.ts +0 -40
  236. package/src/app/api/onboarding/email/route.ts +0 -53
  237. package/src/app/api/settings/telemetry/route.ts +0 -14
  238. package/src/app/api/sync/export/route.ts +0 -54
  239. package/src/app/api/sync/restore/route.ts +0 -37
  240. package/src/app/api/sync/sessions/route.ts +0 -24
  241. package/src/app/auth/callback/route.ts +0 -73
  242. package/src/app/marketplace/page.tsx +0 -19
  243. package/src/components/analytics/analytics-gate-card.tsx +0 -101
  244. package/src/components/marketplace/blueprint-card.tsx +0 -61
  245. package/src/components/marketplace/marketplace-browser.tsx +0 -131
  246. package/src/components/onboarding/email-capture-card.tsx +0 -104
  247. package/src/components/settings/activation-form.tsx +0 -95
  248. package/src/components/settings/cloud-account-section.tsx +0 -147
  249. package/src/components/settings/cloud-sync-section.tsx +0 -155
  250. package/src/components/settings/subscription-section.tsx +0 -410
  251. package/src/components/settings/telemetry-section.tsx +0 -80
  252. package/src/components/shared/premium-gate-overlay.tsx +0 -50
  253. package/src/components/shared/schedule-gate-dialog.tsx +0 -64
  254. package/src/components/shared/upgrade-banner.tsx +0 -112
  255. package/src/hooks/use-supabase-auth.ts +0 -79
  256. package/src/lib/billing/email.ts +0 -54
  257. package/src/lib/billing/products.ts +0 -80
  258. package/src/lib/billing/stripe.ts +0 -101
  259. package/src/lib/cloud/supabase-browser.ts +0 -32
  260. package/src/lib/cloud/supabase-client.ts +0 -56
  261. package/src/lib/license/__tests__/features.test.ts +0 -56
  262. package/src/lib/license/__tests__/key-format.test.ts +0 -88
  263. package/src/lib/license/__tests__/manager.test.ts +0 -64
  264. package/src/lib/license/__tests__/tier-limits.test.ts +0 -79
  265. package/src/lib/license/cloud-validation.ts +0 -60
  266. package/src/lib/license/features.ts +0 -44
  267. package/src/lib/license/key-format.ts +0 -101
  268. package/src/lib/license/limit-check.ts +0 -111
  269. package/src/lib/license/limit-queries.ts +0 -51
  270. package/src/lib/license/manager.ts +0 -345
  271. package/src/lib/license/notifications.ts +0 -59
  272. package/src/lib/license/tier-limits.ts +0 -71
  273. package/src/lib/marketplace/marketplace-client.ts +0 -107
  274. package/src/lib/sync/cloud-sync.ts +0 -235
  275. package/src/lib/telemetry/conversion-events.ts +0 -71
  276. package/src/lib/telemetry/queue.ts +0 -122
  277. package/src/lib/validators/license.ts +0 -33
@@ -0,0 +1,118 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+
3
+ import {
4
+ extractPlanTypeFromIdToken,
5
+ readCodexAuthStateFromClient,
6
+ } from "@/lib/agents/runtime/openai-codex-auth";
7
+
8
+ describe("openai codex auth", () => {
9
+ it("keeps a ChatGPT session connected when rate limit decoding fails", async () => {
10
+ const client = {
11
+ request: vi.fn(async (method: string) => {
12
+ if (method === "account/read") {
13
+ return {
14
+ account: {
15
+ type: "chatgpt",
16
+ email: "sehgal.manav@gmail.com",
17
+ planType: "prolite",
18
+ },
19
+ requiresOpenaiAuth: false,
20
+ };
21
+ }
22
+
23
+ if (method === "account/rateLimits/read") {
24
+ throw new Error("unknown variant `prolite`");
25
+ }
26
+
27
+ throw new Error(`Unexpected method: ${method}`);
28
+ }),
29
+ };
30
+
31
+ const state = await readCodexAuthStateFromClient(client as never, {
32
+ refreshToken: true,
33
+ });
34
+
35
+ expect(state.connected).toBe(true);
36
+ expect(state.account).toEqual({
37
+ type: "chatgpt",
38
+ email: "sehgal.manav@gmail.com",
39
+ planType: "prolite",
40
+ });
41
+ expect(state.rateLimits).toBeNull();
42
+ expect(state.authMode).toBe("chatgpt");
43
+ });
44
+
45
+ it("recovers the plan type from the rate limit error payload when account/read omits it", async () => {
46
+ const client = {
47
+ request: vi.fn(async (method: string) => {
48
+ if (method === "account/read") {
49
+ return {
50
+ account: {
51
+ type: "chatgpt",
52
+ email: "sehgal.manav@gmail.com",
53
+ planType: null,
54
+ },
55
+ requiresOpenaiAuth: false,
56
+ };
57
+ }
58
+
59
+ if (method === "account/rateLimits/read") {
60
+ throw new Error('Decode error: body={ "plan_type": "prolite" }');
61
+ }
62
+
63
+ throw new Error(`Unexpected method: ${method}`);
64
+ }),
65
+ };
66
+
67
+ const state = await readCodexAuthStateFromClient(client as never);
68
+
69
+ expect(state.connected).toBe(true);
70
+ expect(state.account?.planType).toBe("prolite");
71
+ expect(state.rateLimits).toBeNull();
72
+ });
73
+
74
+ it("treats account/read planType=unknown as missing and recovers the upstream plan", async () => {
75
+ const client = {
76
+ request: vi.fn(async (method: string) => {
77
+ if (method === "account/read") {
78
+ return {
79
+ account: {
80
+ type: "chatgpt",
81
+ email: "sehgal.manav@gmail.com",
82
+ planType: "unknown",
83
+ },
84
+ requiresOpenaiAuth: false,
85
+ };
86
+ }
87
+
88
+ if (method === "account/rateLimits/read") {
89
+ throw new Error('Decode error: body={ "plan_type": "prolite" }');
90
+ }
91
+
92
+ throw new Error(`Unexpected method: ${method}`);
93
+ }),
94
+ };
95
+
96
+ const state = await readCodexAuthStateFromClient(client as never);
97
+
98
+ expect(state.connected).toBe(true);
99
+ expect(state.account?.planType).toBe("prolite");
100
+ expect(state.rateLimits).toBeNull();
101
+ });
102
+
103
+ it("extracts the plan type from the stored id token payload", async () => {
104
+ const payload = Buffer.from(
105
+ JSON.stringify({
106
+ "https://api.openai.com/auth": {
107
+ chatgpt_plan_type: "prolite",
108
+ },
109
+ })
110
+ )
111
+ .toString("base64")
112
+ .replace(/\+/g, "-")
113
+ .replace(/\//g, "_")
114
+ .replace(/=+$/g, "");
115
+
116
+ expect(extractPlanTypeFromIdToken(`header.${payload}.signature`)).toBe("prolite");
117
+ });
118
+ });
@@ -24,7 +24,7 @@ interface JsonRpcNotification {
24
24
  type JsonRpcMessage = JsonRpcRequest | JsonRpcResponse | JsonRpcNotification;
25
25
 
26
26
  export interface CodexAppServerClientOptions {
27
- env?: Record<string, string>;
27
+ env?: Record<string, string | undefined>;
28
28
  cwd?: string;
29
29
  }
30
30
 
@@ -59,15 +59,21 @@ export class CodexAppServerClient {
59
59
  ): Promise<CodexAppServerClient> {
60
60
  const port = await reservePort();
61
61
  const listenUrl = `ws://127.0.0.1:${port}`;
62
+ const env: NodeJS.ProcessEnv = { ...process.env };
63
+ for (const [key, value] of Object.entries(options.env ?? {})) {
64
+ if (value === undefined) {
65
+ delete env[key];
66
+ } else {
67
+ env[key] = value;
68
+ }
69
+ }
70
+
62
71
  const child = spawn(
63
72
  "codex",
64
73
  ["app-server", "--listen", listenUrl],
65
74
  {
66
75
  cwd: options.cwd,
67
- env: {
68
- ...process.env,
69
- ...(options.env ?? {}),
70
- },
76
+ env,
71
77
  stdio: ["ignore", "pipe", "pipe"],
72
78
  }
73
79
  );
@@ -0,0 +1,389 @@
1
+ import { mkdir, readFile, rm, writeFile } from "fs/promises";
2
+ import { dirname } from "path";
3
+ import { getLaunchCwd } from "@/lib/environment/workspace-context";
4
+ import {
5
+ getOpenAIApiKey,
6
+ getOpenAIAuthSettings,
7
+ updateOpenAIAuthStatus,
8
+ clearOpenAIOAuthStatus,
9
+ updateOpenAIOAuthStatus,
10
+ type OpenAIAccountInfo,
11
+ type OpenAIAuthMode,
12
+ type OpenAIRateLimitInfo,
13
+ type OpenAIRateLimitWindow,
14
+ } from "@/lib/settings/openai-auth";
15
+ import {
16
+ getStagentCodexAuthPath,
17
+ getStagentCodexConfigPath,
18
+ getStagentCodexDir,
19
+ } from "@/lib/utils/stagent-paths";
20
+ import { CodexAppServerClient } from "./codex-app-server-client";
21
+
22
+ const STAGENT_CODEX_CONFIG = `cli_auth_credentials_store = "file"
23
+ `;
24
+
25
+ interface AccountReadResult {
26
+ account?: {
27
+ type?: string;
28
+ email?: string | null;
29
+ planType?: string | null;
30
+ } | null;
31
+ requiresOpenaiAuth?: boolean;
32
+ }
33
+
34
+ interface RateLimitsReadResult {
35
+ rateLimits?: {
36
+ limitId?: string | null;
37
+ limitName?: string | null;
38
+ primary?: RateLimitWindowLike | null;
39
+ secondary?: RateLimitWindowLike | null;
40
+ } | null;
41
+ }
42
+
43
+ interface RateLimitWindowLike {
44
+ usedPercent?: unknown;
45
+ windowDurationMins?: unknown;
46
+ resetsAt?: unknown;
47
+ }
48
+
49
+ function parseRateLimitWindow(
50
+ value: RateLimitWindowLike | null | undefined
51
+ ): OpenAIRateLimitWindow | null {
52
+ if (!value || typeof value !== "object") return null;
53
+ return {
54
+ usedPercent:
55
+ typeof value.usedPercent === "number" ? value.usedPercent : null,
56
+ windowDurationMins:
57
+ typeof value.windowDurationMins === "number" ? value.windowDurationMins : null,
58
+ resetsAt: typeof value.resetsAt === "number" ? value.resetsAt : null,
59
+ };
60
+ }
61
+
62
+ function parseAccountInfo(
63
+ value: AccountReadResult["account"]
64
+ ): OpenAIAccountInfo | null {
65
+ if (!value?.type) return null;
66
+ if (
67
+ value.type !== "apiKey" &&
68
+ value.type !== "chatgpt" &&
69
+ value.type !== "chatgptAuthTokens"
70
+ ) {
71
+ return null;
72
+ }
73
+
74
+ return {
75
+ type: value.type,
76
+ email: value.email ?? null,
77
+ planType:
78
+ value.planType && value.planType.toLowerCase() !== "unknown"
79
+ ? value.planType
80
+ : null,
81
+ };
82
+ }
83
+
84
+ function extractPlanTypeFromError(error: unknown): string | null {
85
+ const message = error instanceof Error ? error.message : String(error);
86
+ const match = message.match(/"plan_type"\s*:\s*"([^"]+)"/);
87
+ return match?.[1] ?? null;
88
+ }
89
+
90
+ function decodeBase64Url(value: string): string | null {
91
+ try {
92
+ const normalized = value.replace(/-/g, "+").replace(/_/g, "/");
93
+ const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "=");
94
+ return Buffer.from(padded, "base64").toString("utf8");
95
+ } catch {
96
+ return null;
97
+ }
98
+ }
99
+
100
+ export function extractPlanTypeFromIdToken(idToken: string): string | null {
101
+ const [, payload] = idToken.split(".");
102
+ if (!payload) return null;
103
+
104
+ const decoded = decodeBase64Url(payload);
105
+ if (!decoded) return null;
106
+
107
+ try {
108
+ const parsed = JSON.parse(decoded) as {
109
+ "https://api.openai.com/auth"?: {
110
+ chatgpt_plan_type?: string | null;
111
+ };
112
+ };
113
+ return parsed["https://api.openai.com/auth"]?.chatgpt_plan_type ?? null;
114
+ } catch {
115
+ return null;
116
+ }
117
+ }
118
+
119
+ async function readStagentCodexPlanTypeFromAuthFile(): Promise<string | null> {
120
+ try {
121
+ const raw = await readFile(getStagentCodexAuthPath(), "utf8");
122
+ const parsed = JSON.parse(raw) as {
123
+ tokens?: {
124
+ id_token?: string | null;
125
+ } | null;
126
+ };
127
+ const idToken = parsed.tokens?.id_token;
128
+ return idToken ? extractPlanTypeFromIdToken(idToken) : null;
129
+ } catch {
130
+ return null;
131
+ }
132
+ }
133
+
134
+ async function ensureCodexHomeConfig() {
135
+ const codexDir = getStagentCodexDir();
136
+ const configPath = getStagentCodexConfigPath();
137
+
138
+ await mkdir(codexDir, { recursive: true });
139
+ await mkdir(dirname(configPath), { recursive: true });
140
+
141
+ let current = "";
142
+ try {
143
+ current = await readFile(configPath, "utf8");
144
+ } catch {
145
+ // File will be created below.
146
+ }
147
+
148
+ if (current.includes('cli_auth_credentials_store = "file"')) {
149
+ return;
150
+ }
151
+
152
+ const next = current.trim().length > 0
153
+ ? `${current.trimEnd()}\n\n${STAGENT_CODEX_CONFIG}`
154
+ : STAGENT_CODEX_CONFIG;
155
+ await writeFile(configPath, next, "utf8");
156
+ }
157
+
158
+ export async function buildCodexAuthEnv(
159
+ env?: Record<string, string | undefined>
160
+ ): Promise<Record<string, string | undefined>> {
161
+ await ensureCodexHomeConfig();
162
+
163
+ return {
164
+ ...env,
165
+ CODEX_HOME: getStagentCodexDir(),
166
+ OPENAI_API_KEY: env?.OPENAI_API_KEY,
167
+ };
168
+ }
169
+
170
+ export async function connectStagentCodexClient(options: {
171
+ cwd?: string;
172
+ env?: Record<string, string | undefined>;
173
+ } = {}) {
174
+ const env = await buildCodexAuthEnv(options.env);
175
+ return CodexAppServerClient.connect({
176
+ cwd: options.cwd ?? getLaunchCwd(),
177
+ env,
178
+ });
179
+ }
180
+
181
+ export async function initializeCodexClient(client: CodexAppServerClient) {
182
+ await client.request("initialize", {
183
+ clientInfo: {
184
+ name: "Stagent",
185
+ version: "0.1.1",
186
+ },
187
+ capabilities: null,
188
+ });
189
+ }
190
+
191
+ export async function readCodexAuthStateFromClient(
192
+ client: CodexAppServerClient,
193
+ options: { refreshToken?: boolean } = {}
194
+ ) {
195
+ const accountResult = (await client.request("account/read", {
196
+ refreshToken: options.refreshToken ?? false,
197
+ })) as AccountReadResult;
198
+
199
+ const account = parseAccountInfo(accountResult.account ?? null);
200
+ if (account?.type === "chatgpt" && !account.planType) {
201
+ account.planType = await readStagentCodexPlanTypeFromAuthFile();
202
+ }
203
+ let rateLimits: OpenAIRateLimitInfo | null = null;
204
+ if (account?.type === "chatgpt") {
205
+ try {
206
+ const rateLimitResult = (await client.request(
207
+ "account/rateLimits/read"
208
+ )) as RateLimitsReadResult;
209
+ const payload = rateLimitResult.rateLimits ?? null;
210
+ if (payload) {
211
+ rateLimits = {
212
+ limitId: payload.limitId ?? null,
213
+ limitName: payload.limitName ?? null,
214
+ primary: parseRateLimitWindow(payload.primary),
215
+ secondary: parseRateLimitWindow(payload.secondary),
216
+ };
217
+ }
218
+ } catch (error) {
219
+ if (!account.planType) {
220
+ account.planType = extractPlanTypeFromError(error);
221
+ }
222
+ rateLimits = null;
223
+ }
224
+ }
225
+
226
+ const authMode: OpenAIAuthMode =
227
+ account?.type === "apiKey"
228
+ ? "apikey"
229
+ : account?.type === "chatgpt"
230
+ ? "chatgpt"
231
+ : account?.type === "chatgptAuthTokens"
232
+ ? "chatgptAuthTokens"
233
+ : null;
234
+
235
+ return {
236
+ connected: account?.type === "chatgpt",
237
+ account,
238
+ rateLimits,
239
+ requiresOpenaiAuth: Boolean(accountResult.requiresOpenaiAuth),
240
+ authMode,
241
+ };
242
+ }
243
+
244
+ export type ResolvedOpenAICodexAuthContext =
245
+ | {
246
+ method: "api_key";
247
+ apiKeySource: "db" | "env" | "unknown";
248
+ connect: (cwd?: string) => Promise<CodexAppServerClient>;
249
+ }
250
+ | {
251
+ method: "oauth";
252
+ apiKeySource: "oauth";
253
+ connect: (cwd?: string) => Promise<CodexAppServerClient>;
254
+ };
255
+
256
+ export async function resolveOpenAICodexAuthContext(): Promise<ResolvedOpenAICodexAuthContext> {
257
+ const settings = await getOpenAIAuthSettings();
258
+
259
+ if (settings.method === "oauth") {
260
+ if (!settings.oauthConnected) {
261
+ try {
262
+ const state = await readStagentCodexAuthState({ refreshToken: false });
263
+ if (!state.connected) {
264
+ throw new Error("OpenAI ChatGPT sign-in is not configured.");
265
+ }
266
+ } catch {
267
+ throw new Error(
268
+ "OpenAI ChatGPT sign-in is not configured. Sign in from Settings > Providers & Runtimes."
269
+ );
270
+ }
271
+ }
272
+
273
+ return {
274
+ method: "oauth",
275
+ apiKeySource: "oauth",
276
+ connect: (cwd?: string) =>
277
+ connectStagentCodexClient({
278
+ cwd,
279
+ env: { OPENAI_API_KEY: undefined },
280
+ }),
281
+ };
282
+ }
283
+
284
+ const { apiKey, source } = await getOpenAIApiKey();
285
+ if (!apiKey) {
286
+ throw new Error("OpenAI API key is not configured");
287
+ }
288
+
289
+ return {
290
+ method: "api_key",
291
+ apiKeySource: source,
292
+ connect: (cwd?: string) =>
293
+ connectStagentCodexClient({
294
+ cwd,
295
+ env: { OPENAI_API_KEY: apiKey },
296
+ }),
297
+ };
298
+ }
299
+
300
+ export async function ensureOpenAICodexClientAuthenticated(
301
+ client: CodexAppServerClient,
302
+ auth: ResolvedOpenAICodexAuthContext
303
+ ) {
304
+ await initializeCodexClient(client);
305
+
306
+ if (auth.method === "api_key") {
307
+ const { apiKey } = await getOpenAIApiKey();
308
+ if (!apiKey) {
309
+ throw new Error("OpenAI API key is not configured");
310
+ }
311
+ await client.request("account/login/start", {
312
+ type: "apiKey",
313
+ apiKey,
314
+ });
315
+ await updateOpenAIAuthStatus(auth.apiKeySource);
316
+ return;
317
+ }
318
+
319
+ const state = await readCodexAuthStateFromClient(client, {
320
+ refreshToken: true,
321
+ });
322
+ if (!state.connected || state.account?.type !== "chatgpt") {
323
+ throw new Error(
324
+ "OpenAI ChatGPT sign-in is not configured. Sign in from Settings > Providers & Runtimes."
325
+ );
326
+ }
327
+ await updateOpenAIOAuthStatus({
328
+ connected: true,
329
+ account: state.account,
330
+ authMode: state.authMode,
331
+ rateLimits: state.rateLimits,
332
+ });
333
+ }
334
+
335
+ export async function readStagentCodexAuthState(options: {
336
+ refreshToken?: boolean;
337
+ cwd?: string;
338
+ } = {}) {
339
+ let client: CodexAppServerClient | null = null;
340
+
341
+ try {
342
+ client = await connectStagentCodexClient({ cwd: options.cwd });
343
+ await initializeCodexClient(client);
344
+
345
+ const state = await readCodexAuthStateFromClient(client, {
346
+ refreshToken: options.refreshToken,
347
+ });
348
+
349
+ await updateOpenAIOAuthStatus({
350
+ connected: state.connected,
351
+ account: state.account,
352
+ authMode: state.authMode,
353
+ rateLimits: state.rateLimits,
354
+ });
355
+
356
+ return state;
357
+ } catch (error) {
358
+ await clearOpenAIOAuthStatus();
359
+ throw error;
360
+ } finally {
361
+ if (client) {
362
+ await client.close();
363
+ }
364
+ }
365
+ }
366
+
367
+ export async function logoutStagentCodexAuth() {
368
+ let client: CodexAppServerClient | null = null;
369
+
370
+ try {
371
+ client = await connectStagentCodexClient();
372
+ await initializeCodexClient(client);
373
+ await client.request("account/logout");
374
+ } catch {
375
+ // Even if app-server logout fails, clear the isolated credential cache below.
376
+ } finally {
377
+ if (client) {
378
+ await client.close();
379
+ }
380
+ }
381
+
382
+ try {
383
+ await rm(getStagentCodexAuthPath(), { force: true });
384
+ } catch {
385
+ // Ignore cleanup failures.
386
+ }
387
+
388
+ await clearOpenAIOAuthStatus();
389
+ }
@@ -14,14 +14,15 @@ import {
14
14
  prepareTaskOutputDirectory,
15
15
  scanTaskOutputDocuments,
16
16
  } from "@/lib/documents/output-scanner";
17
- import {
18
- getOpenAIApiKey,
19
- updateOpenAIAuthStatus,
20
- } from "@/lib/settings/openai-auth";
21
17
  import { isToolAllowed } from "@/lib/settings/permissions";
22
18
  import { getRuntimeCatalogEntry } from "./catalog";
23
19
  import { getLaunchCwd } from "@/lib/environment/workspace-context";
24
20
  import { CodexAppServerClient } from "./codex-app-server-client";
21
+ import {
22
+ ensureOpenAICodexClientAuthenticated,
23
+ readCodexAuthStateFromClient,
24
+ resolveOpenAICodexAuthContext,
25
+ } from "./openai-codex-auth";
25
26
  import type {
26
27
  AgentRuntimeAdapter,
27
28
  RuntimeConnectionResult,
@@ -585,43 +586,19 @@ async function handleServerRequest(
585
586
  }
586
587
  }
587
588
 
588
- async function initializeOpenAIClient(
589
- client: CodexAppServerClient,
590
- apiKey: string
591
- ) {
592
- await client.request("initialize", {
593
- clientInfo: {
594
- name: "Stagent",
595
- version: "0.1.1",
596
- },
597
- capabilities: null,
598
- });
599
-
600
- await client.request("account/login/start", {
601
- type: "apiKey",
602
- apiKey,
603
- });
604
- }
605
-
606
589
  async function runAssistTurn({
607
590
  prompt,
608
591
  developerInstructions,
609
592
  cwd,
610
593
  }: AssistTurnOptions): Promise<{ text: string; usage: UsageSnapshot }> {
611
- const { apiKey, source } = await getOpenAIApiKey();
612
- if (!apiKey) {
613
- throw new Error("OpenAI API key is not configured");
614
- }
594
+ const auth = await resolveOpenAICodexAuthContext();
615
595
 
616
596
  let client: CodexAppServerClient | null = null;
617
597
  let text = "";
618
598
  let usage: UsageSnapshot = {};
619
599
 
620
600
  try {
621
- client = await CodexAppServerClient.connect({
622
- cwd,
623
- env: { OPENAI_API_KEY: apiKey },
624
- });
601
+ client = await auth.connect(cwd);
625
602
 
626
603
  client.onNotification = (notification: JsonRpcLikeNotification) => {
627
604
  if (notification.method !== "item/agentMessage/delta") return;
@@ -631,8 +608,7 @@ async function runAssistTurn({
631
608
  }
632
609
  };
633
610
 
634
- await initializeOpenAIClient(client, apiKey);
635
- await updateOpenAIAuthStatus(source);
611
+ await ensureOpenAICodexClientAuthenticated(client, auth);
636
612
 
637
613
  const threadResponse = (await client.request("thread/start", {
638
614
  cwd,
@@ -706,11 +682,7 @@ async function executeOpenAICodexTask(
706
682
  ): Promise<void> {
707
683
  const { task, profileId, instructions, prompt, cwd } =
708
684
  await resolveTaskExecutionContext(taskId, options);
709
- const { apiKey, source } = await getOpenAIApiKey();
710
-
711
- if (!apiKey) {
712
- throw new Error("OpenAI API key is not configured");
713
- }
685
+ const auth = await resolveOpenAICodexAuthContext();
714
686
 
715
687
  const abortController = new AbortController();
716
688
  let client: CodexAppServerClient | null = null;
@@ -729,10 +701,7 @@ async function executeOpenAICodexTask(
729
701
  };
730
702
 
731
703
  try {
732
- client = await CodexAppServerClient.connect({
733
- cwd,
734
- env: { OPENAI_API_KEY: apiKey },
735
- });
704
+ client = await auth.connect(cwd);
736
705
 
737
706
  client.onProcessError = (error) => {
738
707
  if (settled) return;
@@ -879,8 +848,7 @@ async function executeOpenAICodexTask(
879
848
  };
880
849
  });
881
850
 
882
- await initializeOpenAIClient(client, apiKey);
883
- await updateOpenAIAuthStatus(source);
851
+ await ensureOpenAICodexClientAuthenticated(client, auth);
884
852
 
885
853
  if (threadId) {
886
854
  await client.request("thread/resume", {
@@ -1051,29 +1019,30 @@ async function runOpenAITaskAssist(
1051
1019
  }
1052
1020
 
1053
1021
  async function testOpenAIConnection(): Promise<RuntimeConnectionResult> {
1054
- const { apiKey, source } = await getOpenAIApiKey();
1055
- if (!apiKey) {
1056
- return {
1057
- connected: false,
1058
- apiKeySource: "unknown",
1059
- error: "OpenAI API key is not configured",
1060
- };
1061
- }
1062
-
1063
1022
  let client: CodexAppServerClient | null = null;
1064
1023
  try {
1065
- client = await CodexAppServerClient.connect({
1066
- cwd: getLaunchCwd(),
1067
- env: { OPENAI_API_KEY: apiKey },
1024
+ const auth = await resolveOpenAICodexAuthContext();
1025
+ client = await auth.connect(getLaunchCwd());
1026
+ await ensureOpenAICodexClientAuthenticated(client, auth);
1027
+ const accountState = await readCodexAuthStateFromClient(client, {
1028
+ refreshToken: auth.apiKeySource === "oauth",
1068
1029
  });
1069
- await initializeOpenAIClient(client, apiKey);
1070
- await client.request("account/read", { refreshToken: false });
1071
- await updateOpenAIAuthStatus(source);
1072
- return { connected: true, apiKeySource: source };
1030
+
1031
+ return {
1032
+ connected: auth.apiKeySource === "oauth" ? accountState.connected : true,
1033
+ apiKeySource: auth.apiKeySource,
1034
+ account: accountState.account,
1035
+ rateLimits: accountState.rateLimits,
1036
+ authMode: accountState.authMode,
1037
+ };
1073
1038
  } catch (error) {
1074
1039
  return {
1075
1040
  connected: false,
1076
- apiKeySource: source,
1041
+ apiKeySource:
1042
+ error instanceof Error &&
1043
+ error.message.includes("ChatGPT sign-in is not configured")
1044
+ ? "oauth"
1045
+ : "unknown",
1077
1046
  error: error instanceof Error ? error.message : String(error),
1078
1047
  };
1079
1048
  } finally {