u-foo 2.5.5 → 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.
- package/bin/ucode.js +9 -0
- package/package.json +1 -1
- package/src/agents/launch/notifier.js +6 -0
- package/src/agents/launch/ptyRunner.js +2 -2
- package/src/agents/launch/ptyWrapper.js +2 -2
- package/src/agents/prompts/native/index.js +2 -2
- package/src/agents/prompts/native/toolDescriptions/bash.js +1 -1
- package/src/agents/prompts/native/toolDescriptions/edit.js +1 -0
- package/src/agents/prompts/native/toolDescriptions/read.js +3 -2
- package/src/code/agent.js +77 -1086
- package/src/code/busConsumer.js +504 -0
- package/src/code/dispatch.js +1 -6
- package/src/code/launcher/ucode.js +8 -254
- package/src/code/launcher/ucodeBootstrap.js +18 -1
- package/src/code/launcher/ucodeBuild.js +0 -3
- package/src/code/launcher/ucodeDoctor.js +26 -8
- package/src/code/launcher/ucodeRuntimeConfig.js +12 -3
- package/src/code/nativeRunner.js +93 -113
- package/src/code/repl.js +610 -0
- package/src/code/sessionStore.js +5 -1
- package/src/code/skills/injection.js +17 -1
- package/src/code/taskDecomposer.js +47 -31
- package/src/code/tools/bash.js +19 -2
- package/src/code/tools/common.js +34 -0
- package/src/code/tools/edit.js +11 -3
- package/src/code/tools/read.js +20 -2
- package/src/coordination/bus/inject.js +52 -7
- package/src/coordination/bus/subscriber.js +33 -6
- package/src/runtime/daemon/deliveryScheduler.js +102 -2
- package/src/runtime/daemon/index.js +8 -1
- package/src/runtime/daemon/ops.js +23 -0
|
@@ -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 = "";
|
|
@@ -224,8 +41,10 @@ function readLastArgValue(args = [], flag = "") {
|
|
|
224
41
|
if (!item) continue;
|
|
225
42
|
if (item === flag) {
|
|
226
43
|
const next = String(args[i + 1] || "").trim();
|
|
227
|
-
if (next
|
|
228
|
-
|
|
44
|
+
if (next && !next.startsWith("--")) {
|
|
45
|
+
value = next;
|
|
46
|
+
i += 1;
|
|
47
|
+
}
|
|
229
48
|
continue;
|
|
230
49
|
}
|
|
231
50
|
if (item.startsWith(`${flag}=`)) {
|
|
@@ -236,54 +55,6 @@ function readLastArgValue(args = [], flag = "") {
|
|
|
236
55
|
return value;
|
|
237
56
|
}
|
|
238
57
|
|
|
239
|
-
function resolveCoreFromPath(coreRoot = "") {
|
|
240
|
-
const requestedRoot = String(coreRoot || "").trim();
|
|
241
|
-
if (!requestedRoot) return null;
|
|
242
|
-
let stat;
|
|
243
|
-
try {
|
|
244
|
-
stat = fs.statSync(requestedRoot);
|
|
245
|
-
} catch {
|
|
246
|
-
return null;
|
|
247
|
-
}
|
|
248
|
-
if (!stat.isDirectory()) return null;
|
|
249
|
-
|
|
250
|
-
const candidates = [
|
|
251
|
-
requestedRoot,
|
|
252
|
-
path.join(requestedRoot, "packages", "coding-agent"),
|
|
253
|
-
];
|
|
254
|
-
|
|
255
|
-
for (const root of candidates) {
|
|
256
|
-
const packageFile = path.join(root, "package.json");
|
|
257
|
-
let pkg = null;
|
|
258
|
-
try {
|
|
259
|
-
pkg = JSON.parse(fs.readFileSync(packageFile, "utf8"));
|
|
260
|
-
} catch {
|
|
261
|
-
continue;
|
|
262
|
-
}
|
|
263
|
-
const binRel = pickBinEntry(pkg && pkg.bin ? pkg.bin : {});
|
|
264
|
-
if (!binRel) continue;
|
|
265
|
-
const binAbs = path.resolve(root, binRel);
|
|
266
|
-
if (!fs.existsSync(binAbs)) continue;
|
|
267
|
-
return {
|
|
268
|
-
command: process.execPath,
|
|
269
|
-
args: [binAbs],
|
|
270
|
-
root,
|
|
271
|
-
};
|
|
272
|
-
}
|
|
273
|
-
return null;
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function resolveCandidateCoreRoot({
|
|
277
|
-
env = process.env,
|
|
278
|
-
config = {},
|
|
279
|
-
} = {}) {
|
|
280
|
-
// Native-only mode: external pi-mono path is no longer used as launch fallback.
|
|
281
|
-
// Keep function for compatibility with older diagnostic surfaces.
|
|
282
|
-
void env;
|
|
283
|
-
void config;
|
|
284
|
-
return null;
|
|
285
|
-
}
|
|
286
|
-
|
|
287
58
|
function resolveNativeFallbackCommand({ env = process.env } = {}) {
|
|
288
59
|
void env;
|
|
289
60
|
const entry = path.resolve(__dirname, "..", "agent.js");
|
|
@@ -342,7 +113,7 @@ function resolveUcodeLaunch({
|
|
|
342
113
|
const promptFile = String(
|
|
343
114
|
env.UFOO_UCODE_PROMPT_FILE
|
|
344
115
|
|| config.ucodePromptFile
|
|
345
|
-
||
|
|
116
|
+
|| ""
|
|
346
117
|
).trim();
|
|
347
118
|
const bootstrapFile = String(
|
|
348
119
|
env.UFOO_UCODE_BOOTSTRAP_FILE
|
|
@@ -359,16 +130,10 @@ function resolveUcodeLaunch({
|
|
|
359
130
|
|| config.ucodeAppendSystemPromptMode
|
|
360
131
|
|| "auto"
|
|
361
132
|
);
|
|
133
|
+
// Native-only mode: the bundled native core always supports
|
|
134
|
+
// --append-system-prompt, so only mode=never suppresses injection.
|
|
362
135
|
const hasSystemPromptArg = hasAnyArg(finalArgs, ["--system-prompt", "--append-system-prompt"]);
|
|
363
|
-
|
|
364
|
-
|| (
|
|
365
|
-
appendSystemPromptMode === "auto"
|
|
366
|
-
&& (
|
|
367
|
-
nativeCore.kind === "native"
|
|
368
|
-
|| isLikelyPiCoreCommand(command, finalArgs)
|
|
369
|
-
)
|
|
370
|
-
);
|
|
371
|
-
if (!hasSystemPromptArg && appendSystemPrompt && appendSystemPromptMode !== "never" && appendSupported) {
|
|
136
|
+
if (!hasSystemPromptArg && appendSystemPrompt && appendSystemPromptMode !== "never") {
|
|
372
137
|
finalArgs.push("--append-system-prompt", appendSystemPrompt);
|
|
373
138
|
}
|
|
374
139
|
const effectiveProvider = readLastArgValue(finalArgs, "--provider");
|
|
@@ -396,20 +161,9 @@ function resolveUcodeLaunch({
|
|
|
396
161
|
}
|
|
397
162
|
|
|
398
163
|
module.exports = {
|
|
399
|
-
bundledModuleRoots,
|
|
400
|
-
defaultBundledCoreRoot,
|
|
401
|
-
defaultBundledPromptFile,
|
|
402
|
-
tokenizeCommand,
|
|
403
|
-
splitCommand,
|
|
404
164
|
hasAnyArg,
|
|
405
|
-
pickBinEntry,
|
|
406
165
|
normalizeAppendSystemPromptMode,
|
|
407
|
-
isLikelyPiCoreCommand,
|
|
408
166
|
readLastArgValue,
|
|
409
|
-
resolveCoreFromPath,
|
|
410
|
-
resolveCandidateCoreRoot,
|
|
411
|
-
canExecutePath,
|
|
412
|
-
resolveExecutableFromPath,
|
|
413
167
|
resolveNativeFallbackCommand,
|
|
414
168
|
resolveUcodeLaunch,
|
|
415
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 =
|
|
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
|
|
|
@@ -3,7 +3,6 @@ const path = require("path");
|
|
|
3
3
|
const { loadConfig } = require("../../config");
|
|
4
4
|
const {
|
|
5
5
|
resolveNativeFallbackCommand,
|
|
6
|
-
defaultBundledPromptFile,
|
|
7
6
|
} = require("./ucode");
|
|
8
7
|
const { inspectUcodeBuildSetup } = require("./ucodeBuild");
|
|
9
8
|
const { inspectUcodeRuntimeConfig } = require("./ucodeRuntimeConfig");
|
|
@@ -30,7 +29,7 @@ function inspectUcodeSetup({
|
|
|
30
29
|
const promptFile = String(
|
|
31
30
|
env.UFOO_UCODE_PROMPT_FILE
|
|
32
31
|
|| config.ucodePromptFile
|
|
33
|
-
||
|
|
32
|
+
|| ""
|
|
34
33
|
).trim();
|
|
35
34
|
const bootstrapFile = String(
|
|
36
35
|
env.UFOO_UCODE_BOOTSTRAP_FILE
|
|
@@ -130,8 +129,11 @@ function formatUcodeDoctor(result = {}) {
|
|
|
130
129
|
if (result.configuredCommand) {
|
|
131
130
|
lines.push(`configured command override (ignored in native-only mode): ${result.configuredCommand}`);
|
|
132
131
|
}
|
|
133
|
-
lines.push(`prompt: ${result.promptFile || "(none)"}${result.promptExists ? "" : "
|
|
132
|
+
lines.push(`prompt: ${result.promptFile || "(none)"}${result.promptFile && !result.promptExists ? " (missing)" : ""}`);
|
|
134
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
|
+
}
|
|
135
137
|
if (result.build && result.build.coreRoot) {
|
|
136
138
|
lines.push(`build: ${result.build.distCliExists ? "ready" : "missing dist"}`);
|
|
137
139
|
lines.push(` core root: ${result.build.coreRoot}`);
|
|
@@ -166,11 +168,27 @@ function prepareAndInspectUcode({
|
|
|
166
168
|
loadConfigImpl = loadConfig,
|
|
167
169
|
} = {}) {
|
|
168
170
|
const inspection = inspectUcodeSetup({ projectRoot, env, loadConfigImpl });
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
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
|
+
}
|
|
174
192
|
return {
|
|
175
193
|
...inspection,
|
|
176
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
|
-
|
|
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
|
-
|
|
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) {
|