u-foo 2.5.6 → 2.5.7

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.
@@ -2,55 +2,6 @@ const { loadConfig } = require("../../config");
2
2
  const path = require("path");
3
3
  const fs = require("fs");
4
4
 
5
- function bundledModuleRoots() {
6
- return [
7
- path.join(__dirname, ".."),
8
- ];
9
- }
10
-
11
- function resolveFirstExisting(paths = []) {
12
- for (const candidate of paths) {
13
- if (!candidate) continue;
14
- try {
15
- if (fs.existsSync(candidate)) return candidate;
16
- } catch {
17
- // ignore
18
- }
19
- }
20
- return "";
21
- }
22
-
23
- function defaultBundledCoreRoot() {
24
- const root = path.join(__dirname, "..");
25
- const agentEntry = path.join(root, "agent.js");
26
- if (resolveFirstExisting([agentEntry])) return root;
27
- return root;
28
- }
29
-
30
- function defaultBundledPromptFile() {
31
- const moduleRoots = bundledModuleRoots();
32
- const candidates = moduleRoots.map((root) => path.join(root, "UCODE_PROMPT.md"));
33
- return resolveFirstExisting(candidates) || candidates[0];
34
- }
35
-
36
- function isWindowsPlatform() {
37
- return process.platform === "win32";
38
- }
39
-
40
- function canExecutePath(filePath = "") {
41
- const target = String(filePath || "").trim();
42
- if (!target) return false;
43
- try {
44
- const stat = fs.statSync(target);
45
- if (!stat.isFile()) return false;
46
- if (isWindowsPlatform()) return true;
47
- fs.accessSync(target, fs.constants.X_OK);
48
- return true;
49
- } catch {
50
- return false;
51
- }
52
- }
53
-
54
5
  function isReadableFile(filePath = "") {
55
6
  const target = String(filePath || "").trim();
56
7
  if (!target) return false;
@@ -61,113 +12,6 @@ function isReadableFile(filePath = "") {
61
12
  }
62
13
  }
63
14
 
64
- function resolveExecutableFromPath(command = "", env = process.env) {
65
- const text = String(command || "").trim();
66
- if (!text) return "";
67
- if (path.isAbsolute(text) || text.includes("/") || text.includes("\\")) {
68
- return canExecutePath(text) ? path.resolve(text) : "";
69
- }
70
-
71
- const pathText = String((env && env.PATH) || process.env.PATH || "").trim();
72
- if (!pathText) return "";
73
- const dirs = pathText.split(path.delimiter).map((item) => String(item || "").trim()).filter(Boolean);
74
- if (dirs.length === 0) return "";
75
-
76
- const hasExplicitExt = /\.[a-zA-Z0-9]+$/.test(text);
77
- const exts = isWindowsPlatform()
78
- ? String((env && env.PATHEXT) || process.env.PATHEXT || ".EXE;.CMD;.BAT;.COM")
79
- .split(";")
80
- .map((item) => item.trim())
81
- .filter(Boolean)
82
- : [""];
83
- const suffixes = hasExplicitExt ? [""] : exts;
84
-
85
- for (const dir of dirs) {
86
- for (const ext of suffixes) {
87
- const candidate = path.join(dir, `${text}${ext}`);
88
- if (canExecutePath(candidate)) return candidate;
89
- }
90
- }
91
- return "";
92
- }
93
-
94
- function tokenizeCommand(raw = "") {
95
- const text = String(raw || "");
96
- const tokens = [];
97
- let current = "";
98
- let quote = "";
99
-
100
- for (let i = 0; i < text.length; i += 1) {
101
- const ch = text[i];
102
- if (quote) {
103
- if (quote === "\"") {
104
- if (ch === "\\") {
105
- if (i + 1 < text.length) {
106
- const next = text[i + 1];
107
- if (next === "\"" || next === "\\") {
108
- current += next;
109
- i += 1;
110
- continue;
111
- }
112
- }
113
- current += "\\";
114
- continue;
115
- }
116
- } else if (quote === "'" && ch === "\\") {
117
- current += "\\";
118
- continue;
119
- }
120
- if (ch === quote) {
121
- quote = "";
122
- continue;
123
- }
124
- current += ch;
125
- continue;
126
- }
127
-
128
- if (ch === "'" || ch === "\"") {
129
- quote = ch;
130
- continue;
131
- }
132
- if (ch === "\\") {
133
- if (i + 1 < text.length) {
134
- const next = text[i + 1];
135
- if (/\s/.test(next) || next === "'" || next === "\"" || next === "\\") {
136
- current += next;
137
- i += 1;
138
- } else {
139
- current += "\\";
140
- }
141
- } else {
142
- current += "\\";
143
- }
144
- continue;
145
- }
146
- if (/\s/.test(ch)) {
147
- if (current) {
148
- tokens.push(current);
149
- current = "";
150
- }
151
- continue;
152
- }
153
- current += ch;
154
- }
155
-
156
- if (quote) {
157
- return String(raw || "").trim().split(/\s+/).filter(Boolean);
158
- }
159
- if (current) tokens.push(current);
160
- return tokens;
161
- }
162
-
163
- function splitCommand(raw, fallback = "pi") {
164
- const text = String(raw || "").trim();
165
- if (!text) return { command: fallback, args: [] };
166
- const parts = tokenizeCommand(text);
167
- if (parts.length === 0) return { command: fallback, args: [] };
168
- return { command: parts[0], args: parts.slice(1) };
169
- }
170
-
171
15
  function hasAnyArg(args = [], names = []) {
172
16
  if (!Array.isArray(args) || args.length === 0) return false;
173
17
  const flags = new Set((Array.isArray(names) ? names : []).filter(Boolean));
@@ -182,19 +26,6 @@ function hasAnyArg(args = [], names = []) {
182
26
  });
