ur-agent 1.65.4 → 1.65.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -40,18 +40,75 @@ var vscode17 = __toESM(require("vscode"));
40
40
  var vscode2 = __toESM(require("vscode"));
41
41
 
42
42
  // src/diffs/store.ts
43
+ var fs2 = __toESM(require("node:fs"));
44
+ var path2 = __toESM(require("node:path"));
45
+ var vscode = __toESM(require("vscode"));
46
+
47
+ // src/util/safeWorkspacePath.ts
48
+ var import_node_crypto = require("node:crypto");
43
49
  var fs = __toESM(require("node:fs"));
44
50
  var path = __toESM(require("node:path"));
45
- var vscode = __toESM(require("vscode"));
51
+ function safeWorkspacePath(workspaceRoot2, candidate, label = "UR workspace data") {
52
+ const root = path.resolve(workspaceRoot2);
53
+ const target = path.resolve(candidate);
54
+ const relative3 = path.relative(root, target);
55
+ if (!relative3 || relative3 === ".." || relative3.startsWith(`..${path.sep}`) || path.isAbsolute(relative3)) {
56
+ throw new Error(`${label} path escapes the workspace`);
57
+ }
58
+ let current = root;
59
+ for (const segment of relative3.split(path.sep)) {
60
+ current = path.join(current, segment);
61
+ try {
62
+ if (fs.lstatSync(current).isSymbolicLink()) {
63
+ throw new Error(`${label} path contains a symbolic link: ${current}`);
64
+ }
65
+ } catch (error) {
66
+ if (isMissingPath(error)) continue;
67
+ throw error;
68
+ }
69
+ }
70
+ return target;
71
+ }
72
+ function writeWorkspaceJsonAtomic(workspaceRoot2, candidate, value, label = "UR workspace data") {
73
+ const target = safeWorkspacePath(workspaceRoot2, candidate, label);
74
+ const directory = path.dirname(target);
75
+ safeWorkspacePath(workspaceRoot2, directory, label);
76
+ fs.mkdirSync(directory, { recursive: true });
77
+ safeWorkspacePath(workspaceRoot2, directory, label);
78
+ const temporary = path.join(
79
+ directory,
80
+ `.${path.basename(target)}.${process.pid}.${(0, import_node_crypto.randomUUID)()}.tmp`
81
+ );
82
+ try {
83
+ fs.writeFileSync(temporary, `${JSON.stringify(value, null, 2)}
84
+ `, {
85
+ encoding: "utf8",
86
+ flag: "wx",
87
+ mode: 384
88
+ });
89
+ fs.renameSync(temporary, target);
90
+ } catch (error) {
91
+ try {
92
+ fs.rmSync(temporary, { force: true });
93
+ } catch {
94
+ }
95
+ throw error;
96
+ }
97
+ }
98
+ function isMissingPath(error) {
99
+ return error instanceof Error && "code" in error && error.code === "ENOENT";
100
+ }
101
+
102
+ // src/diffs/store.ts
46
103
  function workspaceRoot() {
47
104
  const activeUri = vscode.window.activeTextEditor?.document.uri;
48
105
  return (activeUri ? vscode.workspace.getWorkspaceFolder(activeUri) : void 0)?.uri.fsPath ?? vscode.workspace.workspaceFolders?.[0]?.uri.fsPath;
49
106
  }
50
107
  function diffsRoot(root) {
51
- return path.join(root, ".ur", "ide", "diffs");
108
+ return path2.join(root, ".ur", "ide", "diffs");
52
109
  }
53
110
  function manifestPath(root) {
54
- return path.join(diffsRoot(root), "manifest.json");
111
+ return path2.join(diffsRoot(root), "manifest.json");
55
112
  }
