@zq-silk/yui 2.2.0 → 2.4.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 (206) hide show
  1. package/README.md +19 -3
  2. package/dist/artifacts/artifactCapability.js +39 -10
  3. package/dist/artifacts/imageMetadata.js +120 -0
  4. package/dist/artifacts/managedGit.js +7 -4
  5. package/dist/artifacts/taskArtifactRepository.js +6 -0
  6. package/dist/cli/commandCatalog.js +25 -11
  7. package/dist/cli/interactionPolicy.js +4 -0
  8. package/dist/cli/invocationAuthority.js +1 -1
  9. package/dist/commands/executionAuditCommands.js +5 -0
  10. package/dist/commands/globalRoleCommands.js +41 -3
  11. package/dist/commands/projectCommands.js +89 -94
  12. package/dist/commands/roleSkillValidation.js +4 -8
  13. package/dist/commands/taskCommands.js +30 -9
  14. package/dist/commands/taskIntegrationCommands.js +5 -16
  15. package/dist/commands/taskUsageCommand.js +38 -0
  16. package/dist/context/roleSessionContext.js +18 -2
  17. package/dist/context/runContextPack.js +5 -1
  18. package/dist/context/sessionBootstrapManifest.js +10 -1
  19. package/dist/context/skillCatalog.js +108 -0
  20. package/dist/context/skillPackage.js +161 -0
  21. package/dist/context/taskContext.js +3 -2
  22. package/dist/context/taskSearch.js +127 -0
  23. package/dist/controlPlaneCli.js +51 -39
  24. package/dist/controller/clientRuntime.js +3 -2
  25. package/dist/controller/fileSchedulerStoreAdapter.js +32 -6
  26. package/dist/controller/jobControl.js +2 -0
  27. package/dist/controller/nativeHostInput.js +37 -0
  28. package/dist/controller/runtime.js +93 -2
  29. package/dist/controller/runtimeEventInbox.js +2 -2
  30. package/dist/controller/runtimeEventProcessor.js +3 -3
  31. package/dist/doctor/doctor.js +10 -0
  32. package/dist/execution/reviewMainRun.js +1 -0
  33. package/dist/execution/workItemMainRun.js +1 -0
  34. package/dist/executor/codexConfigConflict.js +6 -4
  35. package/dist/executor/effectiveLaunch.js +24 -1
  36. package/dist/executor/fileRoleLaunchPlanner.js +13 -5
  37. package/dist/integration/gitIntegrationService.js +15 -79
  38. package/dist/integration/integrationSourceApplication.js +4 -2
  39. package/dist/job/httpPreview.js +61 -0
  40. package/dist/job/jobCleanupInspection.js +67 -0
  41. package/dist/job/jobRunner.js +12 -2
  42. package/dist/kernel/builtinCapabilities.js +3 -1
  43. package/dist/message/message.js +15 -1
  44. package/dist/output/rolePresentation.js +4 -1
  45. package/dist/repository/executionLaneGitSnapshot.js +2 -1
  46. package/dist/repository/gitCompatibility.js +62 -0
  47. package/dist/repository/gitWorkspace.js +86 -80
  48. package/dist/repository/gitWorktreeInventory.js +121 -0
  49. package/dist/repository/project.js +74 -25
  50. package/dist/repository/taskWorkspaceCoordinator.js +72 -145
  51. package/dist/repository/taskWorkspacePreparer.js +39 -43
  52. package/dist/repository/workspaceCleanupInspection.js +88 -7
  53. package/dist/resources/resourceDiscovery.js +2 -2
  54. package/dist/resources/resourceGc.js +7 -4
  55. package/dist/resources/resourceRegistrar.js +18 -11
  56. package/dist/runtime/agentEndpoint.js +8 -0
  57. package/dist/runtime/agentHost.js +170 -3
  58. package/dist/runtime/codexAppServerRuntime.js +1 -1
  59. package/dist/runtime/codexNativeAccess.js +175 -0
  60. package/dist/runtime/controlledCodexTui.js +116 -0
  61. package/dist/runtime/integrationRuntimeIsolation.js +21 -0
  62. package/dist/runtime/publicReply.js +23 -0
  63. package/dist/runtime/runtimeObservation.js +48 -0
  64. package/dist/runtime/structuredProviderHost.js +16 -7
  65. package/dist/runtime/taskCostMetrics.js +133 -0
  66. package/dist/runtime/taskRuntimeIsolation.js +91 -21
  67. package/dist/runtime/taskUsageMetrics.js +11 -2
  68. package/dist/runtime/taskUsageQuery.js +38 -0
  69. package/dist/scheduler/taskExecutionProjection.js +2 -0
  70. package/dist/scheduler/taskObservabilityProjection.js +9 -1
  71. package/dist/storage/homeLayout.js +13 -0
  72. package/dist/storage/sqliteSchema.js +44 -0
  73. package/dist/storage/sqliteStore.js +53 -0
  74. package/dist/storage/storageVersions.js +1 -1
  75. package/dist/storage/storeRpc.js +2 -0
  76. package/dist/storage/taskSearch.js +35 -0
  77. package/dist/storage/upgrade/projectKnowledgeMigration.js +52 -0
  78. package/dist/task/archiveDiagnostics.js +26 -8
  79. package/dist/task/archivePreflight.js +26 -9
  80. package/dist/task/leaderArchive.js +9 -15
  81. package/dist/task/ordinaryArchive.js +42 -0
  82. package/dist/tmux/tmuxManager.js +8 -8
  83. package/dist/web/assets/assetManifest.js +11 -51
  84. package/dist/web/assets/client/app/catalog.js +215 -0
  85. package/dist/web/assets/client/app/main.js +148 -0
  86. package/dist/web/assets/client/app/selection.js +139 -0
  87. package/dist/web/assets/client/app/settings.js +326 -0
  88. package/dist/web/assets/client/app/taskView.js +280 -0
  89. package/dist/web/assets/client/domain/context.js +96 -0
  90. package/dist/web/assets/client/domain/records.js +126 -0
  91. package/dist/web/assets/client/domain/runs.js +119 -0
  92. package/dist/web/assets/client/domain/taskForms.js +197 -0
  93. package/dist/web/assets/client/domain/vocab.js +69 -0
  94. package/dist/web/assets/client/domain/work.js +116 -0
  95. package/dist/web/assets/client/i18n/en.js +900 -0
  96. package/dist/web/assets/client/i18n/zh-CN.js +898 -0
  97. package/dist/web/assets/client/index.js +114 -0
  98. package/dist/web/assets/client/layout/resizer.js +37 -0
  99. package/dist/web/assets/client/layout/router.js +35 -0
  100. package/dist/web/assets/client/layout/shortcuts.js +45 -0
  101. package/dist/web/assets/client/layout/sizing.js +95 -0
  102. package/dist/web/assets/client/layout/workspace.js +247 -0
  103. package/dist/web/assets/client/{api.js → lib/api.js} +28 -4
  104. package/dist/web/assets/client/{dom.js → lib/dom.js} +24 -12
  105. package/dist/web/assets/client/{format.js → lib/format.js} +7 -0
  106. package/dist/web/assets/client/lib/i18n.js +54 -0
  107. package/dist/web/assets/client/lib/markdown.js +167 -0
  108. package/dist/web/assets/client/lib/prefs.js +28 -0
  109. package/dist/web/assets/client/lib/theme.js +68 -0
  110. package/dist/web/assets/client/ui/containers.js +101 -0
  111. package/dist/web/assets/client/ui/controls.js +51 -0
  112. package/dist/web/assets/client/ui/forms.js +48 -0
  113. package/dist/web/assets/client/ui/metrics.js +137 -0
  114. package/dist/web/assets/client/ui/primitives.js +92 -0
  115. package/dist/web/assets/client/ui/text.js +33 -0
  116. package/dist/web/assets/client/ui/toast.js +12 -0
  117. package/dist/web/assets/client/views/bodySearch.js +172 -0
  118. package/dist/web/assets/client/views/createTask.js +85 -0
  119. package/dist/web/assets/client/views/dock/conversation.js +612 -0
  120. package/dist/web/assets/client/views/dock/discussion.js +173 -0
  121. package/dist/web/assets/client/views/dock/preview.js +129 -0
  122. package/dist/web/assets/client/views/dock/terminal.js +199 -0
  123. package/dist/web/assets/client/views/globalInput.js +67 -0
  124. package/dist/web/assets/client/{overview.js → views/overview.js} +62 -56
  125. package/dist/web/assets/client/{sidebar.js → views/sidebar.js} +56 -62
  126. package/dist/web/assets/client/views/skills.js +152 -0
  127. package/dist/web/assets/client/views/task/actions.js +36 -0
  128. package/dist/web/assets/client/views/task/delivery.js +185 -0
  129. package/dist/web/assets/client/views/task/deliveryResults.js +159 -0
  130. package/dist/web/assets/client/views/task/files.js +272 -0
  131. package/dist/web/assets/client/views/task/observation.js +153 -0
  132. package/dist/web/assets/client/views/task/overview.js +161 -0
  133. package/dist/web/assets/client/views/task/page.js +131 -0
  134. package/dist/web/assets/client/views/task/records.js +163 -0
  135. package/dist/web/assets/client/views/task/runtime.js +130 -0
  136. package/dist/web/assets/client/views/task/timeline.js +190 -0
  137. package/dist/web/assets/shared/geometry.js +19 -0
  138. package/dist/web/assets/{icons.js → shared/icons.js} +4 -0
  139. package/dist/web/assets/shared/taskTabs.js +8 -0
  140. package/dist/web/assets/shared/themes.js +93 -0
  141. package/dist/web/assets/shell/dialogs.js +63 -0
  142. package/dist/web/assets/shell/dock.js +40 -0
  143. package/dist/web/assets/shell/index.js +40 -0
  144. package/dist/web/assets/shell/settings.js +34 -0
  145. package/dist/web/assets/shell/sidebar.js +45 -0
  146. package/dist/web/assets/styles/index.js +41 -0
  147. package/dist/web/assets/styles/{responsive.js → layout/responsive.js} +15 -13
  148. package/dist/web/assets/styles/{layout.js → layout/workspace.js} +17 -24
  149. package/dist/web/assets/styles/markdown.js +22 -3
  150. package/dist/web/assets/styles/tokens.js +10 -63
  151. package/dist/web/assets/styles/ui/controls.js +76 -0
  152. package/dist/web/assets/styles/ui/forms.js +46 -0
  153. package/dist/web/assets/styles/ui/surfaces.js +122 -0
  154. package/dist/web/assets/styles/views/delivery.js +29 -0
  155. package/dist/web/assets/styles/views/dock.js +71 -0
  156. package/dist/web/assets/styles/views/page.js +32 -0
  157. package/dist/web/assets/styles/views/records.js +70 -0
  158. package/dist/web/assets/styles/views/settings.js +26 -0
  159. package/dist/web/assets/styles/views/sidebar.js +74 -0
  160. package/dist/web/assets/styles/views/task.js +80 -0
  161. package/dist/web/assets/styles/views/terminal.js +38 -0
  162. package/dist/web/taskPreviews.js +205 -0
  163. package/dist/web/tmuxWebTerminal.js +16 -9
  164. package/dist/web/webConversation.js +373 -0
  165. package/dist/web/webMaterials.js +155 -0
  166. package/dist/web/webServer.js +306 -18
  167. package/dist/web/webSettings.js +331 -0
  168. package/dist/web/webSkills.js +86 -0
  169. package/dist/web/webSnapshot.js +40 -8
  170. package/dist/web/webTaskSurface.js +114 -9
  171. package/dist/workspace/gitChangeSetCapture.js +3 -1
  172. package/docs/managed-turn-and-session-runtime.md +9 -0
  173. package/docs/managed-turn-and-session-runtime.zh-CN.md +7 -0
  174. package/docs/observability/README.md +13 -3
  175. package/docs/observability/README.zh-CN.md +10 -3
  176. package/docs/release-workflow.md +53 -0
  177. package/docs/release-workflow.zh-CN.md +40 -0
  178. package/docs/storage-baseline.md +38 -1
  179. package/docs/storage-baseline.zh-CN.md +22 -0
  180. package/docs/task-delivery.md +36 -8
  181. package/docs/task-delivery.zh-CN.md +24 -7
  182. package/docs/task-discovery.md +28 -0
  183. package/docs/testing/verification-levels.md +2 -2
  184. package/i18n/README.zh-CN.md +4 -3
  185. package/package.json +1 -1
  186. package/skills/yui-leader/references/execution.md +2 -1
  187. package/skills/yui-leader/references/integration.md +20 -0
  188. package/skills/yui-operator/SKILL.md +4 -0
  189. package/skills/yui-reviewer/SKILL.md +3 -0
  190. package/skills/yui-runtime/SKILL.md +4 -0
  191. package/skills/yui-runtime/references/publication.md +20 -4
  192. package/skills/yui-worker/SKILL.md +3 -0
  193. package/dist/web/assets/client/app.js +0 -804
  194. package/dist/web/assets/client/components.js +0 -418
  195. package/dist/web/assets/client/detail.js +0 -333
  196. package/dist/web/assets/client/dock.js +0 -225
  197. package/dist/web/assets/client/evidence.js +0 -172
  198. package/dist/web/assets/client/forms.js +0 -230
  199. package/dist/web/assets/client/i18n.js +0 -1161
  200. package/dist/web/assets/client/markdown.js +0 -114
  201. package/dist/web/assets/client/records.js +0 -63
  202. package/dist/web/assets/client/sections.js +0 -313
  203. package/dist/web/assets/client/theme.js +0 -71
  204. package/dist/web/assets/shell.js +0 -161
  205. package/dist/web/assets/styles/components.js +0 -169
  206. package/dist/web/assets/styles/views.js +0 -292