183
27
  }
184
28
 
185
- function pickBinEntry(binField = {}) {
186
- if (typeof binField === "string" && binField.trim()) {
187
- return binField.trim();
188
- }
189
- if (!binField || typeof binField !== "object") return "";
190
- const entries = Object.entries(binField)
191
- .filter(([, value]) => typeof value === "string" && value.trim());
192
- if (entries.length === 0) return "";
193
- const preferred = entries.find(([name]) => /^(ucode|core|cli)$/i.test(String(name)));
194
- if (preferred) return preferred[1].trim();
195
- return entries[0][1].trim();
196
- }
197
-
198
29
  function normalizeAppendSystemPromptMode(value = "") {
199
30
  const text = String(value || "").trim().toLowerCase();
200
31
  if (text === "always" || text === "force" || text === "on" || text === "1" || text === "true") return "always";
@@ -202,20 +33,6 @@ function normalizeAppendSystemPromptMode(value = "") {
202
33
  return "auto";
203
34
  }
204
35
 
205
- function isLikelyPiCoreCommand(command = "", args = []) {
206
- const cmdText = String(command || "").trim();
207
- const cmdBase = path.basename(cmdText).toLowerCase();
208
- if (cmdBase === "ucode" || cmdBase === "ucode.exe") return true;
209
-
210
- const joined = [cmdText, ...(Array.isArray(args) ? args : [])]
211
- .map((part) => String(part || "").toLowerCase())
212
- .join(" ");
213
- if (!joined) return false;
214
- if (joined.includes("/src/code/agent.js")) return true;
215
- if (joined.includes("\\src\\code\\agent.js")) return true;
216
- return false;
217
- }
218
-
219
36
  function readLastArgValue(args = [], flag = "") {
220
37
  if (!Array.isArray(args) || !flag) return "";
221
38
  let value = "";
@@ -238,54 +55,6 @@ function readLastArgValue(args = [], flag = "") {
238
55
  return value;
239
56
  }
240
57
 
241
- function resolveCoreFromPath(coreRoot = "") {
242
- const requestedRoot = String(coreRoot || "").trim();
243
- if (!requestedRoot) return null;
244
- let stat;
245
- try {
246
- stat = fs.statSync(requestedRoot);
247
- } catch {
248
- return null;
249
- }
250
- if (!stat.isDirectory()) return null;
251
-
252
- const candidates = [
253
- requestedRoot,
254
- path.join(requestedRoot, "packages", "coding-agent"),
255
- ];
256
-
257
- for (const root of candidates) {
258
- const packageFile = path.join(root, "package.json");
259
- let pkg = null;
260
- try {
261
- pkg = JSON.parse(fs.readFileSync(packageFile, "utf8"));
262
- } catch {
263
- continue;
264
- }
265
- const binRel = pickBinEntry(pkg && pkg.bin ? pkg.bin : {});
266
- if (!binRel) continue;
267
- const binAbs = path.resolve(root, binRel);
268
- if (!fs.existsSync(binAbs)) continue;
269
- return {
270
- command: process.execPath,
271
- args: [binAbs],
272
- root,
273
- };
274
- }
275
- return null;
276
- }
277
-
278
- function resolveCandidateCoreRoot({
279
- env = process.env,
280
- config = {},
281
- } = {}) {
282
- // Native-only mode: external pi-mono path is no longer used as launch fallback.
283
- // Keep function for compatibility with older diagnostic surfaces.
284
- void env;
285
- void config;
286
- return null;
287
- }
288
-
289
58
  function resolveNativeFallbackCommand({ env = process.env } = {}) {
290
59
  void env;
291
60
  const entry = path.resolve(__dirname, "..", "agent.js");
@@ -361,16 +130,10 @@ function resolveUcodeLaunch({
361
130
  || config.ucodeAppendSystemPromptMode
362
131
  || "auto"
363
132
  );
133
+ // Native-only mode: the bundled native core always supports
134
+ // --append-system-prompt, so only mode=never suppresses injection.
364
135
  const hasSystemPromptArg = hasAnyArg(finalArgs, ["--system-prompt", "--append-system-prompt"]);
365
- const appendSupported = appendSystemPromptMode === "always"
366
- || (
367
- appendSystemPromptMode === "auto"
368
- && (
369
- nativeCore.kind === "native"
370
- || isLikelyPiCoreCommand(command, finalArgs)
371
- )
372
- );
373
- if (!hasSystemPromptArg && appendSystemPrompt && appendSystemPromptMode !== "never" && appendSupported) {
136
+ if (!hasSystemPromptArg && appendSystemPrompt && appendSystemPromptMode !== "never") {
374
137
  finalArgs.push("--append-system-prompt", appendSystemPrompt);
375
138
  }
376
139
  const effectiveProvider = readLastArgValue(finalArgs, "--provider");
@@ -398,20 +161,9 @@ function resolveUcodeLaunch({
398
161
  }
399
162
 
400
163
  module.exports = {
401
- bundledModuleRoots,
402
- defaultBundledCoreRoot,
403
- defaultBundledPromptFile,
404
- tokenizeCommand,
405
- splitCommand,
406
164
  hasAnyArg,
407
- pickBinEntry,
408
165
  normalizeAppendSystemPromptMode,
409
- isLikelyPiCoreCommand,
410
166
  readLastArgValue,
411
- resolveCoreFromPath,
412
- resolveCandidateCoreRoot,
413
- canExecutePath,
414
- resolveExecutableFromPath,
415
167
  resolveNativeFallbackCommand,
416
168
  resolveUcodeLaunch,
417
169
  };
@@ -86,16 +86,32 @@ function mergeDefaultUfooProtocolPrompt(projectRoot = "", promptText = "") {
86
86
  return [defaultPrompt, currentPrompt].filter(Boolean).join("\n\n");
87
87
  }
88
88
 
89
+ function isPathInsideRoot(root = "", target = "") {
90
+ const relative = path.relative(path.resolve(root), path.resolve(target));
91
+ return Boolean(relative) && !relative.startsWith("..") && !path.isAbsolute(relative);
92
+ }
93
+
89
94
  function prepareUcodeBootstrap({
90
95
  projectRoot = process.cwd(),
91
96
  promptFile = "",
92
97
  promptText = "",
93
98
  targetFile = "",
94
99
  includeDefaultProtocol = true,
100
+ allowOutsideProjectRoot = false,
95
101
  } = {}) {
96
102
  const resolvedProjectRoot = path.resolve(projectRoot);
97
103
  const resolvedPrompt = String(promptFile || "").trim();
98
- const resolvedTarget = String(targetFile || "").trim() || defaultBootstrapPath(resolvedProjectRoot);
104
+ const resolvedTarget = path.resolve(
105
+ resolvedProjectRoot,
106
+ String(targetFile || "").trim() || defaultBootstrapPath(resolvedProjectRoot)
107
+ );
108
+ // The target may come from the project config (.ufoo/config.json), which is
109
+ // attacker-controlled in a cloned repository. Refuse to write outside the
110
+ // project root unless the caller marked the target as user-trusted (e.g.
111
+ // the user's own UFOO_UCODE_BOOTSTRAP_FILE env var).
112
+ if (!allowOutsideProjectRoot && !isPathInsideRoot(resolvedProjectRoot, resolvedTarget)) {
113
+ throw new Error(`ucode bootstrap target must stay inside the project root: ${resolvedTarget}`);
114
+ }
99
115
 
100
116
  const inlinePromptText = String(promptText || "").trim();
101
117
  const resolvedPromptText = inlinePromptText || readFileSafe(resolvedPrompt);
@@ -129,5 +145,6 @@ module.exports = {
129
145
  resolveProjectRules,
130
146
  defaultBootstrapPath,
131
147
  buildBootstrapContent,
148
+ isPathInsideRoot,
132
149
  prepareUcodeBootstrap,
133
150
  };
@@ -27,15 +27,12 @@ function inspectUcodeBuildSetup({
27
27
  const workspaceRoot = root;
28
28
  const distCliPath = Array.isArray(native.args) && native.args[0] ? path.resolve(native.args[0]) : "";
29
29
  const distCliExists = Boolean(distCliPath && fs.existsSync(distCliPath));
30
- const nodeModulesPath = "";
31
30
  return {
32
31
  projectRoot: root,
33
32
  coreRoot,
34
33
  workspaceRoot,
35
34
  distCliPath,
36
35
  distCliExists,
37
- nodeModulesPath,
38
- nodeModulesExists: Boolean(nodeModulesPath && fs.existsSync(nodeModulesPath)),
39
36
  };
40
37
  }
41
38
 
@@ -131,6 +131,9 @@ function formatUcodeDoctor(result = {}) {
131
131
  }
132
132
  lines.push(`prompt: ${result.promptFile || "(none)"}${result.promptFile && !result.promptExists ? " (missing)" : ""}`);
133
133
  lines.push(`bootstrap: ${result.bootstrapFile || "(none)"}`);
134
+ if (result.bootstrapPrepared && result.bootstrapPrepared.ok === false) {
135
+ lines.push(` warning: bootstrap preparation failed: ${result.bootstrapPrepared.error || "unknown error"}`);
136
+ }
134
137
  if (result.build && result.build.coreRoot) {
135
138
  lines.push(`build: ${result.build.distCliExists ? "ready" : "missing dist"}`);
136
139
  lines.push(` core root: ${result.build.coreRoot}`);
@@ -165,15 +168,27 @@ function prepareAndInspectUcode({
165
168
  loadConfigImpl = loadConfig,
166
169
  } = {}) {
167
170
  const inspection = inspectUcodeSetup({ projectRoot, env, loadConfigImpl });
168
- const prepared = prepareUcodeBootstrap({
169
- projectRoot: inspection.projectRoot,
170
- promptFile: inspection.promptFile,
171
- targetFile: inspection.bootstrapFile,
172
- // The native core already injects the ufoo protocol via the modular
173
- // prompt (src/agents/prompts/native/ufoo.js); inlining it into the
174
- // bootstrap file would append the same protocol text a second time.
175
- includeDefaultProtocol: false,
176
- });
171
+ let prepared;
172
+ try {
173
+ prepared = prepareUcodeBootstrap({
174
+ projectRoot: inspection.projectRoot,
175
+ promptFile: inspection.promptFile,
176
+ targetFile: inspection.bootstrapFile,
177
+ // The user's own env var is trusted; a bootstrap path coming from the
178
+ // project config must stay inside the project root.
179
+ allowOutsideProjectRoot: Boolean(String(env.UFOO_UCODE_BOOTSTRAP_FILE || "").trim()),
180
+ // The native core already injects the ufoo protocol via the modular
181
+ // prompt (src/agents/prompts/native/ufoo.js); inlining it into the
182
+ // bootstrap file would append the same protocol text a second time.
183
+ includeDefaultProtocol: false,
184
+ });
185
+ } catch (err) {
186
+ prepared = {
187
+ ok: false,
188
+ file: inspection.bootstrapFile,
189
+ error: err && err.message ? err.message : "failed to prepare bootstrap",
190
+ };
191
+ }
177
192
  return {
178
193
  ...inspection,
179
194
  bootstrapPrepared: prepared,
@@ -1,5 +1,6 @@
1
1
  const fs = require("fs");
2
2
  const path = require("path");
3
+ const { randomUUID } = require("crypto");
3
4
  const { loadGlobalUcodeConfig } = require("../../config");
4
5
 
5
6
  function readJson(filePath = "", fallback = {}) {
@@ -14,10 +15,17 @@ function readJson(filePath = "", fallback = {}) {
14
15
  }
15
16
  }
16
17
 
17
- function writeJson(filePath = "", data = {}) {
18
+ function writeJson(filePath = "", data = {}, { mode = 0 } = {}) {
18
19
  if (!filePath) return;
19
20
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
20
- fs.writeFileSync(filePath, `${JSON.stringify(data, null, 2)}\n`, "utf8");
21
+ // Write to a temp file and rename so a crash mid-write cannot leave a
22
+ // truncated JSON file behind.
23
+ const tmpFile = `${filePath}.${process.pid}-${randomUUID()}.tmp`;
24
+ fs.writeFileSync(tmpFile, `${JSON.stringify(data, null, 2)}\n`, "utf8");
25
+ fs.renameSync(tmpFile, filePath);
26
+ if (mode) {
27
+ fs.chmodSync(filePath, mode);
28
+ }
21
29
  }
22
30
 
23
31
  function resolveRuntimeValues({
@@ -103,7 +111,8 @@ function prepareUcodeRuntimeConfig({
103
111
  type: "api_key",
104
112
  key: inspection.apiKey,
105
113
  };
106
- writeJson(inspection.authFile, auth);
114
+ // auth.json carries API keys: keep it readable only by the owner.
115
+ writeJson(inspection.authFile, auth, { mode: 0o600 });
107
116
  }
108
117
 
109
118
  if (inspection.provider && inspection.baseUrl) {