56
113
  function patchPath(root, bundle) {
57
114
  return artifactPath(root, bundle, "patch");
@@ -60,58 +117,91 @@ function metadataPath(root, bundle) {
60
117
  return artifactPath(root, bundle, "metadata");
61
118
  }
62
119
  var DIFF_ID_PATTERN = /^diff-[1-9][0-9]*$/u;
120
+ var DIFF_STATUSES = /* @__PURE__ */ new Set(["pending", "commented", "approved", "rejected"]);
121
+ var MAX_DIFF_JSON_BYTES = 16 * 1024 * 1024;
122
+ var MAX_PATCH_BYTES = 64 * 1024 * 1024;
63
123
  function artifactPath(root, bundle, kind) {
64
124
  if (!DIFF_ID_PATTERN.test(bundle.id)) throw new Error(`Invalid UR diff id: ${bundle.id}`);
65
- const relative2 = kind === "patch" ? bundle.patchFile : bundle.metadataFile;
125
+ const relative3 = kind === "patch" ? bundle.patchFile : bundle.metadataFile;
66
126
  const expected = kind === "patch" ? `patches/${bundle.id}.patch` : `metadata/${bundle.id}.json`;
67
- if (relative2.replaceAll("\\", "/") !== expected) {
127
+ if (relative3.replaceAll("\\", "/") !== expected) {
68
128
  throw new Error(`Invalid UR diff ${kind} path for ${bundle.id}`);
69
129
  }
70
- const rootPath = path.resolve(diffsRoot(root));
71
- const target = path.resolve(rootPath, relative2);
72
- if (!target.startsWith(`${rootPath}${path.sep}`)) throw new Error(`UR diff ${kind} path escapes the diff store`);
73
- return target;
130
+ const rootPath = path2.resolve(diffsRoot(root));
131
+ const target = path2.resolve(rootPath, relative3);
132
+ if (!target.startsWith(`${rootPath}${path2.sep}`)) throw new Error(`UR diff ${kind} path escapes the diff store`);
133
+ return safeWorkspacePath(root, target, `UR diff ${kind}`);
74
134
  }
75
- function isValidBundle(value) {
76
- if (!value || typeof value !== "object") return false;
135
+ function isValidBundle(root, value) {
136
+ if (!isRecord(value)) return false;
77
137
  const bundle = value;
78
138
  try {
79
- patchPath(".", bundle);
80
- metadataPath(".", bundle);
81
- return true;
139
+ patchPath(root, bundle);
140
+ metadataPath(root, bundle);
141
+ return typeof bundle.title === "string" && bundle.title.length > 0 && DIFF_STATUSES.has(bundle.status) && (bundle.baseRef === void 0 || typeof bundle.baseRef === "string") && (bundle.staged === void 0 || typeof bundle.staged === "boolean") && Array.isArray(bundle.files) && bundle.files.every(isValidFileChange) && Array.isArray(bundle.comments) && bundle.comments.every(isValidComment) && typeof bundle.createdAt === "string" && typeof bundle.updatedAt === "string";
82
142
  } catch {
83
143
  return false;
84
144
  }
85
145
  }
86
- function readJson(file, fallback) {
146
+ function readJson(root, file, fallback, maxBytes = MAX_DIFF_JSON_BYTES) {
87
147
  try {
88
- return JSON.parse(fs.readFileSync(file, "utf8"));
148
+ const safeFile = safeWorkspacePath(root, file, "UR diff");
149
+ const size = fs2.statSync(safeFile).size;
150
+ if (!Number.isSafeInteger(size) || size < 0 || size > maxBytes) {
151
+ return fallback;
152
+ }
153
+ return JSON.parse(fs2.readFileSync(safeFile, "utf8"));
89
154
  } catch {
90
155
  return fallback;
91
156
  }
92
157
  }
93
- function writeJson(file, value) {
94
- fs.mkdirSync(path.dirname(file), { recursive: true });
95
- fs.writeFileSync(file, `${JSON.stringify(value, null, 2)}
96
- `);
158
+ function writeJson(root, file, value) {
159
+ writeWorkspaceJsonAtomic(root, file, value, "UR diff");
97
160
  }
98
161
  function loadManifest(root) {
99
- const manifest = readJson(manifestPath(root), { version: 1, diffs: [] });
100
- return Array.isArray(manifest.diffs) ? { version: 1, diffs: manifest.diffs.filter(isValidBundle) } : { version: 1, diffs: [] };
162
+ const manifest = readJson(
163
+ root,
164
+ manifestPath(root),
165
+ { version: 1, diffs: [] }
166
+ );
167
+ return isRecord(manifest) && Array.isArray(manifest.diffs) ? {
168
+ version: 1,
169
+ diffs: manifest.diffs.filter((bundle) => isValidBundle(root, bundle))
170
+ } : { version: 1, diffs: [] };
101
171
  }
102
172
  function loadBundleMetadata(root, bundle) {
103
- const metadata = readJson(metadataPath(root, bundle), bundle);
104
- return isValidBundle(metadata) && metadata.id === bundle.id ? metadata : bundle;
173
+ const metadata = readJson(
174
+ root,
175
+ metadataPath(root, bundle),
176
+ bundle
177
+ );
178
+ return isValidBundle(root, metadata) && metadata.id === bundle.id ? metadata : bundle;
105
179
  }
106
180
  function readPatch(root, bundle) {
107
181
  const file = patchPath(root, bundle);
108
- return fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
182
+ if (!fs2.existsSync(file)) return "";
183
+ const size = fs2.statSync(file).size;
184
+ if (!Number.isSafeInteger(size) || size < 0 || size > MAX_PATCH_BYTES) {
185
+ throw new Error(`UR diff patch exceeds ${MAX_PATCH_BYTES / (1024 * 1024)} MiB`);
186
+ }
187
+ return fs2.readFileSync(file, "utf8");
109
188
  }
110
189
  function writeManifest(root, manifest) {
111
- writeJson(manifestPath(root), manifest);
190
+ writeJson(root, manifestPath(root), manifest);
112
191
  }
113
192
  function writeBundleMetadata(root, bundle) {
114
- writeJson(metadataPath(root, bundle), bundle);
193
+ writeJson(root, metadataPath(root, bundle), bundle);
194
+ }
195
+ function isRecord(value) {
196
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
197
+ }
198
+ function isValidFileChange(value) {
199
+ if (!isRecord(value)) return false;
200
+ return typeof value.path === "string" && Number.isSafeInteger(value.additions) && Number(value.additions) >= 0 && Number.isSafeInteger(value.deletions) && Number(value.deletions) >= 0;
201
+ }
202
+ function isValidComment(value) {
203
+ if (!isRecord(value)) return false;
204
+ return typeof value.at === "string" && typeof value.text === "string" && (value.file === void 0 || typeof value.file === "string") && (value.line === void 0 || Number.isSafeInteger(value.line) && Number(value.line) > 0);
115
205
  }
116
206
 
117
207
  // src/bridge/urCli.ts
@@ -234,7 +324,7 @@ function isCapturedNonZeroExit(error) {
234
324
 
235
325
  // src/actions/background.ts
236
326
  var VALID_STATUSES = ["queued", "running", "completed", "failed", "canceled"];
237
- function isRecord(value) {
327
+ function isRecord2(value) {
238
328
  return typeof value === "object" && value !== null;
239
329
  }
240
330
  function tryParseBackgroundListJson(raw) {
@@ -244,10 +334,10 @@ function tryParseBackgroundListJson(raw) {
244
334
  } catch {
245
335
  return null;
246
336
  }
247
- if (!isRecord(data) || !Array.isArray(data.tasks)) return null;
337
+ if (!isRecord2(data) || !Array.isArray(data.tasks)) return null;
248
338
  const summaries = [];
249
339
  for (const entry of data.tasks) {
250
- if (!isRecord(entry)) continue;
340
+ if (!isRecord2(entry)) continue;
251
341
  if (typeof entry.id !== "string" || typeof entry.task !== "string") continue;
252
342
  if (!VALID_STATUSES.includes(entry.status)) continue;
253
343
  const status = entry.status;
@@ -652,7 +742,7 @@ var ActionsTreeProvider = class {
652
742
  };
653
743
 
654
744
  // src/chat/chatController.ts
655
- var import_node_crypto2 = require("node:crypto");
745
+ var import_node_crypto4 = require("node:crypto");
656
746
  var vscode6 = __toESM(require("vscode"));
657
747
 
658
748
  // src/bridge/types.ts
@@ -670,6 +760,12 @@ function isCanUseToolRequest(message) {
670
760
  var import_node_child_process3 = require("node:child_process");
671
761
  var NdjsonBuffer = class {
672
762
  buffer = "";
763
+ discardingOversizedLine = false;
764
+ maxLineLength;
765
+ droppedOversizedLine = false;
766
+ constructor(maxLineLength = 16 * 1024 * 1024) {
767
+ this.maxLineLength = Number.isSafeInteger(maxLineLength) && maxLineLength > 0 ? maxLineLength : 16 * 1024 * 1024;
768
+ }
673
769
  /** Feed a raw chunk (may contain zero, one, or many complete lines, and may
674
770
  * split a line across two calls). Returns every complete, parseable line
675
771
  * found. Malformed lines are dropped, never thrown — the CLI's own
@@ -677,21 +773,45 @@ var NdjsonBuffer = class {
677
773
  * to stderr, so a malformed line here means something unexpected slipped
678
774
  * through, not a reason to crash the extension. */
679
775
  push(chunk) {
680
- this.buffer += chunk;
681
776
  const messages = [];
682
- for (; ; ) {
683
- const newline = this.buffer.indexOf("\n");
684
- if (newline === -1) break;
685
- const line = this.buffer.slice(0, newline);
686
- this.buffer = this.buffer.slice(newline + 1);
687
- const parsed = parseNdjsonLine(line);
777
+ let offset = 0;
778
+ while (offset < chunk.length) {
779
+ const newline = chunk.indexOf("\n", offset);
780
+ const end = newline === -1 ? chunk.length : newline;
781
+ const segment = chunk.slice(offset, end);
782
+ if (this.discardingOversizedLine) {
783
+ if (newline === -1) return messages;
784
+ this.discardingOversizedLine = false;
785
+ offset = newline + 1;
786
+ continue;
787
+ }
788
+ if (this.buffer.length + segment.length > this.maxLineLength) {
789
+ this.buffer = "";
790
+ this.droppedOversizedLine = true;
791
+ if (newline === -1) {
792
+ this.discardingOversizedLine = true;
793
+ return messages;
794
+ }
795
+ offset = newline + 1;
796
+ continue;
797
+ }
798
+ this.buffer += segment;
799
+ if (newline === -1) return messages;
800
+ const parsed = parseNdjsonLine(this.buffer);
801
+ this.buffer = "";
688
802
  if (parsed) messages.push(parsed);
803
+ offset = newline + 1;
689
804
  }
690
805
  return messages;
691
806
  }
692
807
  /** Whatever is left with no trailing newline yet (a genuinely partial line
693
808
  * stays buffered; call this only once the stream has actually ended). */
694
809
  flush() {
810
+ if (this.discardingOversizedLine) {
811
+ this.buffer = "";
812
+ this.discardingOversizedLine = false;
813
+ return [];
814
+ }
695
815
  const rest = this.buffer;
696
816
  this.buffer = "";
697
817
  const parsed = parseNdjsonLine(rest);
@@ -736,6 +856,7 @@ function buildControlResponse(requestId, decision) {
736
856
  };
737
857
  }
738
858
  var defaultSpawn = (command2, args, options) => (0, import_node_child_process3.spawn)(command2, args, options);
859
+ var MAX_CAPTURED_STDERR_CHARS = 16 * 1024;
739
860
  function runUrTurn(request, handlers, deps = {}) {
740
861
  const spawnFn = deps.spawn ?? defaultSpawn;
741
862
  const executable = deps.executable ?? (deps.command ? { command: deps.command, args: [], source: "configured", display: deps.command } : resolveUrCommand({ cwd: request.cwd }));
@@ -768,7 +889,8 @@ function runUrTurn(request, handlers, deps = {}) {
768
889
  } };
769
890
  }
770
891
  const stdoutBuffer = new NdjsonBuffer();
771
- const stderrChunks = [];
892
+ let stderrTail = "";
893
+ let omittedStderrChars = 0;
772
894
  let sawResult = false;
773
895
  let resultIsError = false;
774
896
  let canceled = false;
@@ -776,8 +898,11 @@ function runUrTurn(request, handlers, deps = {}) {
776
898
  const finish = (exitCode, signal, spawnError) => {
777
899
  if (settled) return;
778
900
  settled = true;
779
- const stderr = stderrChunks.join("");
780
- const ok = !canceled && !spawnError && sawResult && !resultIsError;
901
+ const stderr = omittedStderrChars > 0 ? `[${omittedStderrChars} earlier stderr characters omitted]
902
+ ${stderrTail}` : stderrTail;
903
+ const protocolError = stdoutBuffer.droppedOversizedLine ? "UR emitted an oversized stream-JSON line; the extension refused to buffer unbounded output." : void 0;
904
+ const exitedCleanly = exitCode === 0 && signal === null;
905
+ const ok = !canceled && !spawnError && !protocolError && exitedCleanly && sawResult && !resultIsError;
781
906
  handlers.onExit({
782
907
  ok,
783
908
  exitCode,
@@ -785,7 +910,22 @@ function runUrTurn(request, handlers, deps = {}) {
785
910
  canceled,
786
911
  sawResult,
787
912
  stderr,
788
- error: spawnError ?? (!ok && !canceled ? deriveErrorMessage(executable, request.cwd, sawResult, resultIsError, exitCode, signal, stderr) : void 0)
913
+ error: spawnError ?? (!ok && !canceled ? protocolError ? formatTurnFailure({
914
+ executable,
915
+ cwd: request.cwd,
916
+ exitCode,
917
+ signal,
918
+ stderr,
919
+ reason: protocolError
920
+ }) : deriveErrorMessage(
921
+ executable,
922
+ request.cwd,
923
+ sawResult,
924
+ resultIsError,
925
+ exitCode,
926
+ signal,
927
+ stderr
928
+ ) : void 0)
789
929
  });
790
930
  };
791
931
  const handleMessage = (message) => {
@@ -811,7 +951,15 @@ function runUrTurn(request, handlers, deps = {}) {
811
951
  }
812
952
  });
813
953
  child.stderr?.on("data", (chunk) => {
814
- stderrChunks.push(chunk.toString("utf8"));
954
+ const text = chunk.toString("utf8");
955
+ const combined = stderrTail + text;
956
+ if (combined.length > MAX_CAPTURED_STDERR_CHARS) {
957
+ const excess = combined.length - MAX_CAPTURED_STDERR_CHARS;
958
+ omittedStderrChars += excess;
959
+ stderrTail = combined.slice(excess);
960
+ } else {
961
+ stderrTail = combined;
962
+ }
815
963
  });
816
964
  child.on("error", (error) => {
817
965
  finish(
@@ -822,7 +970,8 @@ function runUrTurn(request, handlers, deps = {}) {
822
970
  cwd: request.cwd,
823
971
  exitCode: null,
824
972
  signal: null,
825
- stderr: stderrChunks.join(""),
973
+ stderr: omittedStderrChars > 0 ? `[${omittedStderrChars} earlier stderr characters omitted]
974
+ ${stderrTail}` : stderrTail,
826
975
  reason: `Failed to run: ${errorMessage2(error)}`
827
976
  })
828
977
  );
@@ -849,7 +998,7 @@ function writeControlResponse(child, requestId, decision) {
849
998
  }
850
999
  }
851
1000
  function deriveErrorMessage(executable, cwd, sawResult, resultIsError, exitCode, signal, stderr) {
852
- const reason = sawResult && resultIsError ? "UR reported an error completing this turn." : "UR exited without producing a successful result.";
1001
+ const reason = sawResult && resultIsError ? "UR reported an error completing this turn." : sawResult ? "UR produced a result, but the process exited unsuccessfully." : "UR exited without producing a successful result.";
853
1002
  return formatTurnFailure({ executable, cwd, exitCode, signal, stderr, reason });
854
1003
  }
855
1004
  function formatTurnFailure(options) {
@@ -874,7 +1023,7 @@ function errorMessage2(error) {
874
1023
  }
875
1024
 
876
1025
  // src/context/ideContext.ts
877
- var path2 = __toESM(require("node:path"));
1026
+ var path3 = __toESM(require("node:path"));
878
1027
  function formatAttachmentLabel(attachment) {
879
1028
  if (attachment.kind === "file") return `@${attachment.file.path}`;
880
1029
  const { path: filePath, startLine, endLine } = attachment.selection;
@@ -918,7 +1067,7 @@ function captureEditorSnapshot() {
918
1067
  if (!editor) return { workspaceRoot: vscode18.workspace.workspaceFolders?.[0]?.uri.fsPath };
919
1068
  const workspaceRoot2 = vscode18.workspace.getWorkspaceFolder(editor.document.uri)?.uri.fsPath;
920
1069
  const absolutePath = editor.document.uri.fsPath;
921
- const relativePath = workspaceRoot2 ? path2.relative(workspaceRoot2, absolutePath) : absolutePath;
1070
+ const relativePath = workspaceRoot2 ? path3.relative(workspaceRoot2, absolutePath) : absolutePath;
922
1071
  const activeFile = { path: relativePath, languageId: editor.document.languageId };
923
1072
  const selection = editor.selection;
924
1073
  if (selection.isEmpty) return { workspaceRoot: workspaceRoot2, activeFile };
@@ -934,48 +1083,67 @@ function captureEditorSnapshot() {
934
1083
  }
935
1084
 
936
1085
  // src/sessions/sessionStore.ts
937
- var import_node_crypto = require("node:crypto");
938
- var fs2 = __toESM(require("node:fs"));
939
- var path3 = __toESM(require("node:path"));
1086
+ var import_node_crypto2 = require("node:crypto");
1087
+ var fs3 = __toESM(require("node:fs"));
1088
+ var path4 = __toESM(require("node:path"));
940
1089
  var SESSION_ID_PATTERN = /^[a-zA-Z0-9-]{1,128}$/;
941
1090
  var TITLE_MAX_LENGTH = 60;
942
1091
  var DEFAULT_TITLE = "New Chat";
1092
+ var MAX_MANIFEST_BYTES = 4 * 1024 * 1024;
1093
+ var MAX_SESSION_BYTES = 64 * 1024 * 1024;
943
1094
  function chatRoot(root) {
944
- return path3.join(root, ".ur", "ide", "chat");
1095
+ return path4.join(root, ".ur", "ide", "chat");
945
1096
  }
946
1097
  function manifestPath2(root) {
947
- return path3.join(chatRoot(root), "manifest.json");
1098
+ return path4.join(chatRoot(root), "manifest.json");
948
1099
  }
949
1100
  function isValidSessionId(id) {
950
1101
  return SESSION_ID_PATTERN.test(id);
951
1102
  }
952
1103
  function sessionFilePath(root, id) {
953
1104
  if (!isValidSessionId(id)) return null;
954
- const sessionsDir = path3.join(chatRoot(root), "sessions");
955
- const target = path3.join(sessionsDir, `${id}.json`);
956
- const resolvedDir = path3.resolve(sessionsDir) + path3.sep;
957
- const resolvedTarget = path3.resolve(target);
1105
+ const sessionsDir = path4.join(chatRoot(root), "sessions");
1106
+ const target = path4.join(sessionsDir, `${id}.json`);
1107
+ const resolvedDir = path4.resolve(sessionsDir) + path4.sep;
1108
+ const resolvedTarget = path4.resolve(target);
958
1109
  if (!resolvedTarget.startsWith(resolvedDir)) return null;
959
1110
  return target;
960
1111
  }
961
- function readJson2(file, fallback) {
1112
+ function readJson2(root, file, fallback, maxBytes) {
962
1113
  try {
963
- return JSON.parse(fs2.readFileSync(file, "utf8"));
1114
+ const safeFile = safeWorkspacePath(root, file, "UR chat");
1115
+ const size = fs3.statSync(safeFile).size;
1116
+ if (!Number.isSafeInteger(size) || size < 0 || size > maxBytes) {
1117
+ return fallback;
1118
+ }
1119
+ return JSON.parse(fs3.readFileSync(safeFile, "utf8"));
964
1120
  } catch {
965
1121
  return fallback;
966
1122
  }
967
1123
  }
968
- function writeJson2(file, value) {
969
- fs2.mkdirSync(path3.dirname(file), { recursive: true });
970
- fs2.writeFileSync(file, `${JSON.stringify(value, null, 2)}
971
- `);
1124
+ function writeJson2(root, file, value) {
1125
+ writeWorkspaceJsonAtomic(root, file, value, "UR chat");
972
1126
  }
973
1127
  function readManifest(root) {
974
- const manifest = readJson2(manifestPath2(root), { version: 1, sessions: [] });
975
- return Array.isArray(manifest.sessions) ? manifest : { version: 1, sessions: [] };
1128
+ const manifest = readJson2(
1129
+ root,
1130
+ manifestPath2(root),
1131
+ { version: 1, sessions: [] },
1132
+ MAX_MANIFEST_BYTES
1133
+ );
1134
+ if (!isRecord3(manifest) || !Array.isArray(manifest.sessions)) {
1135
+ return { version: 1, sessions: [] };
1136
+ }
1137
+ const unique = /* @__PURE__ */ new Map();
1138
+ for (const session of manifest.sessions) {
1139
+ if (isValidSession(session, root) && !unique.has(session.id)) {
1140
+ unique.set(session.id, session);
1141
+ }
1142
+ }
1143
+ return { version: 1, sessions: [...unique.values()] };
976
1144
  }
977
1145
  function writeManifest2(root, manifest) {
978
- writeJson2(manifestPath2(root), manifest);
1146
+ writeJson2(root, manifestPath2(root), manifest);
979
1147
  }
980
1148
  function upsertManifestEntry(root, session) {
981
1149
  const manifest = readManifest(root);
@@ -989,9 +1157,10 @@ function upsertManifestEntry(root, session) {
989
1157
  }
990
1158
  function createSession(root, options = {}) {
991
1159
  const now = (/* @__PURE__ */ new Date()).toISOString();
1160
+ const requestedTitle = typeof options.title === "string" ? options.title.trim() : "";
992
1161
  const session = {
993
- id: (0, import_node_crypto.randomUUID)(),
994
- title: options.title?.trim() || DEFAULT_TITLE,
1162
+ id: (0, import_node_crypto2.randomUUID)(),
1163
+ title: requestedTitle ? requestedTitle.slice(0, TITLE_MAX_LENGTH) : DEFAULT_TITLE,
995
1164
  workspaceRoot: root,
996
1165
  createdAt: now,
997
1166
  updatedAt: now
@@ -999,7 +1168,7 @@ function createSession(root, options = {}) {
999
1168
  const record = { session, messages: [] };
1000
1169
  const file = sessionFilePath(root, session.id);
1001
1170
  if (!file) throw new Error(`Generated an invalid session id: ${session.id}`);
1002
- writeJson2(file, record);
1171
+ writeJson2(root, file, record);
1003
1172
  upsertManifestEntry(root, session);
1004
1173
  return record;
1005
1174
  }
@@ -1010,10 +1179,17 @@ function listSessions(root, options = {}) {
1010
1179
  }
1011
1180
  function readSession(root, id) {
1012
1181
  const file = sessionFilePath(root, id);
1013
- if (!file || !fs2.existsSync(file)) return null;
1014
- return readJson2(file, null);
1182
+ if (!file) return null;
1183
+ const record = readJson2(
1184
+ root,
1185
+ file,
1186
+ null,
1187
+ MAX_SESSION_BYTES
1188
+ );
1189
+ return isValidRecord(record, root, id) ? record : null;
1015
1190
  }
1016
1191
  function appendMessage(root, id, message) {
1192
+ if (!isValidMessage(message, id)) return null;
1017
1193
  const record = readSession(root, id);
1018
1194
  if (!record) return null;
1019
1195
  record.messages.push(message);
@@ -1023,18 +1199,21 @@ function appendMessage(root, id, message) {
1023
1199
  }
1024
1200
  const file = sessionFilePath(root, id);
1025
1201
  if (!file) return null;
1026
- writeJson2(file, record);
1202
+ writeJson2(root, file, record);
1027
1203
  upsertManifestEntry(root, record.session);
1028
1204
  return record;
1029
1205
  }
1030
1206
  function setCliSessionId(root, id, cliSessionId) {
1207
+ if (typeof cliSessionId !== "string" || !cliSessionId || cliSessionId.length > 256 || cliSessionId.includes("\0")) {
1208
+ return null;
1209
+ }
1031
1210
  const record = readSession(root, id);
1032
1211
  if (!record) return null;
1033
1212
  record.session.cliSessionId = cliSessionId;
1034
1213
  record.session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
1035
1214
  const file = sessionFilePath(root, id);
1036
1215
  if (!file) return null;
1037
- writeJson2(file, record);
1216
+ writeJson2(root, file, record);
1038
1217
  upsertManifestEntry(root, record.session);
1039
1218
  return record;
1040
1219
  }
@@ -1043,9 +1222,66 @@ function deriveTitle(message) {
1043
1222
  if (!text) return DEFAULT_TITLE;
1044
1223
  return text.length > TITLE_MAX_LENGTH ? `${text.slice(0, TITLE_MAX_LENGTH - 1)}\u2026` : text;
1045
1224
  }
1225
+ function isRecord3(value) {
1226
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
1227
+ }
1228
+ function isValidSession(value, root) {
1229
+ if (!isRecord3(value)) return false;
1230
+ return typeof value.id === "string" && isValidSessionId(value.id) && typeof value.title === "string" && value.title.length > 0 && value.title.length <= TITLE_MAX_LENGTH && typeof value.workspaceRoot === "string" && path4.resolve(value.workspaceRoot) === path4.resolve(root) && typeof value.createdAt === "string" && typeof value.updatedAt === "string" && (value.cliSessionId === void 0 || typeof value.cliSessionId === "string" && value.cliSessionId.length > 0 && value.cliSessionId.length <= 256 && !value.cliSessionId.includes("\0")) && (value.archived === void 0 || typeof value.archived === "boolean");
1231
+ }
1232
+ function isValidRecord(value, root, id) {
1233
+ if (!isRecord3(value) || !isValidSession(value.session, root)) return false;
1234
+ if (value.session.id !== id || !Array.isArray(value.messages)) return false;
1235
+ return value.messages.every((message) => isValidMessage(message, id));
1236
+ }
1237
+ function isValidMessage(value, sessionId) {
1238
+ if (!isRecord3(value)) return false;
1239
+ if (typeof value.id !== "string" || !value.id || value.id.length > 256 || value.sessionId !== sessionId || value.role !== "user" && value.role !== "assistant" && value.role !== "status" || typeof value.createdAt !== "string" || !Array.isArray(value.content)) {
1240
+ return false;
1241
+ }
1242
+ return value.content.every(isValidContentBlock);
1243
+ }
1244
+ function isValidContentBlock(value) {
1245
+ if (!isRecord3(value) || typeof value.type !== "string") return false;
1246
+ if (value.type === "text") return typeof value.text === "string";
1247
+ if (value.type === "tool_use") {
1248
+ return typeof value.id === "string" && typeof value.name === "string" && "input" in value;
1249
+ }
1250
+ if (value.type === "tool_result") {
1251
+ return typeof value.toolUseId === "string" && typeof value.ok === "boolean" && typeof value.summary === "string";
1252
+ }
1253
+ if (value.type === "permission_request") {
1254
+ return typeof value.requestId === "string" && typeof value.toolName === "string" && (value.resolved === void 0 || value.resolved === "allow" || value.resolved === "deny");
1255
+ }
1256
+ return false;
1257
+ }
1046
1258
 
1047
1259
  // src/chat/chatPanel.ts
1260
+ var import_node_crypto3 = require("node:crypto");
1048
1261
  var vscode5 = __toESM(require("vscode"));
1262
+
1263
+ // src/chat/webviewProtocol.ts
1264
+ var MAX_WEBVIEW_PROMPT_LENGTH = 1e6;
1265
+ var MAX_REQUEST_ID_LENGTH = 256;
1266
+ function isWebviewInboundMessage(value) {
1267
+ if (!isRecord4(value) || typeof value.type !== "string") return false;
1268
+ if (value.type === "ready" || value.type === "cancel") return true;
1269
+ if (value.type === "send") {
1270
+ return typeof value.text === "string" && value.text.length <= MAX_WEBVIEW_PROMPT_LENGTH && !value.text.includes("\0");
1271
+ }
1272
+ if (value.type === "permissionDecision") {
1273
+ return typeof value.requestId === "string" && value.requestId.length > 0 && value.requestId.length <= MAX_REQUEST_ID_LENGTH && (value.decision === "allow" || value.decision === "deny");
1274
+ }
1275
+ if (value.type === "removeAttachment") {
1276
+ return Number.isSafeInteger(value.index) && Number(value.index) >= 0 && Number(value.index) <= 1e4;
1277
+ }
1278
+ return false;
1279
+ }
1280
+ function isRecord4(value) {
1281
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
1282
+ }
1283
+
1284
+ // src/chat/chatPanel.ts
1049
1285
  var ChatPanel = class _ChatPanel {
1050
1286
  static current;
1051
1287
  panel;
@@ -1055,7 +1291,9 @@ var ChatPanel = class _ChatPanel {
1055
1291
  this.panel = panel;
1056
1292
  this.panel.webview.html = renderChatHtml(this.panel.webview);
1057
1293
  this.disposables.push(
1058
- this.panel.webview.onDidReceiveMessage((message) => onMessage(message)),
1294
+ this.panel.webview.onDidReceiveMessage((message) => {
1295
+ if (isWebviewInboundMessage(message)) onMessage(message);
1296
+ }),
1059
1297
  this.panel.onDidDispose(() => this.handleDispose())
1060
1298
  );
1061
1299
  }
@@ -1086,10 +1324,7 @@ var ChatPanel = class _ChatPanel {
1086
1324
  }
1087
1325
  };
1088
1326
  function nonce() {
1089
- const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
1090
- let value = "";
1091
- for (let i = 0; i < 32; i++) value += chars.charAt(Math.floor(Math.random() * chars.length));
1092
- return value;
1327
+ return (0, import_node_crypto3.randomBytes)(24).toString("base64url");
1093
1328
  }
1094
1329
  function renderChatHtml(webview) {
1095
1330
  const scriptNonce = nonce();
@@ -1212,22 +1447,22 @@ function renderChatHtml(webview) {
1212
1447
  </style>
1213
1448
  </head>
1214
1449
  <body>
1215
- <div id="banner"></div>
1216
- <div id="messages">
1450
+ <div id="banner" role="alert" aria-live="assertive"></div>
1451
+ <div id="messages" role="log" aria-live="polite" aria-relevant="additions" aria-label="UR chat messages">
1217
1452
  <div id="empty-state">Ask UR about this workspace. Use <code>UR: Add Selection to Chat</code> or <code>UR: Add Current File to Chat</code> to attach code first.</div>
1218
1453
  </div>
1219
- <div id="permission-prompt">
1220
- <div><strong>UR wants to use <span id="permission-tool"></span></strong></div>
1454
+ <div id="permission-prompt" role="alertdialog" aria-modal="true" aria-labelledby="permission-title">
1455
+ <div id="permission-title"><strong>UR wants to use <span id="permission-tool"></span></strong></div>
1221
1456
  <div class="input-preview" id="permission-input"></div>
1222
1457
  <div class="actions">
1223
1458
  <button id="permission-allow">Allow</button>
1224
1459
  <button id="permission-deny" class="secondary">Deny</button>
1225
1460
  </div>
1226
1461
  </div>
1227
- <div id="attachments"></div>
1228
- <div id="status-line"></div>
1462
+ <div id="attachments" aria-label="Attached editor context"></div>
1463
+ <div id="status-line" role="status" aria-live="polite"></div>
1229
1464
  <form id="composer">
1230
- <textarea id="input" placeholder="Message UR\u2026" rows="2"></textarea>
1465
+ <textarea id="input" aria-label="Message UR" maxlength="1000000" placeholder="Message UR\u2026" rows="2"></textarea>
1231
1466
  <button id="send" type="submit">Send</button>
1232
1467
  <button id="cancel" type="button" class="secondary" hidden>Cancel</button>
1233
1468
  </form>
@@ -1246,6 +1481,8 @@ function renderChatHtml(webview) {
1246
1481
  const inputEl = document.getElementById('input');
1247
1482
  const sendButton = document.getElementById('send');
1248
1483
  const cancelButton = document.getElementById('cancel');
1484
+ const permissionAllowButton = document.getElementById('permission-allow');
1485
+ const permissionDenyButton = document.getElementById('permission-deny');
1249
1486
 
1250
1487
  let currentStatus = 'idle';
1251
1488
  let pendingPermissionRequestId = null;
@@ -1312,6 +1549,8 @@ function renderChatHtml(webview) {
1312
1549
  const remove = document.createElement('button');
1313
1550
  remove.type = 'button';
1314
1551
  remove.textContent = '\\u00d7';
1552
+ remove.title = 'Remove ' + attachment.label;
1553
+ remove.setAttribute('aria-label', 'Remove ' + attachment.label);
1315
1554
  remove.addEventListener('click', function () {
1316
1555
  vscode.postMessage({ type: 'removeAttachment', index: index });
1317
1556
  });
@@ -1337,9 +1576,15 @@ function renderChatHtml(webview) {
1337
1576
  bannerEl.classList.add('visible');
1338
1577
  }
1339
1578
 
1579
+ function hideBanner() {
1580
+ bannerEl.textContent = '';
1581
+ bannerEl.classList.remove('visible');
1582
+ }
1583
+
1340
1584
  window.addEventListener('message', function (event) {
1341
1585
  const message = event.data;
1342
1586
  if (message.type === 'init') {
1587
+ hideBanner();
1343
1588
  renderAll(message.messages);
1344
1589
  renderAttachments(message.attachments);
1345
1590
  applyStatus(message.status);
@@ -1349,9 +1594,12 @@ function renderChatHtml(webview) {
1349
1594
  applyStatus(message.status);
1350
1595
  } else if (message.type === 'permissionRequest') {
1351
1596
  pendingPermissionRequestId = message.requestId;
1597
+ permissionAllowButton.disabled = false;
1598
+ permissionDenyButton.disabled = false;
1352
1599
  permissionToolEl.textContent = message.toolName;
1353
1600
  permissionInputEl.textContent = JSON.stringify(message.input, null, 2);
1354
1601
  permissionEl.classList.add('visible');
1602
+ permissionDenyButton.focus();
1355
1603
  } else if (message.type === 'permissionResolved') {
1356
1604
  if (pendingPermissionRequestId === message.requestId) {
1357
1605
  pendingPermissionRequestId = null;
@@ -1370,6 +1618,7 @@ function renderChatHtml(webview) {
1370
1618
  event.preventDefault();
1371
1619
  const text = inputEl.value.trim();
1372
1620
  if (!text || currentStatus === 'running') return;
1621
+ hideBanner();
1373
1622
  vscode.postMessage({ type: 'send', text: text });
1374
1623
  inputEl.value = '';
1375
1624
  });
@@ -1385,12 +1634,16 @@ function renderChatHtml(webview) {
1385
1634
  vscode.postMessage({ type: 'cancel' });
1386
1635
  });
1387
1636
 
1388
- document.getElementById('permission-allow').addEventListener('click', function () {
1637
+ permissionAllowButton.addEventListener('click', function () {
1389
1638
  if (!pendingPermissionRequestId) return;
1639
+ permissionAllowButton.disabled = true;
1640
+ permissionDenyButton.disabled = true;
1390
1641
  vscode.postMessage({ type: 'permissionDecision', requestId: pendingPermissionRequestId, decision: 'allow' });
1391
1642
  });
1392
- document.getElementById('permission-deny').addEventListener('click', function () {
1643
+ permissionDenyButton.addEventListener('click', function () {
1393
1644
  if (!pendingPermissionRequestId) return;
1645
+ permissionAllowButton.disabled = true;
1646
+ permissionDenyButton.disabled = true;
1394
1647
  vscode.postMessage({ type: 'permissionDecision', requestId: pendingPermissionRequestId, decision: 'deny' });
1395
1648
  });
1396
1649
 
@@ -1649,7 +1902,7 @@ var ChatController = class {
1649
1902
  this.ensurePanel();
1650
1903
  const sessionId = this.record.session.id;
1651
1904
  const userMessage = {
1652
- id: (0, import_node_crypto2.randomUUID)(),
1905
+ id: (0, import_node_crypto4.randomUUID)(),
1653
1906
  sessionId,
1654
1907
  role: "user",
1655
1908
  content: [{ type: "text", text: promptText }],
@@ -1708,7 +1961,7 @@ var ChatController = class {
1708
1961
  }
1709
1962
  appendChatMessage(root, sessionId, role, content) {
1710
1963
  if (!this.record || this.record.session.id !== sessionId) return;
1711
- const message = { id: (0, import_node_crypto2.randomUUID)(), sessionId, role, content, createdAt: (/* @__PURE__ */ new Date()).toISOString() };
1964
+ const message = { id: (0, import_node_crypto4.randomUUID)(), sessionId, role, content, createdAt: (/* @__PURE__ */ new Date()).toISOString() };
1712
1965
  appendMessage(root, sessionId, message);
1713
1966
  this.record.messages.push(message);
1714
1967
  this.panel?.post({ type: "messageAppended", message });
@@ -1721,10 +1974,10 @@ var ChatController = class {
1721
1974
  if (turnId !== this.activeTurnId) {
1722
1975
  return Promise.resolve({ behavior: "deny", message: "This chat turn is no longer active." });
1723
1976
  }
1724
- return new Promise((resolve3) => {
1977
+ return new Promise((resolve4) => {
1725
1978
  const toolName = request.request.tool_name ?? "tool";
1726
1979
  const input = request.request.input ?? {};
1727
- this.pendingPermissions.set(request.request_id, { resolve: resolve3, toolName, input, turnId });
1980
+ this.pendingPermissions.set(request.request_id, { resolve: resolve4, toolName, input, turnId });
1728
1981
  this.panel?.post({ type: "permissionRequest", requestId: request.request_id, toolName, input });
1729
1982
  });
1730
1983
  }
@@ -1881,7 +2134,7 @@ var ChatTreeProvider = class {
1881
2134
 
1882
2135
  // src/diffs/actions.ts
1883
2136
  var import_node_child_process4 = require("node:child_process");
1884
- var fs3 = __toESM(require("node:fs"));
2137
+ var fs4 = __toESM(require("node:fs"));
1885
2138
  var import_node_util2 = require("node:util");
1886
2139
  var vscode8 = __toESM(require("vscode"));
1887
2140
  var execFileAsync2 = (0, import_node_util2.promisify)(import_node_child_process4.execFile);
@@ -1921,7 +2174,7 @@ async function applyDiff(item, provider) {
1921
2174
  return;
1922
2175
  }
1923
2176
  const patch = patchPath(root, bundle);
1924
- if (!fs3.existsSync(patch)) {
2177
+ if (!fs4.existsSync(patch)) {
1925
2178
  vscode8.window.showErrorMessage(`UR patch file missing for ${bundle.id}.`);
1926
2179
  return;
1927
2180
  }
@@ -2053,7 +2306,7 @@ async function openDiff(item) {
2053
2306
 
2054
2307
  // src/model/modelPicker.ts
2055
2308
  var vscode10 = __toESM(require("vscode"));
2056
- function isRecord2(value) {
2309
+ function isRecord5(value) {
2057
2310
  return typeof value === "object" && value !== null;
2058
2311
  }
2059
2312
  function parseProviderList(raw) {
@@ -2061,7 +2314,7 @@ function parseProviderList(raw) {
2061
2314
  const data = JSON.parse(raw);
2062
2315
  if (!Array.isArray(data)) return [];
2063
2316
  return data.flatMap((entry) => {
2064
- if (!isRecord2(entry) || typeof entry.id !== "string" || typeof entry.name !== "string") return [];
2317
+ if (!isRecord5(entry) || typeof entry.id !== "string" || typeof entry.name !== "string") return [];
2065
2318
  return [
2066
2319
  {
2067
2320
  id: entry.id,
@@ -2079,14 +2332,14 @@ function parseProviderList(raw) {
2079
2332
  function parseProviderModels(raw) {
2080
2333
  try {
2081
2334
  const data = JSON.parse(raw);
2082
- if (!isRecord2(data) || typeof data.provider !== "string" || !Array.isArray(data.models)) return void 0;
2335
+ if (!isRecord5(data) || typeof data.provider !== "string" || !Array.isArray(data.models)) return void 0;
2083
2336
  const source = data.source === "live" || data.source === "cache" || data.source === "static" ? data.source : "static";
2084
2337
  return {
2085
2338
  provider: data.provider,
2086
2339
  source,
2087
2340
  warning: typeof data.warning === "string" ? data.warning : void 0,
2088
2341
  models: data.models.flatMap((model) => {
2089
- if (!isRecord2(model) || typeof model.id !== "string") return [];
2342
+ if (!isRecord5(model) || typeof model.id !== "string") return [];
2090
2343
  return [
2091
2344
  {
2092
2345
  id: model.id,
@@ -2104,7 +2357,7 @@ function parseProviderModels(raw) {
2104
2357
  function parseProviderStatus(raw) {
2105
2358
  try {
2106
2359
  const data = JSON.parse(raw);
2107
- if (!isRecord2(data)) return {};
2360
+ if (!isRecord5(data)) return {};
2108
2361
  return {
2109
2362
  provider: typeof data.provider === "string" ? data.provider : void 0,
2110
2363
  model: typeof data.model === "string" ? data.model : void 0
@@ -2116,8 +2369,8 @@ function parseProviderStatus(raw) {
2116
2369
  function parseIdeStatus(raw) {
2117
2370
  try {
2118
2371
  const data = JSON.parse(raw);
2119
- if (!isRecord2(data)) return {};
2120
- const provider = isRecord2(data.provider) ? data.provider : {};
2372
+ if (!isRecord5(data)) return {};
2373
+ const provider = isRecord5(data.provider) ? data.provider : {};
2121
2374
  return {
2122
2375
  model: typeof provider.model === "string" ? provider.model : void 0
2123
2376
  };
@@ -2333,7 +2586,7 @@ function deriveMultimodalSupport(providerId) {
2333
2586
  // src/options/providerOptionsLoader.ts
2334
2587
  var PROVIDER_KINDS = ["ur-native", "subscription-cli", "subscription-placeholder"];
2335
2588
  var ACCESS_TYPES = ["subscription", "api", "local", "server"];
2336
- function isRecord3(value) {
2589
+ function isRecord6(value) {
2337
2590
  return typeof value === "object" && value !== null;
2338
2591
  }
2339
2592
  function parseProviderListJson(raw) {
@@ -2346,7 +2599,7 @@ function parseProviderListJson(raw) {
2346
2599
  if (!Array.isArray(data)) return [];
2347
2600
  const options = [];
2348
2601
  for (const entry of data) {
2349
- if (!isRecord3(entry)) continue;
2602
+ if (!isRecord6(entry)) continue;
2350
2603
  if (typeof entry.id !== "string" || typeof entry.name !== "string") continue;
2351
2604
  const providerKind = PROVIDER_KINDS.includes(entry.providerKind) ? entry.providerKind : "subscription-placeholder";
2352
2605
  const accessType = ACCESS_TYPES.includes(entry.accessType) ? entry.accessType : "api";
@@ -2590,13 +2843,13 @@ async function showSearchActions() {
2590
2843
  var vscode15 = __toESM(require("vscode"));
2591
2844
 
2592
2845
  // src/status/statusData.ts
2593
- function isRecord4(value) {
2846
+ function isRecord7(value) {
2594
2847
  return typeof value === "object" && value !== null;
2595
2848
  }
2596
2849
  function safeParseRecord(raw) {
2597
2850
  try {
2598
2851
  const parsed = JSON.parse(raw);
2599
- return isRecord4(parsed) ? parsed : {};
2852
+ return isRecord7(parsed) ? parsed : {};
2600
2853
  } catch {
2601
2854
  return {};
2602
2855
  }
@@ -2609,8 +2862,8 @@ function asKnownBoolean(value) {
2609
2862
  }
2610
2863
  function parseIdeStatusJson(raw, fallbackWorkspaceRoot = "") {
2611
2864
  const data = safeParseRecord(raw);
2612
- const acpRaw = isRecord4(data.acp) ? data.acp : {};
2613
- const providerRaw = isRecord4(data.provider) ? data.provider : {};
2865
+ const acpRaw = isRecord7(data.acp) ? data.acp : {};
2866
+ const providerRaw = isRecord7(data.provider) ? data.provider : {};
2614
2867
  return {
2615
2868
  workspaceRoot: typeof data.workspaceRoot === "string" && data.workspaceRoot ? data.workspaceRoot : fallbackWorkspaceRoot,
2616
2869
  acp: {