@@ -18,6 +18,7 @@ import { closeAgentHostEndpoints } from "./agentHostCleanup.js";
18
18
  import { readAgentRunConfigurationObservation, unknownAgentRunConfiguration } from "./agentRunConfiguration.js";
19
19
  import { CodexPreSubmissionError } from "./codexAppServerRuntime.js";
20
20
  import { runCodexInteractiveHost } from "./codexInteractiveHost.js";
21
+ import { openControlledCodexTui } from "./controlledCodexTui.js";
21
22
  import { validateAgentHostLaunchPayload } from "./launchBroker.js";
22
23
  import { validateRuntimeProcessExitObservation } from "./processExitObservation.js";
23
24
  import { persistRuntimeProcessExitObservation, replayRuntimeProcessExitOutbox } from "./processExitOutbox.js";
@@ -108,6 +109,10 @@ export async function runAgentHost(input) {
108
109
  let snapshot = hostSnapshot("idle");
109
110
  let dispatchTail = Promise.resolve();
110
111
  let promptHuman = () => { };
112
+ let humanConsole;
113
+ let nativeTui;
114
+ let nativeOpening;
115
+ let modelChanging = false;
111
116
  const recentSteerAttempts = new Map();
112
117
  await replayExitOutbox(input.home);
113
118
  const updateSnapshot = (next) => {
@@ -444,6 +449,8 @@ export async function runAgentHost(input) {
444
449
  && providerSession.adapterId === "codex"
445
450
  && !hostStopRequested;
446
451
  if (reconnectableCodexClient) {
452
+ nativeTui?.close();
453
+ nativeTui = undefined;
447
454
  session = undefined;
448
455
  if (codexClientAttachedAt !== undefined
449
456
  && Date.now() - codexClientAttachedAt >= CODEX_CLIENT_STABLE_MS) {
@@ -1028,7 +1035,128 @@ export async function runAgentHost(input) {
1028
1035
  promptHuman();
1029
1036
  return snapshot;
1030
1037
  };
1031
- const control = await openAgentHostControl(input.home, payload, () => snapshot, async (request) => {
1038
+ const nativeMutation = async (expected, method, params) => {
1039
+ if (session !== expected || params.threadId !== expected.nativeSessionId || !authority
1040
+ || (sessionPayload?.environment.YUI_SESSION_SCOPE === "task" && sessionPayload.environment.YUI_ROLE !== "leader")) {
1041
+ throw new Error("Native input targets a replaced or unsupported Session.");
1042
+ }
1043
+ if (method === "turn/interrupt") {
1044
+ if (!activeRunAttemptId || params.turnId !== activeNativeTurnId)
1045
+ throw new Error("Native stop targets a different Turn.");
1046
+ const cancelled = await expected.cancel(activeRunAttemptId);
1047
+ if (cancelled.status !== "requested")
1048
+ throw new Error(`Native stop is ${cancelled.status}; do not replay.`);
1049
+ return {};
1050
+ }
1051
+ if (!Array.isArray(params.input) || params.input.length === 0
1052
+ || params.input.some(part => !part || typeof part !== "object"
1053
+ || part.type !== "text" || typeof part.text !== "string")) {
1054
+ throw new Error("Managed native access currently supports text input only.");
1055
+ }
1056
+ const boundedText = params.input.map(part => part.text).join("\n");
1057
+ if (!boundedText.trim() || boundedText.includes("\0") || Buffer.byteLength(boundedText) > 12000)
1058
+ throw new Error("Native input exceeds its text bound.");
1059
+ const run = { attemptId: `human:${authority.holderId}:${randomUUID()}`, boundedText };
1060
+ if (method === "turn/steer") {
1061
+ const turnId = activeNativeTurnId;
1062
+ if (params.expectedTurnId !== turnId || !turnId)
1063
+ throw new Error("Native steer targets a different Turn.");
1064
+ if (!sessionPayload)
1065
+ throw new Error("Native Session launch context is unavailable.");
1066
+ // Persist human intent and the existing steer receipt before Provider
1067
+ // write. A fresh human turn id is not an additional-input binding.
1068
+ const prepared = await callControllerIdempotently(input.home, "runtime.native-steer-prepare", {
1069
+ ...hostRunControlParams(sessionPayload, expected.nativeSessionId, authority, run.attemptId),
1070
+ nativeTurnId: turnId, boundedText
1071
+ });
1072
+ const result = await handleControl({ protocol: AGENT_HOST_CONTROL_PROTOCOL, type: "steer-turn",
1073
+ nativeSessionId: expected.nativeSessionId, nativeTurnId: turnId, authority, run: prepared });
1074
+ if (result.outcome !== "accepted")
1075
+ throw new Error("Native steer acceptance is unconfirmed; do not resubmit.");
1076
+ return { turnId };
1077
+ }
1078
+ const result = await handleControl({ protocol: AGENT_HOST_CONTROL_PROTOCOL, type: "submit-turn",
1079
+ nativeSessionId: expected.nativeSessionId, authority, run });
1080
+ const accepted = result.snapshot;
1081
+ if (result.outcome !== "accepted" || accepted.inputAcceptance !== "provider" || !accepted.nativeTurnId) {
1082
+ throw new Error("Native acceptance is unconfirmed; do not resubmit.");
1083
+ }
1084
+ return { turn: { id: accepted.nativeTurnId, items: [], status: "inProgress", error: null } };
1085
+ };
1086
+ const handleControl = async (request) => {
1087
+ if (request.type === "native-model") {
1088
+ return enqueueSerialized(async () => {
1089
+ const current = session;
1090
+ if (!current?.nativeAccess || current.nativeSessionId !== request.nativeSessionId
1091
+ || !sessionPayload || activeNativeTurnId || authority?.owner !== "controller"
1092
+ || (sessionPayload.environment.YUI_SESSION_SCOPE === "task" && sessionPayload.environment.YUI_ROLE !== "leader")
1093
+ || (sessionPayload.environment.YUI_SESSION_SCOPE === "global" && sessionPayload.environment.YUI_ROLE !== "operator")) {
1094
+ throw new Error("Model switching requires the idle current controlled Codex Session, without a native terminal writer.");
1095
+ }
1096
+ if (nativeTui || nativeOpening) {
1097
+ throw new Error("Exit the native TUI before changing the Session model. Closing a browser terminal viewer only detaches it; the Host-owned TUI remains open.");
1098
+ }
1099
+ modelChanging = true;
1100
+ try {
1101
+ await assertHostExecutionEnvironment(input.home, sessionPayload, current.nativeSessionId);
1102
+ try {
1103
+ await current.nativeAccess.setModel(request.model);
1104
+ }
1105
+ catch (error) {
1106
+ if (error instanceof CodexPreSubmissionError)
1107
+ throw error;
1108
+ throw new ProviderDeliveryUnknownError(`Native model adoption is unconfirmed: ${errorText(error)}`, `model:${current.nativeSessionId}`, { cause: error });
1109
+ }
1110
+ return controlResult("accepted", { ...snapshot, runConfiguration: current.runConfiguration });
1111
+ }
1112
+ finally {
1113
+ modelChanging = false;
1114
+ }
1115
+ });
1116
+ }
1117
+ if (request.type === "native-open" || request.type === "native-respond") {
1118
+ const current = session;
1119
+ if (!current?.nativeAccess || current.nativeSessionId !== request.nativeSessionId
1120
+ || !sessionPayload || (sessionPayload.environment.YUI_SESSION_SCOPE === "task"
1121
+ && sessionPayload.environment.YUI_ROLE !== "leader"))
1122
+ throw new Error("Native access requires the current controlled Codex Operator/Leader.");
1123
+ if (request.type === "native-respond") {
1124
+ await assertHostExecutionEnvironment(input.home, sessionPayload, current.nativeSessionId);
1125
+ await current.nativeAccess.respond(request.requestId, request.nativeSessionId, request.nativeTurnId ?? null, request.result);
1126
+ }
1127
+ else if (!nativeTui) {
1128
+ if (modelChanging)
1129
+ throw new Error("Wait for model adoption before attaching native input.");
1130
+ if (!process.stdin.isTTY)
1131
+ throw new Error("Native TUI requires the Host's owned terminal.");
1132
+ humanConsole?.close();
1133
+ humanConsole = undefined;
1134
+ nativeOpening ??= openControlledCodexTui(sessionPayload, current.nativeSessionId, current.nativeAccess, {
1135
+ mutate: (method, params) => nativeMutation(current, method, params),
1136
+ respond: async (requestId, nativeTurnId, result) => {
1137
+ await handleControl({ protocol: AGENT_HOST_CONTROL_PROTOCOL, type: "native-respond",
1138
+ nativeSessionId: current.nativeSessionId, requestId, nativeTurnId, result });
1139
+ },
1140
+ onExit: () => {
1141
+ nativeTui = undefined;
1142
+ process.stdout.write("\r\nNative TUI detached. Reattach the terminal to reopen this same Thread; the Agent Host remains running.\r\n");
1143
+ },
1144
+ onError: error => process.stderr.write(`Native TUI: ${errorText(error)}\n`)
1145
+ });
1146
+ try {
1147
+ nativeTui = await nativeOpening;
1148
+ }
1149
+ finally {
1150
+ nativeOpening = undefined;
1151
+ }
1152
+ if (session !== current) {
1153
+ nativeTui.close();
1154
+ nativeTui = undefined;
1155
+ throw new Error("Session changed while opening native access.");
1156
+ }
1157
+ }
1158
+ return controlResult("accepted", { ...snapshot, nativeTui: !!nativeTui });
1159
+ }
1032
1160
  if (request.type === "status") {
1033
1161
  // Read the Agent's configuration at answer time, not from the stored
1034
1162
  // snapshot. The Agent may have changed it since the last state
@@ -1036,10 +1164,20 @@ export async function runAgentHost(input) {
1036
1164
  // current reading rather than the one that was true at launch. Nothing is
1037
1165
  // sent to the Agent to obtain it: this reads what the Session has already
1038
1166
  // been told.
1167
+ let requestBytes = 0;
1168
+ const nativeRequests = session?.nativeAccess?.requests.filter(request => {
1169
+ requestBytes += Buffer.byteLength(JSON.stringify(request));
1170
+ return requestBytes <= 8000;
1171
+ }).slice(0, 8);
1039
1172
  return controlResult("status", session === undefined
1040
1173
  ? snapshot
1041
1174
  : validateSnapshot({
1042
1175
  ...snapshot,
1176
+ nativeTui: !!nativeTui,
1177
+ ...(nativeRequests === undefined ? {} : { nativeRequests,
1178
+ nativeRequestsOmitted: nativeRequests.length !== session.nativeAccess.requests.length }),
1179
+ ...(session.publicReply === undefined ? {} : { publicReply: session.publicReply }),
1180
+ ...(session.adapterId === "codex" ? { publicReplyObservation: "supported" } : {}),
1043
1181
  runConfiguration: session.runConfiguration
1044
1182
  }));
1045
1183
  }
@@ -1138,6 +1276,8 @@ export async function runAgentHost(input) {
1138
1276
  const accepted = await enqueueSerialized(async () => setAuthority(request));
1139
1277
  return controlResult("accepted", accepted);
1140
1278
  }
1279
+ if (request.type !== "launch")
1280
+ throw new Error("Unsupported Host request.");
1141
1281
  const redeemed = await redeem(input.home, request.ticket);
1142
1282
  if (activeRunPayload !== undefined
1143
1283
  || ["starting", "ready", "settling", "delivery-unknown"].includes(snapshot.state)) {
@@ -1149,8 +1289,9 @@ export async function runAgentHost(input) {
1149
1289
  }
1150
1290
  const accepted = await enqueueDispatch(redeemed);
1151
1291
  return controlResult("accepted", accepted);
1152
- });
1153
- const humanConsole = process.stdin.isTTY
1292
+ };
1293
+ const control = await openAgentHostControl(input.home, payload, () => snapshot, handleControl);
1294
+ humanConsole = process.stdin.isTTY
1154
1295
  ? createInterface({ input: process.stdin, output: process.stdout, terminal: true })
1155
1296
  : undefined;
1156
1297
  promptHuman = () => {
@@ -1231,6 +1372,7 @@ export async function runAgentHost(input) {
1231
1372
  if (forceKillTimer !== undefined)
1232
1373
  clearTimeout(forceKillTimer);
1233
1374
  humanConsole?.close();
1375
+ nativeTui?.close();
1234
1376
  await closeAgentHostEndpoints({
1235
1377
  owner: endpointOwner, session, lease: endpointLease, adapterId: endpointAdapterId,
1236
1378
  attemptId: activeRunAttemptId ?? lastTerminal?.attemptId,
@@ -1309,6 +1451,9 @@ export async function sendAgentHostAuthorityControl(input) {
1309
1451
  export async function sendAgentHostCancelControl(input) {
1310
1452
  return await sendAgentHostControl(input);
1311
1453
  }
1454
+ export async function sendAgentHostNativeControl(input) {
1455
+ return sendAgentHostControl(input);
1456
+ }
1312
1457
  export async function inspectAgentHost(input) {
1313
1458
  const result = await sendAgentHostControl({
1314
1459
  ...input,
@@ -1550,6 +1695,12 @@ function boundControlResponse(result) {
1550
1695
  };
1551
1696
  if (withinControlBound(result))
1552
1697
  return result;
1698
+ if (result.snapshot.publicReply !== undefined) {
1699
+ const { publicReply: _omitted, ...snapshot } = result.snapshot;
1700
+ result = { ...result, snapshot };
1701
+ if (withinControlBound(result))
1702
+ return result;
1703
+ }
1553
1704
  const clip = (text, chars) => {
1554
1705
  if (text.length <= chars)
1555
1706
  return text;
@@ -1613,6 +1764,22 @@ function validateControl(control) {
1613
1764
  }
1614
1765
  if (control.type === "status")
1615
1766
  return Object.freeze({ ...control });
1767
+ if (control.type === "native-open" || control.type === "native-respond" || control.type === "native-model") {
1768
+ validateIdentity(control.nativeSessionId, "native Session id");
1769
+ if (control.type === "native-model" && (typeof control.model !== "string"
1770
+ || !control.model.trim() || control.model.length > 200 || control.model.includes("\0"))) {
1771
+ throw new Error("Invalid native model.");
1772
+ }
1773
+ if (control.type === "native-respond") {
1774
+ if (control.nativeTurnId !== null)
1775
+ validateIdentity(control.nativeTurnId, "native Turn id");
1776
+ if ((typeof control.requestId !== "string" && typeof control.requestId !== "number")
1777
+ || control.result === null || typeof control.result !== "object" || Array.isArray(control.result)) {
1778
+ throw new Error("Invalid native request response.");
1779
+ }
1780
+ }
1781
+ return Object.freeze({ ...control });
1782
+ }
1616
1783
  if (control.type === "cancel") {
1617
1784
  if (control.nativeOnly !== undefined && typeof control.nativeOnly !== "boolean") {
1618
1785
  throw new Error("Native-only cancellation flag is invalid.");
@@ -42,7 +42,7 @@ export class CodexAppServerRuntime {
42
42
  cwd: text(input.cwd, "Codex thread cwd"),
43
43
  ...threadOptions(input)
44
44
  });
45
- return { conversationId: threadId(result) };
45
+ return { conversationId: threadId(result), raw: result };
46
46
  }
47
47
  async resumeConversation(conversationId, options = {}) {
48
48
  const id = text(conversationId, "Codex thread id");
@@ -0,0 +1,175 @@
1
+ import { isDeepStrictEqual } from "node:util";
2
+ import { unknownAgentRunConfiguration } from "./agentRunConfiguration.js";
3
+ import { CodexPreSubmissionError } from "./codexAppServerRuntime.js";
4
+ const READS = new Set([
5
+ "config/read", "configRequirements/read", "model/list", "collaborationMode/list",
6
+ "experimentalFeature/list", "skills/list", "hooks/list", "mcpServerStatus/list", "account/read",
7
+ "account/rateLimits/read", "modelProvider/capabilities/read", "permissionProfile/list",
8
+ "thread/read", "thread/turns/list", "thread/items/list", "thread/goal/get"
9
+ ]);
10
+ const REQUESTS = new Set([
11
+ "item/commandExecution/requestApproval", "item/fileChange/requestApproval",
12
+ "item/tool/requestUserInput", "item/permissions/requestApproval", "mcpServer/elicitation/request"
13
+ ]);
14
+ /** A view of one existing connection, never another Thread or durable input queue.
15
+ * Both the native TUI and Web answer the original server request on this transport. */
16
+ export function createCodexNativeAccess(threadId, initialized, channel, openingMessages = [], initialConfiguration) {
17
+ const pending = new Map();
18
+ const listeners = new Set();
19
+ let configuration = initialConfiguration;
20
+ let observedAt = new Date().toISOString();
21
+ const observed = () => typeof configuration?.model !== "string"
22
+ ? unknownAgentRunConfiguration("No confirmed native model response. Reconnect to restore the fixed launch configuration.")
23
+ : {
24
+ status: "observed", observedAt,
25
+ handshake: { status: "unsupported", reason: "Model reported by Codex thread/start or thread/resume; not a capability handshake." },
26
+ requested: [],
27
+ axes: [{ key: "model", category: "model", current: { status: "observed", value: configuration.model }, offered: [] }]
28
+ };
29
+ const observe = (message) => {
30
+ const params = message.params;
31
+ if (params?.threadId !== threadId)
32
+ return;
33
+ if ((typeof message.id === "string" || typeof message.id === "number")
34
+ && typeof message.method === "string" && REQUESTS.has(message.method)
35
+ && (typeof params.turnId === "string"
36
+ || (message.method === "mcpServer/elicitation/request" && params.turnId == null))) {
37
+ pending.set(message.id, { id: message.id, method: message.method,
38
+ threadId, turnId: typeof params.turnId === "string" ? params.turnId : null, params });
39
+ }
40
+ if (message.method === "serverRequest/resolved")
41
+ pending.delete(params.requestId);
42
+ if (message.method === "turn/completed") {
43
+ const turn = params.turn;
44
+ for (const [id, request] of pending)
45
+ if (request.turnId === turn?.id)
46
+ pending.delete(id);
47
+ }
48
+ for (const listener of listeners)
49
+ listener(message);
50
+ };
51
+ for (const message of openingMessages)
52
+ observe(message);
53
+ channel.onMessage(observe);
54
+ return {
55
+ initialized,
56
+ get attached() { return listeners.size > 0; },
57
+ get requests() { return [...pending.values()]; },
58
+ get runConfiguration() { return observed(); },
59
+ async setModel(model) {
60
+ try {
61
+ if (!model.trim() || model.length > 200 || model.includes("\0"))
62
+ throw new Error("Invalid model.");
63
+ const read = await channel.request("thread/read", { threadId, includeTurns: false });
64
+ const thread = read.thread;
65
+ if (thread?.id !== threadId || thread.status?.type !== "idle") {
66
+ throw new Error("Model switching requires a confirmed idle Thread.");
67
+ }
68
+ }
69
+ catch (error) {
70
+ throw new CodexPreSubmissionError(error instanceof Error ? error.message : "Native idle state is unavailable.");
71
+ }
72
+ // No fork/start, account, permission or Role-default mutation. Do not
73
+ // preserve an old observation if the native mutation becomes uncertain.
74
+ configuration = undefined;
75
+ const result = await channel.request("thread/resume", { threadId, model, excludeTurns: true });
76
+ if (result.thread?.id !== threadId || result.model !== model) {
77
+ throw new Error("Native model adoption was not confirmed; inspect before another change.");
78
+ }
79
+ configuration = result;
80
+ observedAt = new Date().toISOString();
81
+ },
82
+ async read(method, params) {
83
+ // TUI startup attaches to the already opened Thread without resuming it
84
+ // with new options or changing the Host's configured execution boundary.
85
+ if (method === "thread/resume") {
86
+ if (params.threadId !== threadId)
87
+ throw new Error("Select the existing Session; new/forked Threads are not supported here.");
88
+ const page = params.initialTurnsPage;
89
+ configuration = await channel.request("thread/resume", { threadId,
90
+ ...(typeof params.excludeTurns === "boolean" ? { excludeTurns: params.excludeTurns } : {}),
91
+ ...(page ? { initialTurnsPage: { ...page, limit: Math.min(40, typeof page.limit === "number" && page.limit > 0 ? page.limit : 40) } } : {}) });
92
+ observedAt = new Date().toISOString();
93
+ return configuration;
94
+ }
95
+ if (!READS.has(method))
96
+ throw new Error(`Native operation is not available in managed access: ${method}.`);
97
+ if (method.startsWith("thread/") && params.threadId !== threadId) {
98
+ throw new Error("Native read targets a different Thread.");
99
+ }
100
+ // Native TUI owns its hydration protocol (legacy TUI can explicitly ask
101
+ // for full turns). Web history remains independently bounded/paginated.
102
+ return channel.request(method, method === "thread/turns/list" || method === "thread/items/list"
103
+ ? { ...params, limit: Math.min(40, typeof params.limit === "number" && params.limit > 0 ? params.limit : 40) }
104
+ : params);
105
+ },
106
+ validateTurn(params) {
107
+ if (!configuration)
108
+ throw new Error("Native TUI has not attached to the existing Thread.");
109
+ for (const [key, source] of Object.entries({
110
+ model: "model", cwd: "cwd", runtimeWorkspaceRoots: "runtimeWorkspaceRoots",
111
+ approvalPolicy: "approvalPolicy", approvalsReviewer: "approvalsReviewer",
112
+ effort: "reasoningEffort", collaborationMode: "collaborationMode"
113
+ })) {
114
+ if (params[key] != null && !isDeepStrictEqual(params[key], configuration[source])) {
115
+ throw new Error(`Native ${key} differs from the managed Session; change it through Yui configuration.`);
116
+ }
117
+ }
118
+ for (const key of ["sandboxPolicy", "permissions", "environments", "toolOutput", "additionalContext", "outputSchema", "disabledPluginIds", "serviceTierForTurn", "summary", "personality", "cyberAccessProgram"]) {
119
+ if (params[key] != null)
120
+ throw new Error(`Native ${key} overrides are not supported in managed access.`);
121
+ }
122
+ if (params.serviceTier != null && params.serviceTier !== (configuration.serviceTier ?? "default")) {
123
+ throw new Error("Native service tier differs from the managed Session.");
124
+ }
125
+ },
126
+ async respond(id, expectedThread, turnId, result) {
127
+ const request = pending.get(id);
128
+ if (!request || request.threadId !== expectedThread || request.turnId !== turnId) {
129
+ throw new Error("Native request is no longer pending for this exact Thread and Turn.");
130
+ }
131
+ validateNativeResponse(request, result);
132
+ // Claim before awaiting transport. A lost send acknowledgement is unknown,
133
+ // not permission for another entry to repeat a possibly applied answer.
134
+ pending.delete(id);
135
+ await channel.send({ id, result });
136
+ },
137
+ events(listener) { listeners.add(listener); return () => { listeners.delete(listener); }; }
138
+ };
139
+ }
140
+ function validateNativeResponse(request, result) {
141
+ if (!result || typeof result !== "object" || Array.isArray(result))
142
+ throw new Error("Missing native response.");
143
+ if (request.method === "mcpServer/elicitation/request") {
144
+ if (!["accept", "decline", "cancel"].includes(String(result.action))
145
+ || (result.action !== "accept" && result.content != null)) {
146
+ throw new Error("Invalid native MCP elicitation response.");
147
+ }
148
+ }
149
+ else if (request.method === "item/tool/requestUserInput") {
150
+ const answers = result.answers;
151
+ const questions = request.params.questions;
152
+ if (!answers || typeof answers !== "object" || Array.isArray(answers) || !Array.isArray(questions)) {
153
+ throw new Error("Native question requires answers keyed by question id.");
154
+ }
155
+ const ids = questions.map(q => q.id);
156
+ if (Object.keys(answers).some(id => !ids.includes(id))
157
+ || ids.some(id => {
158
+ if (typeof id !== "string")
159
+ return true;
160
+ const answer = answers[id];
161
+ return !Array.isArray(answer?.answers) || answer.answers.some(value => typeof value !== "string");
162
+ })) {
163
+ throw new Error("Native answers do not match the original questions.");
164
+ }
165
+ }
166
+ else if (request.method === "item/permissions/requestApproval") {
167
+ // Web currently exposes decline only; richer native grants remain the TUI's
168
+ // protocol responsibility. Never infer broad grants from an accept button.
169
+ if (!result.permissions || typeof result.permissions !== "object")
170
+ throw new Error("Missing native permissions response.");
171
+ }
172
+ else if (!["accept", "acceptForSession", "decline", "cancel"].includes(String(result.decision))) {
173
+ throw new Error("Invalid native approval decision.");
174
+ }
175
+ }
@@ -0,0 +1,116 @@
1
+ import { spawn } from "node:child_process";
2
+ import { randomBytes } from "node:crypto";
3
+ import { once } from "node:events";
4
+ import WebSocket, { WebSocketServer } from "ws";
5
+ import { localCodexTuiEnvironment } from "./codexInteractiveHost.js";
6
+ /** The TUI is a view of the Host-owned connection. It cannot launch, fork,
7
+ * reconfigure, or submit outside the Host's existing input admission. */
8
+ export async function openControlledCodexTui(payload, threadId, access, input) {
9
+ const baseArgs = JSON.parse(payload.environment.YUI_AGENT_BASE_ARGS ?? "[]");
10
+ if (!Array.isArray(baseArgs) || baseArgs.some(arg => typeof arg !== "string"))
11
+ throw new Error("Invalid Codex base arguments.");
12
+ const token = randomBytes(32).toString("hex");
13
+ const server = new WebSocketServer({
14
+ host: "127.0.0.1", port: 0, maxPayload: 1024 * 1024, perMessageDeflate: false,
15
+ verifyClient: (info) => info.req.headers.authorization === `Bearer ${token}`
16
+ });
17
+ let socket;
18
+ let closed = false;
19
+ let markReady;
20
+ let failReady;
21
+ const ready = new Promise((resolve, reject) => { markReady = resolve; failReady = reject; });
22
+ void ready.catch(() => { });
23
+ const unsubscribe = access.events(message => {
24
+ if (socket?.readyState === WebSocket.OPEN)
25
+ socket.send(JSON.stringify(message));
26
+ });
27
+ server.on("connection", client => {
28
+ if (socket || closed) {
29
+ client.close(1008, "TUI already attached.");
30
+ return;
31
+ }
32
+ socket = client;
33
+ client.on("error", input.onError);
34
+ client.on("message", (bytes, binary) => {
35
+ void (async () => {
36
+ if (binary)
37
+ throw new Error("Expected native JSON request.");
38
+ const message = JSON.parse(bytes.toString());
39
+ const id = message.id;
40
+ if (typeof id !== "number" && typeof id !== "string")
41
+ return; // notifications
42
+ try {
43
+ if (message.method === undefined) {
44
+ const request = access.requests.find(request => request.id === id);
45
+ if (!request)
46
+ throw new Error("Native request is no longer pending.");
47
+ await input.respond(id, request.turnId, message.result);
48
+ return;
49
+ }
50
+ const method = String(message.method), params = (message.params ?? {});
51
+ if (method === "turn/start")
52
+ access.validateTurn(params);
53
+ const result = method === "initialize" ? access.initialized
54
+ : ["turn/start", "turn/steer", "turn/interrupt"].includes(method) ? await input.mutate(method, params)
55
+ : await access.read(method, params);
56
+ if (client.readyState === WebSocket.OPEN)
57
+ client.send(JSON.stringify({ id, result }));
58
+ if (method === "thread/resume") {
59
+ markReady();
60
+ for (const request of access.requests)
61
+ client.send(JSON.stringify({
62
+ id: request.id, method: request.method, params: request.params
63
+ }));
64
+ }
65
+ }
66
+ catch (error) {
67
+ if (client.readyState === WebSocket.OPEN)
68
+ client.send(JSON.stringify({
69
+ id, error: { code: -32600, message: error instanceof Error ? error.message : String(error) }
70
+ }));
71
+ }
72
+ })().catch(input.onError);
73
+ });
74
+ });
75
+ try {
76
+ await once(server, "listening");
77
+ }
78
+ catch (error) {
79
+ unsubscribe();
80
+ server.close();
81
+ throw error;
82
+ }
83
+ const address = server.address();
84
+ if (!address || typeof address === "string")
85
+ throw new Error("Native TUI relay did not bind.");
86
+ const child = spawn(payload.command, [...baseArgs,
87
+ "--remote", `ws://127.0.0.1:${address.port}`, "--remote-auth-token-env", "YUI_CODEX_REMOTE_AUTH_TOKEN",
88
+ "resume", threadId], {
89
+ cwd: payload.cwd, stdio: "inherit",
90
+ env: { ...localCodexTuiEnvironment(payload.environment), YUI_CODEX_REMOTE_AUTH_TOKEN: token }
91
+ });
92
+ const close = () => {
93
+ if (closed)
94
+ return;
95
+ closed = true;
96
+ unsubscribe();
97
+ socket?.terminate();
98
+ server.close();
99
+ child.kill("SIGTERM");
100
+ };
101
+ server.on("error", input.onError);
102
+ child.on("error", error => { failReady(error); input.onError(error); close(); input.onExit(); });
103
+ child.once("exit", () => { failReady(new Error("Native TUI exited before attachment.")); close(); input.onExit(); });
104
+ const deadline = setTimeout(() => failReady(new Error("Native TUI did not attach to the selected Thread.")), 10_000);
105
+ try {
106
+ await ready;
107
+ }
108
+ catch (error) {
109
+ close();
110
+ throw error;
111
+ }
112
+ finally {
113
+ clearTimeout(deadline);
114
+ }
115
+ return { close };
116
+ }
@@ -0,0 +1,21 @@
1
+ import { resolve } from "node:path";
2
+ import { controllerSocketPath } from "../core/controllerEndpoint.js";
3
+ import { managedIntegrationRuntimeRoot } from "../storage/homeLayout.js";
4
+ import { yuiTmuxServerName } from "../tmux/tmuxManager.js";
5
+ import { FileTaskRuntimeIsolation } from "./taskRuntimeIsolation.js";
6
+ /** One identity/layout for Integration execution and exact-owner cleanup. */
7
+ export function defaultIntegrationRuntimeIsolation(home, homeId) {
8
+ const controlHome = resolve(home);
9
+ const runtimeRoot = managedIntegrationRuntimeRoot(controlHome);
10
+ return new FileTaskRuntimeIsolation({
11
+ runtimeRoot,
12
+ pathLayout: "compact",
13
+ controlPlane: {
14
+ yuiHome: controlHome,
15
+ managedRuntimeRoot: runtimeRoot,
16
+ controllerSocketPath: controllerSocketPath(homeId),
17
+ tmuxNamespace: yuiTmuxServerName(controlHome),
18
+ globalInstallPaths: [process.execPath]
19
+ }
20
+ });
21
+ }
@@ -0,0 +1,23 @@
1
+ export function foldPublicReply(previous, nativeSessionId, method, params) {
2
+ if (params.threadId !== nativeSessionId)
3
+ return previous;
4
+ if (method === "turn/started")
5
+ return undefined;
6
+ if (typeof params.turnId !== "string")
7
+ return previous;
8
+ if (method === "item/agentMessage/delta" && typeof params.itemId === "string" && typeof params.delta === "string") {
9
+ const same = previous?.id === params.itemId && previous.turnId === params.turnId;
10
+ const text = (same ? previous.text : "") + params.delta;
11
+ return { nativeSessionId, turnId: params.turnId, id: params.itemId,
12
+ text: text.slice(0, 6000), truncated: text.length > 6000 || !!(same && previous.truncated) };
13
+ }
14
+ if ((method === "item/started" || method === "item/completed")
15
+ && params.item !== null && typeof params.item === "object") {
16
+ const item = params.item;
17
+ if (item.type === "agentMessage" && typeof item.id === "string" && typeof item.text === "string") {
18
+ return { nativeSessionId, turnId: params.turnId, id: item.id,
19
+ text: item.text.slice(0, 6000), truncated: item.text.length > 6000 };
20
+ }
21
+ }
22
+ return previous;
23
+ }