agentlas 1.0.65 → 1.0.67
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/CHANGELOG.md +94 -0
- package/README.md +12 -4
- package/bin/agentlas.cjs +354 -16
- package/engine/acp/server.cjs +34 -3
- package/engine/agentlas-api-agent.cjs +151 -120
- package/engine/agentlas-banner.cjs +6 -2
- package/engine/agentlas-capabilities.cjs +17 -10
- package/engine/agentlas-cloud-runtime.cjs +4 -5
- package/engine/agentlas-config.cjs +19 -54
- package/engine/agentlas-core-harness.cjs +59 -5
- package/engine/agentlas-desktop-loadout.cjs +17 -3
- package/engine/agentlas-evolution.cjs +75 -18
- package/engine/agentlas-experience-exchange.cjs +145 -14
- package/engine/agentlas-experience-intake.cjs +2 -2
- package/engine/agentlas-i18n.cjs +0 -2
- package/engine/agentlas-input.cjs +273 -20
- package/engine/agentlas-judgment.cjs +0 -0
- package/engine/agentlas-mcp-env.cjs +84 -23
- package/engine/agentlas-mcp-wrapper.cjs +36 -4
- package/engine/agentlas-memory-governance.cjs +2 -1
- package/engine/agentlas-memory-import.cjs +49 -12
- package/engine/agentlas-native-host.cjs +279 -44
- package/engine/agentlas-onboard.cjs +23 -1
- package/engine/agentlas-permissions.cjs +5 -1
- package/engine/agentlas-sqlite-policy.cjs +40 -2
- package/engine/agentlas-tasks.cjs +7 -0
- package/engine/agentlas-tools.cjs +37 -10
- package/engine/agentlas-ui.cjs +26 -4
- package/engine/agentlas-workforce.cjs +30 -5
- package/engine/agentlas-workload-routing.cjs +83 -10
- package/engine/agentlas.cjs +21 -4
- package/engine/agents/builder.cjs +35 -10
- package/engine/agents/files.cjs +81 -10
- package/engine/agents/import-local.cjs +10 -2
- package/engine/agents/registry.cjs +10 -3
- package/engine/agents/router.cjs +7 -7
- package/engine/agents/routes.cjs +31 -9
- package/engine/automation/daemon.cjs +154 -40
- package/engine/automation/launchd.cjs +395 -16
- package/engine/automation/schedule.cjs +90 -3
- package/engine/automation/store.cjs +163 -59
- package/engine/browser/cdp.cjs +190 -17
- package/engine/browser/vault.cjs +1 -0
- package/engine/cli-output.cjs +96 -30
- package/engine/cloud/auth.cjs +131 -13
- package/engine/cloud/hub-client.cjs +41 -16
- package/engine/cloud-assets/cas.cjs +18 -9
- package/engine/cloud-assets/commands.cjs +108 -30
- package/engine/cloud-assets/package.cjs +66 -3
- package/engine/cloud-assets/restore.cjs +12 -9
- package/engine/cloud-assets/state.cjs +609 -116
- package/engine/commands/acp.cjs +13 -2
- package/engine/commands/automation.cjs +75 -24
- package/engine/commands/billing.cjs +6 -1
- package/engine/commands/browser.cjs +61 -29
- package/engine/commands/build.cjs +72 -14
- package/engine/commands/call.cjs +1 -1
- package/engine/commands/cd.cjs +1 -1
- package/engine/commands/cloud.cjs +2 -1
- package/engine/commands/connect.cjs +38 -13
- package/engine/commands/creds.cjs +74 -29
- package/engine/commands/doctor.cjs +17 -1
- package/engine/commands/document.cjs +38 -10
- package/engine/commands/env.cjs +6 -1
- package/engine/commands/firm.cjs +42 -12
- package/engine/commands/graph.cjs +36 -32
- package/engine/commands/help.cjs +11 -1
- package/engine/commands/hep.cjs +1 -1
- package/engine/commands/import.cjs +5 -4
- package/engine/commands/index.cjs +1 -1
- package/engine/commands/install.cjs +6 -4
- package/engine/commands/list.cjs +56 -16
- package/engine/commands/login.cjs +5 -0
- package/engine/commands/logout.cjs +6 -1
- package/engine/commands/mcp.cjs +150 -37
- package/engine/commands/memory.cjs +3 -2
- package/engine/commands/multimodal.cjs +12 -0
- package/engine/commands/native.cjs +5 -1
- package/engine/commands/one.cjs +78 -33
- package/engine/commands/plugin.cjs +13 -2
- package/engine/commands/project.cjs +15 -12
- package/engine/commands/research.cjs +18 -7
- package/engine/commands/roles.cjs +104 -28
- package/engine/commands/route.cjs +2 -2
- package/engine/commands/run.cjs +42 -9
- package/engine/commands/search.cjs +23 -2
- package/engine/commands/setup.cjs +6 -1
- package/engine/commands/storm.cjs +44 -6
- package/engine/commands/swarm.cjs +47 -9
- package/engine/commands/telegram.cjs +6 -1
- package/engine/commands/uninstall.cjs +12 -6
- package/engine/commands/update.cjs +5 -0
- package/engine/commands/upload.cjs +2 -1
- package/engine/commands/usage.cjs +6 -1
- package/engine/commands/version.cjs +6 -1
- package/engine/commands/whoami.cjs +6 -1
- package/engine/commands/workforce.cjs +60 -16
- package/engine/core/capability-grants.cjs +7 -2
- package/engine/core/daemon-client.cjs +36 -7
- package/engine/core/db.cjs +10 -2
- package/engine/core/desktop-core-fetch.cjs +477 -77
- package/engine/core/schema-ensure.cjs +23 -11
- package/engine/experience/build.cjs +5 -1
- package/engine/experience/intents.cjs +35 -82
- package/engine/experience/runtime.cjs +7 -1
- package/engine/experience/variant.cjs +58 -4
- package/engine/firms/orchestrate.cjs +9 -6
- package/engine/graph/ask-model.cjs +22 -11
- package/engine/graph/interview.cjs +74 -23
- package/engine/graph/package.cjs +63 -8
- package/engine/hephaestus/local-core.cjs +18 -5
- package/engine/hephaestus/runtime.cjs +48 -43
- package/engine/hub/install.cjs +484 -63
- package/engine/hub/plugins.cjs +391 -29
- package/engine/mcp/consent.cjs +25 -17
- package/engine/mcp/contract.cjs +229 -24
- package/engine/mcp/inventory.cjs +46 -10
- package/engine/mcp/plan.cjs +10 -1
- package/engine/mcp/probe.cjs +24 -9
- package/engine/memory-cli/curate.cjs +74 -16
- package/engine/oberon/common.cjs +37 -12
- package/engine/oberon/manifest.cjs +25 -2
- package/engine/oberon/outputs.cjs +49 -19
- package/engine/oberon/render.cjs +665 -40
- package/engine/project/career-graph.cjs +15 -2
- package/engine/project/controller.cjs +133 -15
- package/engine/project/credentials.cjs +382 -34
- package/engine/project/env-file.cjs +93 -8
- package/engine/project/memory-context.cjs +11 -57
- package/engine/project/ontology.cjs +66 -25
- package/engine/project/paths.cjs +42 -8
- package/engine/project/seed.cjs +88 -51
- package/engine/project/state.cjs +41 -16
- package/engine/project/team.cjs +26 -1
- package/engine/runtime-refusal.cjs +4 -1
- package/engine/runtimes/acp-driver.cjs +56 -9
- package/engine/runtimes/auth-evidence.cjs +21 -2
- package/engine/runtimes/detect.cjs +28 -8
- package/engine/runtimes/kinds.cjs +12 -4
- package/engine/runtimes/overrides.cjs +4 -3
- package/engine/runtimes/resolve.cjs +8 -7
- package/engine/runtimes/roles.cjs +7 -5
- package/engine/sessions/apply-fences.cjs +396 -56
- package/engine/sessions/fences.cjs +111 -27
- package/engine/sessions/memory-turn.cjs +62 -3
- package/engine/sessions/orchestrator.cjs +11 -1
- package/engine/sessions/prompt.cjs +66 -9
- package/engine/sessions/session.cjs +70 -10
- package/engine/sessions/sink.cjs +78 -12
- package/engine/sessions/store.cjs +29 -7
- package/engine/storm/storm.cjs +26 -3
- package/engine/storm/swarm.cjs +31 -3
- package/engine/telegram/connect.cjs +185 -22
- package/engine/ui/commands-catalog.cjs +1 -1
- package/engine/ui/palette.cjs +12 -2
- package/engine/ui/repl.cjs +57 -9
- package/engine/ui/screens.cjs +2 -4
- package/engine/vendor/desktop-core.manifest.json +5 -5
- package/engine/workforce/capture.cjs +9 -27
- package/engine/workforce/deps.cjs +18 -6
- package/engine/workforce/local-core-transport.cjs +68 -5
- package/package.json +1 -1
- package/engine/commands/career-graph.cjs +0 -51
|
@@ -10,9 +10,257 @@
|
|
|
10
10
|
*/
|
|
11
11
|
const fs = require("node:fs");
|
|
12
12
|
const path = require("node:path");
|
|
13
|
+
const crypto = require("node:crypto");
|
|
13
14
|
const { loadArch } = require("../core/db.cjs");
|
|
14
15
|
const { runCwd } = require("./paths.cjs");
|
|
15
16
|
|
|
17
|
+
const MAX_MANAGED_CREDENTIAL_METADATA_BYTES = 1024 * 1024;
|
|
18
|
+
const MAX_CREDENTIAL_FILE_BYTES = 16 * 1024 * 1024;
|
|
19
|
+
const CREDENTIAL_NOFOLLOW = fs.constants.O_NOFOLLOW || 0;
|
|
20
|
+
const credentialDestinationBindings = new Map();
|
|
21
|
+
|
|
22
|
+
function credentialSameDirectoryIdentity(left, right) {
|
|
23
|
+
return Boolean(
|
|
24
|
+
left && right && left.isDirectory() && right.isDirectory() &&
|
|
25
|
+
!left.isSymbolicLink() && !right.isSymbolicLink() &&
|
|
26
|
+
left.dev === right.dev && left.ino === right.ino,
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function credentialDirectoryAnchor(target, label, containedBy = null) {
|
|
31
|
+
const stat = fs.lstatSync(target);
|
|
32
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error(`${label} must be a real managed directory`);
|
|
33
|
+
const realpath = fs.realpathSync.native(target);
|
|
34
|
+
if (containedBy && !(
|
|
35
|
+
realpath === containedBy.realpath || realpath.startsWith(`${containedBy.realpath}${path.sep}`)
|
|
36
|
+
)) throw new Error(`${label} escaped the managed project`);
|
|
37
|
+
return { path: target, realpath, dev: stat.dev, ino: stat.ino, stat };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function credentialAssertDirectoryAnchor(anchor, label, containedBy = null) {
|
|
41
|
+
const current = credentialDirectoryAnchor(anchor.path, label, containedBy);
|
|
42
|
+
if (!credentialSameDirectoryIdentity(anchor.stat || anchor, current.stat || current) || current.realpath !== anchor.realpath) {
|
|
43
|
+
throw new Error(`${label} changed while it was being used`);
|
|
44
|
+
}
|
|
45
|
+
return current;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function credentialSameFileIdentity(left, right) {
|
|
49
|
+
return Boolean(
|
|
50
|
+
left && right && left.isFile() && right.isFile() &&
|
|
51
|
+
!left.isSymbolicLink() && !right.isSymbolicLink() &&
|
|
52
|
+
left.dev === right.dev && left.ino === right.ino,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function credentialSameFileSnapshot(left, right) {
|
|
57
|
+
return credentialSameFileIdentity(left, right) && left.nlink === right.nlink &&
|
|
58
|
+
left.size === right.size && left.mtimeMs === right.mtimeMs && left.ctimeMs === right.ctimeMs;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function credentialFileSnapshot(file, label, { allowMissing = false, allowHardLinks = false } = {}) {
|
|
62
|
+
let stat;
|
|
63
|
+
try { stat = fs.lstatSync(file); }
|
|
64
|
+
catch (error) {
|
|
65
|
+
if (allowMissing && error && error.code === "ENOENT") return null;
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
if (!stat.isFile() || stat.isSymbolicLink() || (!allowHardLinks && stat.nlink !== 1) || stat.size > MAX_CREDENTIAL_FILE_BYTES) {
|
|
69
|
+
throw new Error(`${label} must be a bounded regular non-symbolic-link file`);
|
|
70
|
+
}
|
|
71
|
+
return stat;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function credentialRemoveOwnedFile(file, expected, { allowLinked = false } = {}) {
|
|
75
|
+
try {
|
|
76
|
+
const current = fs.lstatSync(file);
|
|
77
|
+
if (credentialSameFileIdentity(current, expected) && (current.nlink === 1 || (allowLinked && current.nlink >= 2))) {
|
|
78
|
+
fs.unlinkSync(file);
|
|
79
|
+
}
|
|
80
|
+
} catch { /* leave unknown successors and recovery artifacts untouched */ }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function credentialRestoreBackup(backup, target, expected, parent) {
|
|
84
|
+
try {
|
|
85
|
+
credentialAssertDirectoryAnchor(parent, "credential destination directory");
|
|
86
|
+
if (credentialFileSnapshot(target, "credential destination successor", { allowMissing: true })) return false;
|
|
87
|
+
const current = credentialFileSnapshot(backup, "credential destination backup");
|
|
88
|
+
if (!current || !credentialSameFileIdentity(current, expected) || current.nlink !== 1) return false;
|
|
89
|
+
fs.linkSync(backup, target);
|
|
90
|
+
const restored = credentialFileSnapshot(target, "credential destination restored", { allowHardLinks: true });
|
|
91
|
+
if (!restored || !credentialSameFileIdentity(restored, expected) || restored.nlink < 2) return false;
|
|
92
|
+
fs.unlinkSync(backup);
|
|
93
|
+
return true;
|
|
94
|
+
} catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function credentialPublishFile(parent, targetName, temporary, expected) {
|
|
100
|
+
credentialAssertDirectoryAnchor(parent, "credential destination directory");
|
|
101
|
+
const target = path.join(parent.realpath, targetName);
|
|
102
|
+
const current = credentialFileSnapshot(target, "credential destination", { allowMissing: true });
|
|
103
|
+
if ((expected && (!current || !credentialSameFileSnapshot(current, expected))) || (!expected && current)) {
|
|
104
|
+
throw new Error("credential destination changed before replacement");
|
|
105
|
+
}
|
|
106
|
+
let backup = null;
|
|
107
|
+
let linked = false;
|
|
108
|
+
try {
|
|
109
|
+
if (expected) {
|
|
110
|
+
backup = `${target}.previous-${process.pid}-${crypto.randomUUID()}`;
|
|
111
|
+
fs.renameSync(target, backup);
|
|
112
|
+
const moved = credentialFileSnapshot(backup, "credential destination backup");
|
|
113
|
+
if (!moved || !credentialSameFileIdentity(moved, expected) || moved.nlink !== 1) {
|
|
114
|
+
credentialRestoreBackup(backup, target, expected, parent);
|
|
115
|
+
throw new Error("credential destination changed before replacement");
|
|
116
|
+
}
|
|
117
|
+
credentialAssertDirectoryAnchor(parent, "credential destination directory");
|
|
118
|
+
if (credentialFileSnapshot(target, "credential destination successor", { allowMissing: true })) {
|
|
119
|
+
throw new Error("credential destination successor appeared during replacement");
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
credentialAssertDirectoryAnchor(parent, "credential destination directory");
|
|
123
|
+
if (credentialFileSnapshot(target, "credential destination successor", { allowMissing: true })) {
|
|
124
|
+
throw new Error("credential destination successor appeared during replacement");
|
|
125
|
+
}
|
|
126
|
+
fs.linkSync(temporary.path, target);
|
|
127
|
+
linked = true;
|
|
128
|
+
const linkedTarget = credentialFileSnapshot(target, "credential destination", { allowHardLinks: true });
|
|
129
|
+
if (!linkedTarget || !credentialSameFileIdentity(linkedTarget, temporary.stat) || linkedTarget.nlink < 2) {
|
|
130
|
+
throw new Error("credential publication produced an unsafe target");
|
|
131
|
+
}
|
|
132
|
+
credentialAssertDirectoryAnchor(parent, "credential destination directory");
|
|
133
|
+
credentialRemoveOwnedFile(temporary.path, temporary.stat, { allowLinked: true });
|
|
134
|
+
const installed = credentialFileSnapshot(target, "credential destination");
|
|
135
|
+
if (!installed || !credentialSameFileIdentity(installed, temporary.stat) || installed.nlink !== 1) {
|
|
136
|
+
throw new Error("credential publication identity changed");
|
|
137
|
+
}
|
|
138
|
+
try { fs.chmodSync(target, 0o600); } catch { /* Windows/best-effort */ }
|
|
139
|
+
const final = credentialFileSnapshot(target, "credential destination");
|
|
140
|
+
if (!final || !credentialSameFileIdentity(final, temporary.stat) || final.nlink !== 1 ||
|
|
141
|
+
(process.platform !== "win32" && (final.mode & 0o777) !== 0o600)) {
|
|
142
|
+
throw new Error("credential publication mode or identity changed");
|
|
143
|
+
}
|
|
144
|
+
credentialAssertDirectoryAnchor(parent, "credential destination directory");
|
|
145
|
+
if (backup) {
|
|
146
|
+
const backupStat = credentialFileSnapshot(backup, "credential destination backup", { allowMissing: true });
|
|
147
|
+
if (backupStat && credentialSameFileIdentity(backupStat, expected) && backupStat.nlink === 1) fs.unlinkSync(backup);
|
|
148
|
+
}
|
|
149
|
+
return target;
|
|
150
|
+
} catch (error) {
|
|
151
|
+
if (linked) credentialRemoveOwnedFile(target, temporary.stat);
|
|
152
|
+
credentialRemoveOwnedFile(temporary.path, temporary.stat);
|
|
153
|
+
if (backup) credentialRestoreBackup(backup, target, expected, parent);
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function credentialProjectRootCli(projectPath) {
|
|
159
|
+
const requested = path.resolve(projectPath || "");
|
|
160
|
+
const root = fs.realpathSync.native(requested);
|
|
161
|
+
const stat = fs.statSync(root);
|
|
162
|
+
if (!stat.isDirectory()) throw new Error("credential project must be a real directory");
|
|
163
|
+
return root;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function ensureManagedDirectoryCli(root, relativePath, mode = 0o700) {
|
|
167
|
+
let current = root;
|
|
168
|
+
for (const segment of String(relativePath || "").split(/[\\/]+/).filter(Boolean)) {
|
|
169
|
+
current = path.join(current, segment);
|
|
170
|
+
try {
|
|
171
|
+
const stat = fs.lstatSync(current);
|
|
172
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("credential store directories must not be symbolic links");
|
|
173
|
+
} catch (error) {
|
|
174
|
+
if (!error || error.code !== "ENOENT") throw error;
|
|
175
|
+
fs.mkdirSync(current, { recursive: false, mode });
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return current;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function readManagedTextSnapshotCli(filePath, maxBytes = MAX_MANAGED_CREDENTIAL_METADATA_BYTES) {
|
|
182
|
+
let before;
|
|
183
|
+
try { before = fs.lstatSync(filePath); }
|
|
184
|
+
catch (error) {
|
|
185
|
+
if (error && error.code === "ENOENT") return { exists: false, text: "", mode: 0o600 };
|
|
186
|
+
throw error;
|
|
187
|
+
}
|
|
188
|
+
if (!before.isFile() || before.isSymbolicLink() || before.size > maxBytes) {
|
|
189
|
+
throw new Error("credential store file must be a bounded regular non-symbolic-link file");
|
|
190
|
+
}
|
|
191
|
+
const fd = fs.openSync(filePath, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
|
|
192
|
+
try {
|
|
193
|
+
const opened = fs.fstatSync(fd);
|
|
194
|
+
if (!opened.isFile() || opened.size > maxBytes || opened.dev !== before.dev || opened.ino !== before.ino) {
|
|
195
|
+
throw new Error("credential store file changed while opening");
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
exists: true,
|
|
199
|
+
text: fs.readFileSync(fd, "utf8"),
|
|
200
|
+
mode: before.mode & 0o777,
|
|
201
|
+
stat: before,
|
|
202
|
+
};
|
|
203
|
+
} finally {
|
|
204
|
+
fs.closeSync(fd);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function assertManagedSnapshotUnchangedCli(filePath, snapshot) {
|
|
209
|
+
let current;
|
|
210
|
+
try { current = fs.lstatSync(filePath); }
|
|
211
|
+
catch (error) {
|
|
212
|
+
if (error && error.code === "ENOENT" && !snapshot.exists) return;
|
|
213
|
+
throw new Error("credential store file changed before replacement");
|
|
214
|
+
}
|
|
215
|
+
if (
|
|
216
|
+
!snapshot.exists || !current.isFile() || current.isSymbolicLink() || !snapshot.stat ||
|
|
217
|
+
current.dev !== snapshot.stat.dev || current.ino !== snapshot.stat.ino ||
|
|
218
|
+
current.size !== snapshot.stat.size || current.mtimeMs !== snapshot.stat.mtimeMs
|
|
219
|
+
) throw new Error("credential store file changed before replacement");
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function replaceManagedFileCli(tempPath, filePath, snapshot) {
|
|
223
|
+
assertManagedSnapshotUnchangedCli(filePath, snapshot);
|
|
224
|
+
try {
|
|
225
|
+
fs.renameSync(tempPath, filePath);
|
|
226
|
+
return;
|
|
227
|
+
} catch (error) {
|
|
228
|
+
if (
|
|
229
|
+
process.platform !== "win32" || !snapshot.exists ||
|
|
230
|
+
!["EEXIST", "EPERM", "EACCES"].includes(error && error.code)
|
|
231
|
+
) throw error;
|
|
232
|
+
}
|
|
233
|
+
assertManagedSnapshotUnchangedCli(filePath, snapshot);
|
|
234
|
+
const backup = `${filePath}.agentlas-${process.pid}-${crypto.randomUUID()}.bak`;
|
|
235
|
+
fs.renameSync(filePath, backup);
|
|
236
|
+
try {
|
|
237
|
+
fs.renameSync(tempPath, filePath);
|
|
238
|
+
} catch (error) {
|
|
239
|
+
try { if (!fs.existsSync(filePath)) fs.renameSync(backup, filePath); } catch { /* leave recoverable backup */ }
|
|
240
|
+
throw error;
|
|
241
|
+
}
|
|
242
|
+
try { fs.rmSync(backup, { force: true }); } catch { /* committed target is authoritative */ }
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function writeManagedTextAtomicCli(filePath, text, options = {}) {
|
|
246
|
+
const snapshot = options.snapshot || readManagedTextSnapshotCli(filePath, options.maxBytes);
|
|
247
|
+
const bytes = Buffer.byteLength(text, "utf8");
|
|
248
|
+
if (bytes > (options.maxBytes || MAX_MANAGED_CREDENTIAL_METADATA_BYTES)) throw new Error("credential store file exceeds its safety limit");
|
|
249
|
+
const temp = path.join(path.dirname(filePath), `.${path.basename(filePath)}.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
250
|
+
try {
|
|
251
|
+
fs.writeFileSync(temp, text, { encoding: "utf8", mode: options.mode || snapshot.mode || 0o600, flag: "wx" });
|
|
252
|
+
replaceManagedFileCli(temp, filePath, snapshot);
|
|
253
|
+
} finally {
|
|
254
|
+
try { fs.rmSync(temp, { force: true }); } catch { /* best-effort cleanup */ }
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function ensureManagedTextFileCli(filePath, content, mode = 0o600) {
|
|
259
|
+
const snapshot = readManagedTextSnapshotCli(filePath);
|
|
260
|
+
if (!snapshot.exists) writeManagedTextAtomicCli(filePath, content, { snapshot, mode });
|
|
261
|
+
return snapshot.exists ? snapshot.text : content;
|
|
262
|
+
}
|
|
263
|
+
|
|
16
264
|
function localCredentialConfigCli(arch) {
|
|
17
265
|
return {
|
|
18
266
|
mapFile: arch.localCredentialsMapFile || "local-credentials.map.json",
|
|
@@ -94,24 +342,24 @@ ${credentialIndexSectionContentCli(arch)}
|
|
|
94
342
|
`;
|
|
95
343
|
}
|
|
96
344
|
function ensureSoulCredentialIndexCli(projectPath, projectName, arch) {
|
|
345
|
+
const root = credentialProjectRootCli(projectPath);
|
|
97
346
|
const memoryDir = (arch && arch.memoryDir) || ".agentlas";
|
|
98
347
|
const soulFile = (arch && arch.soulFile) || "project-soul-memory.md";
|
|
99
|
-
const dir =
|
|
348
|
+
const dir = ensureManagedDirectoryCli(root, memoryDir);
|
|
100
349
|
const soul = path.join(dir, soulFile);
|
|
101
|
-
|
|
102
|
-
if (!
|
|
103
|
-
|
|
350
|
+
const snapshot = readManagedTextSnapshotCli(soul);
|
|
351
|
+
if (!snapshot.exists) {
|
|
352
|
+
writeManagedTextAtomicCli(soul, projectSoulTemplateCli(projectName, arch), { snapshot, mode: 0o600 });
|
|
104
353
|
return soul;
|
|
105
354
|
}
|
|
106
|
-
|
|
107
|
-
try { content = fs.readFileSync(soul, "utf8"); } catch { content = ""; }
|
|
355
|
+
const content = snapshot.text;
|
|
108
356
|
if (!content.includes(CREDENTIAL_INDEX_SECTION_CLI)) {
|
|
109
357
|
const section = credentialIndexSectionContentCli(arch);
|
|
110
358
|
const marker = "\n## Project Purpose";
|
|
111
359
|
const next = content.includes(marker)
|
|
112
360
|
? content.replace(marker, `\n${section}\n## Project Purpose`)
|
|
113
361
|
: `${content.trimEnd()}\n\n${section}\n`;
|
|
114
|
-
|
|
362
|
+
writeManagedTextAtomicCli(soul, next.endsWith("\n") ? next : next + "\n", { snapshot, mode: 0o600 });
|
|
115
363
|
}
|
|
116
364
|
return soul;
|
|
117
365
|
}
|
|
@@ -157,7 +405,8 @@ Do not commit files from this folder.
|
|
|
157
405
|
`;
|
|
158
406
|
}
|
|
159
407
|
function ensureAgentlasCredentialIgnoreCli(projectPath, cfg) {
|
|
160
|
-
const
|
|
408
|
+
const root = credentialProjectRootCli(projectPath);
|
|
409
|
+
const gitignorePath = path.join(root, ".gitignore");
|
|
161
410
|
const marker = "# Agentlas local credentials";
|
|
162
411
|
const block = `${marker}
|
|
163
412
|
.env
|
|
@@ -171,45 +420,62 @@ ${cfg.credentialsDir}/*
|
|
|
171
420
|
!${cfg.credentialsDir}/
|
|
172
421
|
!${cfg.credentialsDir}/${cfg.readmeFile}
|
|
173
422
|
`;
|
|
174
|
-
|
|
175
|
-
|
|
423
|
+
const snapshot = readManagedTextSnapshotCli(gitignorePath);
|
|
424
|
+
const existing = snapshot.text;
|
|
176
425
|
if (existing.includes(marker)) {
|
|
177
|
-
if (!/^\._\*$/m.test(existing))
|
|
426
|
+
if (!/^\._\*$/m.test(existing)) {
|
|
427
|
+
writeManagedTextAtomicCli(gitignorePath, `${existing.trimEnd()}\n._*\n`, { snapshot, mode: snapshot.mode || 0o644 });
|
|
428
|
+
}
|
|
178
429
|
return;
|
|
179
430
|
}
|
|
180
431
|
const next = existing.trimEnd() ? `${existing.trimEnd()}\n\n${block}` : block;
|
|
181
|
-
|
|
432
|
+
writeManagedTextAtomicCli(gitignorePath, next.endsWith("\n") ? next : next + "\n", {
|
|
433
|
+
snapshot,
|
|
434
|
+
mode: snapshot.exists ? snapshot.mode : 0o644,
|
|
435
|
+
});
|
|
182
436
|
}
|
|
183
437
|
function ensureLocalCredentialStoreCli(projectPath, projectName, arch) {
|
|
438
|
+
const root = credentialProjectRootCli(projectPath);
|
|
184
439
|
const cfg = localCredentialConfigCli(arch || loadArch());
|
|
185
|
-
const dir =
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
if (!fs.existsSync(credentialsReadme)) fs.writeFileSync(credentialsReadme, credentialsReadmeContentCli(cfg), "utf8");
|
|
440
|
+
const dir = ensureManagedDirectoryCli(root, (arch && arch.memoryDir) || ".agentlas");
|
|
441
|
+
const signingDir = ensureManagedDirectoryCli(root, cfg.signingDir);
|
|
442
|
+
const credentialsDir = ensureManagedDirectoryCli(root, cfg.credentialsDir);
|
|
443
|
+
const envExample = path.join(root, cfg.envExampleFile);
|
|
444
|
+
ensureManagedTextFileCli(envExample, envExampleContentCli(cfg), 0o600);
|
|
445
|
+
const signingReadme = path.join(signingDir, cfg.readmeFile);
|
|
446
|
+
ensureManagedTextFileCli(signingReadme, signingReadmeContentCli(cfg), 0o600);
|
|
447
|
+
const credentialsReadme = path.join(credentialsDir, cfg.readmeFile);
|
|
448
|
+
ensureManagedTextFileCli(credentialsReadme, credentialsReadmeContentCli(cfg), 0o600);
|
|
195
449
|
const mapPath = path.join(dir, cfg.mapFile);
|
|
196
|
-
|
|
197
|
-
|
|
450
|
+
const mapText = ensureManagedTextFileCli(
|
|
451
|
+
mapPath,
|
|
452
|
+
JSON.stringify(localCredentialsMapSkeletonCli(root, projectName, cfg), null, 2) + "\n",
|
|
453
|
+
0o600,
|
|
454
|
+
);
|
|
455
|
+
let mapValue;
|
|
456
|
+
try { mapValue = JSON.parse(mapText); }
|
|
457
|
+
catch (error) { throw new Error(`credential map is invalid JSON: ${String((error && error.message) || error)}`); }
|
|
458
|
+
if (!mapValue || typeof mapValue !== "object" || Array.isArray(mapValue)) {
|
|
459
|
+
throw new Error("credential map must contain a JSON object");
|
|
198
460
|
}
|
|
199
|
-
ensureAgentlasCredentialIgnoreCli(
|
|
200
|
-
return { cfg, mapPath };
|
|
461
|
+
ensureAgentlasCredentialIgnoreCli(root, cfg);
|
|
462
|
+
return { cfg, mapPath, projectRoot: root };
|
|
201
463
|
}
|
|
202
|
-
function readJsonObjectCli(file, fallback) {
|
|
464
|
+
function readJsonObjectCli(file, fallback, options = {}) {
|
|
203
465
|
try {
|
|
204
|
-
const parsed = JSON.parse(
|
|
205
|
-
|
|
206
|
-
|
|
466
|
+
const parsed = JSON.parse(readManagedTextSnapshotCli(file).text);
|
|
467
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
|
|
468
|
+
if (options.strict) throw new Error("credential map must contain a JSON object");
|
|
469
|
+
return fallback;
|
|
470
|
+
} catch (error) {
|
|
471
|
+
if (options.strict) throw new Error(`credential map is unreadable or invalid: ${String((error && error.message) || error)}`);
|
|
207
472
|
return fallback;
|
|
208
473
|
}
|
|
209
474
|
}
|
|
210
475
|
function upsertLocalCredentialMapCli(projectPath, projectName, arch, entry) {
|
|
211
|
-
const { cfg, mapPath } = ensureLocalCredentialStoreCli(projectPath, projectName, arch || loadArch());
|
|
212
|
-
const
|
|
476
|
+
const { cfg, mapPath, projectRoot } = ensureLocalCredentialStoreCli(projectPath, projectName, arch || loadArch());
|
|
477
|
+
const snapshot = readManagedTextSnapshotCli(mapPath);
|
|
478
|
+
const data = readJsonObjectCli(mapPath, localCredentialsMapSkeletonCli(projectPath, projectName, cfg), { strict: true });
|
|
213
479
|
const now = new Date().toISOString();
|
|
214
480
|
data.updatedAt = now;
|
|
215
481
|
if (!Array.isArray(data.entries)) data.entries = [];
|
|
@@ -230,17 +496,96 @@ function upsertLocalCredentialMapCli(projectPath, projectName, arch, entry) {
|
|
|
230
496
|
const idx = data.entries.findIndex((row) => row && row.id === id);
|
|
231
497
|
if (idx >= 0) data.entries[idx] = { ...data.entries[idx], ...clean };
|
|
232
498
|
else data.entries.push(clean);
|
|
233
|
-
|
|
499
|
+
data.projectRoot = projectRoot;
|
|
500
|
+
writeManagedTextAtomicCli(mapPath, JSON.stringify(data, null, 2) + "\n", { snapshot, mode: 0o600 });
|
|
234
501
|
}
|
|
235
502
|
// v1은 fail()로 즉시 종료했다 — v2 규칙은 throw → 명령이 ctx.err + return 1 로 변환.
|
|
236
503
|
function safeCredentialDestRelCli(destRel) {
|
|
237
504
|
const rel = path.normalize(String(destRel || "")).replace(/\\/g, "/");
|
|
238
|
-
if (!rel || path.isAbsolute(rel) || rel === "." || rel.startsWith("../") || rel
|
|
505
|
+
if (!rel || path.isAbsolute(rel) || rel === "." || rel.startsWith("../") || /[\u0000-\u001f\u007f]/.test(rel)) {
|
|
239
506
|
throw new Error("credential destination must be a relative path inside the project");
|
|
240
507
|
}
|
|
241
508
|
return rel;
|
|
242
509
|
}
|
|
243
510
|
|
|
511
|
+
function resolveCredentialDestinationCli(projectPath, destRel) {
|
|
512
|
+
const root = credentialProjectRootCli(projectPath);
|
|
513
|
+
const rel = safeCredentialDestRelCli(destRel);
|
|
514
|
+
const rootAnchor = credentialDirectoryAnchor(root, "credential project root");
|
|
515
|
+
const parentRel = path.dirname(rel);
|
|
516
|
+
const parent = parentRel === "." ? root : ensureManagedDirectoryCli(root, parentRel);
|
|
517
|
+
const realParent = fs.realpathSync.native(parent);
|
|
518
|
+
const relativeParent = path.relative(root, realParent);
|
|
519
|
+
if (path.isAbsolute(relativeParent) || relativeParent === ".." || relativeParent.startsWith(`..${path.sep}`)) {
|
|
520
|
+
throw new Error("credential destination escaped the project");
|
|
521
|
+
}
|
|
522
|
+
const parentAnchor = credentialDirectoryAnchor(realParent, "credential destination directory", rootAnchor);
|
|
523
|
+
const destination = path.join(parentAnchor.realpath, path.basename(rel));
|
|
524
|
+
credentialDestinationBindings.set(path.resolve(destination), {
|
|
525
|
+
root: rootAnchor,
|
|
526
|
+
parent: parentAnchor,
|
|
527
|
+
});
|
|
528
|
+
return destination;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
function copyCredentialFileAtomicCli(sourcePath, destinationPath, options = {}) {
|
|
532
|
+
const resolvedDestination = path.resolve(destinationPath);
|
|
533
|
+
const binding = credentialDestinationBindings.get(resolvedDestination) || null;
|
|
534
|
+
try {
|
|
535
|
+
const source = fs.realpathSync.native(path.resolve(sourcePath));
|
|
536
|
+
const sourceStat = fs.statSync(source);
|
|
537
|
+
if (!sourceStat.isFile() || sourceStat.size <= 0 || sourceStat.size > MAX_CREDENTIAL_FILE_BYTES) {
|
|
538
|
+
throw new Error("credential source must be a non-empty regular file no larger than 16 MiB");
|
|
539
|
+
}
|
|
540
|
+
const root = binding ? credentialAssertDirectoryAnchor(binding.root, "credential project root") : null;
|
|
541
|
+
const parent = binding
|
|
542
|
+
? credentialAssertDirectoryAnchor(binding.parent, "credential destination directory", root)
|
|
543
|
+
: credentialDirectoryAnchor(path.dirname(resolvedDestination), "credential destination directory");
|
|
544
|
+
if (path.dirname(resolvedDestination) !== parent.path || path.basename(resolvedDestination).includes(path.sep)) {
|
|
545
|
+
throw new Error("credential destination path is not anchored to its managed directory");
|
|
546
|
+
}
|
|
547
|
+
const destination = path.join(parent.realpath, path.basename(resolvedDestination));
|
|
548
|
+
const snapshot = (() => {
|
|
549
|
+
try {
|
|
550
|
+
const stat = credentialFileSnapshot(destination, "credential destination");
|
|
551
|
+
if (!options.force) throw new Error("credential destination already exists (use --force to replace)");
|
|
552
|
+
return { exists: true, stat };
|
|
553
|
+
} catch (error) {
|
|
554
|
+
if (error && error.code === "ENOENT") return { exists: false };
|
|
555
|
+
throw error;
|
|
556
|
+
}
|
|
557
|
+
})();
|
|
558
|
+
const temp = path.join(parent.realpath, `.${path.basename(destination)}.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
559
|
+
let tempStat = null;
|
|
560
|
+
try {
|
|
561
|
+
credentialAssertDirectoryAnchor(parent, "credential destination directory", root);
|
|
562
|
+
fs.copyFileSync(source, temp, fs.constants.COPYFILE_EXCL);
|
|
563
|
+
tempStat = credentialFileSnapshot(temp, "credential temporary file");
|
|
564
|
+
if (!tempStat || tempStat.size !== sourceStat.size) {
|
|
565
|
+
throw new Error("credential copy did not produce the exact regular file");
|
|
566
|
+
}
|
|
567
|
+
let readFd;
|
|
568
|
+
try {
|
|
569
|
+
readFd = fs.openSync(temp, fs.constants.O_RDONLY | CREDENTIAL_NOFOLLOW);
|
|
570
|
+
const opened = fs.fstatSync(readFd);
|
|
571
|
+
if (!credentialSameFileSnapshot(opened, tempStat) || opened.size !== sourceStat.size) {
|
|
572
|
+
throw new Error("credential temporary file changed while opening");
|
|
573
|
+
}
|
|
574
|
+
} finally {
|
|
575
|
+
if (readFd !== undefined) try { fs.closeSync(readFd); } catch { /* preserve original failure */ }
|
|
576
|
+
}
|
|
577
|
+
try { fs.chmodSync(temp, 0o600); } catch { /* Windows/best-effort */ }
|
|
578
|
+
credentialAssertDirectoryAnchor(parent, "credential destination directory", root);
|
|
579
|
+
credentialPublishFile(parent, path.basename(destination), { path: temp, stat: tempStat }, snapshot.exists ? snapshot.stat : null);
|
|
580
|
+
} finally {
|
|
581
|
+
if (tempStat) credentialRemoveOwnedFile(temp, tempStat);
|
|
582
|
+
}
|
|
583
|
+
return destinationPath;
|
|
584
|
+
} finally {
|
|
585
|
+
if (binding) credentialDestinationBindings.delete(resolvedDestination);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
244
589
|
module.exports = {
|
|
245
590
|
CREDENTIAL_INDEX_SECTION_CLI,
|
|
246
591
|
localCredentialConfigCli,
|
|
@@ -259,4 +604,7 @@ module.exports = {
|
|
|
259
604
|
readJsonObjectCli,
|
|
260
605
|
upsertLocalCredentialMapCli,
|
|
261
606
|
safeCredentialDestRelCli,
|
|
607
|
+
credentialProjectRootCli,
|
|
608
|
+
resolveCredentialDestinationCli,
|
|
609
|
+
copyCredentialFileAtomicCli,
|
|
262
610
|
};
|
|
@@ -8,13 +8,85 @@
|
|
|
8
8
|
*/
|
|
9
9
|
const fs = require("node:fs");
|
|
10
10
|
const path = require("node:path");
|
|
11
|
+
const crypto = require("node:crypto");
|
|
12
|
+
|
|
13
|
+
const MAX_DOT_ENV_BYTES = 512 * 1024;
|
|
14
|
+
|
|
15
|
+
function readEnvSnapshot(file) {
|
|
16
|
+
let before;
|
|
17
|
+
try {
|
|
18
|
+
before = fs.lstatSync(file);
|
|
19
|
+
} catch (error) {
|
|
20
|
+
if (error && error.code === "ENOENT") return { exists: false, body: "" };
|
|
21
|
+
throw error;
|
|
22
|
+
}
|
|
23
|
+
if (!before.isFile() || before.isSymbolicLink() || before.size > MAX_DOT_ENV_BYTES) {
|
|
24
|
+
throw new Error("credential env target must be a bounded regular non-symbolic-link file");
|
|
25
|
+
}
|
|
26
|
+
const fd = fs.openSync(file, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
|
|
27
|
+
try {
|
|
28
|
+
const opened = fs.fstatSync(fd);
|
|
29
|
+
if (
|
|
30
|
+
!opened.isFile() || opened.size > MAX_DOT_ENV_BYTES ||
|
|
31
|
+
opened.dev !== before.dev || opened.ino !== before.ino
|
|
32
|
+
) throw new Error("credential env target changed while opening");
|
|
33
|
+
return {
|
|
34
|
+
exists: true,
|
|
35
|
+
body: fs.readFileSync(fd, "utf8"),
|
|
36
|
+
dev: opened.dev,
|
|
37
|
+
ino: opened.ino,
|
|
38
|
+
size: opened.size,
|
|
39
|
+
mtimeMs: opened.mtimeMs,
|
|
40
|
+
ctimeMs: opened.ctimeMs,
|
|
41
|
+
};
|
|
42
|
+
} finally {
|
|
43
|
+
fs.closeSync(fd);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function assertEnvSnapshotUnchanged(file, snapshot) {
|
|
48
|
+
let current;
|
|
49
|
+
try { current = fs.lstatSync(file); }
|
|
50
|
+
catch (error) {
|
|
51
|
+
if (error && error.code === "ENOENT" && !snapshot.exists) return;
|
|
52
|
+
throw new Error("credential env target changed before replacement");
|
|
53
|
+
}
|
|
54
|
+
if (
|
|
55
|
+
!snapshot.exists || !current.isFile() || current.isSymbolicLink() ||
|
|
56
|
+
current.dev !== snapshot.dev || current.ino !== snapshot.ino ||
|
|
57
|
+
current.size !== snapshot.size || current.mtimeMs !== snapshot.mtimeMs || current.ctimeMs !== snapshot.ctimeMs
|
|
58
|
+
) throw new Error("credential env target changed before replacement");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function replaceEnvFileAtomic(temp, file, snapshot) {
|
|
62
|
+
assertEnvSnapshotUnchanged(file, snapshot);
|
|
63
|
+
try {
|
|
64
|
+
fs.renameSync(temp, file);
|
|
65
|
+
return;
|
|
66
|
+
} catch (error) {
|
|
67
|
+
if (
|
|
68
|
+
process.platform !== "win32" || !snapshot.exists ||
|
|
69
|
+
!["EEXIST", "EPERM", "EACCES"].includes(error && error.code)
|
|
70
|
+
) throw error;
|
|
71
|
+
}
|
|
72
|
+
assertEnvSnapshotUnchanged(file, snapshot);
|
|
73
|
+
const backup = `${file}.agentlas-${process.pid}-${crypto.randomUUID()}.bak`;
|
|
74
|
+
fs.renameSync(file, backup);
|
|
75
|
+
try {
|
|
76
|
+
fs.renameSync(temp, file);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
try { if (!fs.existsSync(file)) fs.renameSync(backup, file); } catch { /* leave the backup recoverable */ }
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
try { fs.rmSync(backup, { force: true }); } catch { /* committed target is authoritative */ }
|
|
82
|
+
}
|
|
11
83
|
|
|
12
84
|
/** .env 파싱 — 값 보존 없이 키만 필요할 때도 같은 파서를 쓴다 (KEY=VALUE, # 주석). */
|
|
13
85
|
function readDotEnvFile(file) {
|
|
14
86
|
const result = {};
|
|
15
87
|
let raw;
|
|
16
88
|
try {
|
|
17
|
-
raw =
|
|
89
|
+
raw = readEnvSnapshot(file).body;
|
|
18
90
|
} catch {
|
|
19
91
|
return result;
|
|
20
92
|
}
|
|
@@ -30,17 +102,30 @@ function readDotEnvFile(file) {
|
|
|
30
102
|
}
|
|
31
103
|
|
|
32
104
|
function upsertEnvLine(file, key, value) {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
105
|
+
if (!/^[A-Z][A-Z0-9_]*$/.test(String(key || ""))) throw new Error("credential env key must look like ENV_NAME");
|
|
106
|
+
const text = String(value == null ? "" : value);
|
|
107
|
+
if (/[\u0000\r\n]/.test(text)) throw new Error("credential env value must be a single line; use `agentlas creds file` for multiline credentials");
|
|
108
|
+
const snapshot = readEnvSnapshot(file);
|
|
109
|
+
let body = snapshot.body;
|
|
110
|
+
const line = `${key}=${text}`;
|
|
36
111
|
const re = new RegExp("^" + key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&") + "=.*$", "m");
|
|
37
|
-
|
|
112
|
+
// Replacement strings interpret `$&`, `$1`, and similar tokens. Credentials
|
|
113
|
+
// are opaque bytes, so use a function replacement and preserve them verbatim.
|
|
114
|
+
if (re.test(body)) body = body.replace(re, () => line);
|
|
38
115
|
else body = body ? body.replace(/\n?$/, "\n") + line + "\n" : line + "\n";
|
|
39
|
-
|
|
116
|
+
if (Buffer.byteLength(body, "utf8") > MAX_DOT_ENV_BYTES) throw new Error("credential env file exceeds the 512 KiB safety limit");
|
|
117
|
+
const directory = path.dirname(file);
|
|
118
|
+
fs.mkdirSync(directory, { recursive: true });
|
|
119
|
+
const temp = path.join(directory, `.${path.basename(file)}.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
120
|
+
try {
|
|
121
|
+
fs.writeFileSync(temp, body, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
122
|
+
replaceEnvFileAtomic(temp, file, snapshot);
|
|
123
|
+
} finally {
|
|
124
|
+
try { fs.rmSync(temp, { force: true }); } catch { /* best-effort cleanup */ }
|
|
125
|
+
}
|
|
40
126
|
// 이 헬퍼의 모든 호출자는 credential 값/경로를 기록한다. 새 파일뿐 아니라 기존 0644
|
|
41
127
|
// 파일도 매번 0600으로 수렴시켜 같은 머신의 다른 계정이 읽지 못하게 한다.
|
|
42
|
-
fs.writeFileSync(file, body, { encoding: "utf8", mode: 0o600 });
|
|
43
128
|
try { fs.chmodSync(file, 0o600); } catch { /* Windows/읽기전용 FS best-effort */ }
|
|
44
129
|
}
|
|
45
130
|
|
|
46
|
-
module.exports = { readDotEnvFile, upsertEnvLine };
|
|
131
|
+
module.exports = { MAX_DOT_ENV_BYTES, readDotEnvFile, upsertEnvLine };
|