agentlas 1.0.64 → 1.0.66
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 +87 -0
- package/README.md +8 -6
- package/bin/agentlas.cjs +341 -16
- package/engine/acp/server.cjs +27 -1
- 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 +29 -5
- package/engine/agentlas-workload-routing.cjs +83 -10
- package/engine/agentlas.cjs +10 -3
- 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/bootstrap-schema.sql +4 -4
- package/engine/browser/cdp.cjs +315 -32
- package/engine/browser/vault.cjs +1 -0
- package/engine/cli-output.cjs +80 -28
- 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 +308 -113
- 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 +67 -34
- package/engine/commands/build.cjs +42 -12
- 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 +40 -12
- 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 +8 -2
- 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 +29 -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 +230 -61
- 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 +15 -5
- package/engine/mcp/plan.cjs +5 -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 +212 -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 +187 -24
- 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
|
@@ -182,6 +182,7 @@ async function runCareerGraphCli(args, opts) {
|
|
|
182
182
|
const sub = normalizedArgs[0] || "status";
|
|
183
183
|
const passivePaths = careerGraphPathsForCli(projectPath);
|
|
184
184
|
if (sub === "status" || sub === "list") {
|
|
185
|
+
if (normalizedArgs.length > 1) throw new Error(`usage: career-graph ${sub}`);
|
|
185
186
|
if (!initializedAgentlasProjectPathCli(projectPath)) {
|
|
186
187
|
return [
|
|
187
188
|
ko ? "커리어 그래프: 초기화되지 않음" : "Career Graph: not initialized",
|
|
@@ -192,7 +193,10 @@ async function runCareerGraphCli(args, opts) {
|
|
|
192
193
|
}
|
|
193
194
|
return formatCareerGraphStatusCli(passivePaths, opts.lang);
|
|
194
195
|
}
|
|
195
|
-
if (sub === "help" || sub === "--help" || sub === "-h")
|
|
196
|
+
if (sub === "help" || sub === "--help" || sub === "-h") {
|
|
197
|
+
if (normalizedArgs.length > 1) throw new Error("usage: career-graph help");
|
|
198
|
+
return careerGraphUsageLinesCli(opts.lang);
|
|
199
|
+
}
|
|
196
200
|
if (["ingest", "query", "verify", "trace"].includes(String(sub))) {
|
|
197
201
|
return [
|
|
198
202
|
ko
|
|
@@ -207,6 +211,15 @@ async function runCareerGraphCli(args, opts) {
|
|
|
207
211
|
const parsed = await parseOntologyNaturalArgsCli(normalizedArgs.join(" "), cwd);
|
|
208
212
|
return runCareerGraphCli(parsed, opts);
|
|
209
213
|
}
|
|
214
|
+
let directFlags = null;
|
|
215
|
+
if (sub === "open") {
|
|
216
|
+
if (normalizedArgs.length !== 1) throw new Error("usage: career-graph open");
|
|
217
|
+
} else if (sub === "add") {
|
|
218
|
+
directFlags = parseFlagsCli(normalizedArgs.slice(1));
|
|
219
|
+
if (directFlags._.length < 1 || directFlags._.length > 3) throw new Error("usage: career-graph add <path> [kind] [scope] [--kind <kind>] [--scope <scope>]");
|
|
220
|
+
if (directFlags.kind !== undefined && directFlags._[1] !== undefined) throw new Error("kind was provided twice; use either the positional value or --kind");
|
|
221
|
+
if (directFlags.scope !== undefined && directFlags._[2] !== undefined) throw new Error("scope was provided twice; use either the positional value or --scope");
|
|
222
|
+
}
|
|
210
223
|
const paths = ensureCareerGraphCli(projectPath, opts.lang);
|
|
211
224
|
if (sub === "open") {
|
|
212
225
|
const opened = opts.noOpen ? true : openLocalPathCli(paths.inboxPath, opts.notify);
|
|
@@ -215,7 +228,7 @@ async function runCareerGraphCli(args, opts) {
|
|
|
215
228
|
: (ko ? "커리어 그래프 수신함을 자동으로 열지 못했습니다. 직접 여세요" : "Could not open Career Graph inbox automatically; open it manually")}: ${paths.inboxPath}`];
|
|
216
229
|
}
|
|
217
230
|
if (sub === "add") {
|
|
218
|
-
const flags =
|
|
231
|
+
const flags = directFlags;
|
|
219
232
|
const source = flags._[0];
|
|
220
233
|
const kind = inferOntologyKindCli(flags.kind || flags._[1], normalizedArgs.join(" "));
|
|
221
234
|
const scope = inferOntologyScopeCli(flags.scope || flags._[2], normalizedArgs.join(" "), kind);
|
|
@@ -4,6 +4,30 @@ const fs = require("node:fs");
|
|
|
4
4
|
const path = require("node:path");
|
|
5
5
|
const { listRoutableAgents } = require("../agents/registry.cjs");
|
|
6
6
|
|
|
7
|
+
const MAX_AGENT_POOL_MEMBERS = 64;
|
|
8
|
+
const MAX_AGENT_POOL_BYTES = 256 * 1024;
|
|
9
|
+
const MAX_AGENT_POOL_MEMBER_BYTES = 8 * 1024;
|
|
10
|
+
const MAX_AGENT_ID_BYTES = 512;
|
|
11
|
+
const MAX_RELEASE_ID_BYTES = 512;
|
|
12
|
+
const MAX_MEMBER_NAME_BYTES = 4 * 1024;
|
|
13
|
+
const MAX_PROJECT_NAME_BYTES = 4 * 1024;
|
|
14
|
+
const MAX_SYSTEM_PROMPT_BYTES = 256 * 1024;
|
|
15
|
+
const MAX_COMBINED_SYSTEM_PROMPT_BYTES = 512 * 1024;
|
|
16
|
+
|
|
17
|
+
function projectError(code, message) {
|
|
18
|
+
return Object.assign(new Error(message), { code, honestStop: true });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function boundedText(value, maxBytes, label, { allowEmpty = false } = {}) {
|
|
22
|
+
if (typeof value !== "string") throw projectError("project_team_unreadable", `This project's ${label} is invalid.`);
|
|
23
|
+
const text = value.trim();
|
|
24
|
+
if (!allowEmpty && !text) throw projectError("project_team_unreadable", `This project's ${label} is empty.`);
|
|
25
|
+
if (Buffer.byteLength(text, "utf8") > maxBytes || /[\u0000\u0001-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(text)) {
|
|
26
|
+
throw projectError("project_team_unreadable", `This project's ${label} is invalid or too large.`);
|
|
27
|
+
}
|
|
28
|
+
return text;
|
|
29
|
+
}
|
|
30
|
+
|
|
7
31
|
function canonicalPath(value) {
|
|
8
32
|
const resolved = path.resolve(String(value || ""));
|
|
9
33
|
try {
|
|
@@ -27,19 +51,80 @@ function projectColumns(db) {
|
|
|
27
51
|
}
|
|
28
52
|
|
|
29
53
|
function parseAgentPool(raw) {
|
|
54
|
+
if (raw !== undefined && raw !== null && typeof raw !== "string") {
|
|
55
|
+
throw projectError("project_team_unreadable", "This project's agent team cannot be read. Open the project in Agentlas Desktop and save its team again.");
|
|
56
|
+
}
|
|
57
|
+
const encoded = raw === undefined || raw === null ? "[]" : raw;
|
|
58
|
+
if (Buffer.byteLength(encoded, "utf8") > MAX_AGENT_POOL_BYTES) {
|
|
59
|
+
throw projectError("project_team_unreadable", "This project's agent team is too large. Open the project in Agentlas Desktop and save its team again.");
|
|
60
|
+
}
|
|
30
61
|
let parsed;
|
|
31
62
|
try {
|
|
32
|
-
parsed = JSON.parse(
|
|
63
|
+
parsed = JSON.parse(encoded || "[]");
|
|
33
64
|
} catch {
|
|
34
|
-
throw
|
|
65
|
+
throw projectError("project_team_unreadable", "This project's agent team cannot be read. Open the project in Agentlas Desktop and save its team again.");
|
|
35
66
|
}
|
|
36
67
|
if (!Array.isArray(parsed)) {
|
|
37
|
-
throw
|
|
68
|
+
throw projectError("project_team_unreadable", "This project's agent team cannot be read. Open the project in Agentlas Desktop and save its team again.");
|
|
38
69
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
70
|
+
if (parsed.length > MAX_AGENT_POOL_MEMBERS) {
|
|
71
|
+
throw projectError("project_team_too_large", "This project's agent team has too many members. Open the project in Agentlas Desktop and save its team again.");
|
|
72
|
+
}
|
|
73
|
+
const seen = new Set();
|
|
74
|
+
return parsed.map((member, index) => {
|
|
75
|
+
if (!member || typeof member !== "object" || Array.isArray(member)) {
|
|
76
|
+
throw projectError("project_team_unreadable", `This project's agent team member ${index + 1} is invalid.`);
|
|
77
|
+
}
|
|
78
|
+
if (Buffer.byteLength(JSON.stringify(member), "utf8") > MAX_AGENT_POOL_MEMBER_BYTES) {
|
|
79
|
+
throw projectError("project_team_unreadable", `This project's agent team member ${index + 1} is too large.`);
|
|
80
|
+
}
|
|
81
|
+
const keys = Object.keys(member).sort();
|
|
82
|
+
const legacyKeys = ["agentId", "nameSnapshot", "releaseId", "source"];
|
|
83
|
+
const canonicalKeys = ["agentId", "controllerAgentId", "entityKind", "firmId", "nameSnapshot", "releaseId", "source", "targetId"];
|
|
84
|
+
const isLegacy = keys.length === legacyKeys.length && keys.every((key, keyIndex) => key === legacyKeys[keyIndex]);
|
|
85
|
+
const isCanonical = keys.length === canonicalKeys.length && keys.every((key, keyIndex) => key === canonicalKeys[keyIndex]);
|
|
86
|
+
if (!isLegacy && !isCanonical) {
|
|
87
|
+
throw projectError("project_team_unreadable", `This project's agent team member ${index + 1} is incomplete.`);
|
|
88
|
+
}
|
|
89
|
+
const nameSnapshot = boundedText(member.nameSnapshot, MAX_MEMBER_NAME_BYTES, `agent team member ${index + 1} name`);
|
|
90
|
+
if (!["local", "cloud", "hub"].includes(member.source)) {
|
|
91
|
+
throw projectError("project_team_unreadable", `This project's agent team member ${index + 1} has an invalid source.`);
|
|
92
|
+
}
|
|
93
|
+
const releaseId = member.releaseId === null
|
|
94
|
+
? null
|
|
95
|
+
: boundedText(member.releaseId, MAX_RELEASE_ID_BYTES, `agent team member ${index + 1} release`);
|
|
96
|
+
if (isLegacy) {
|
|
97
|
+
const agentId = boundedText(member.agentId, MAX_AGENT_ID_BYTES, `agent team member ${index + 1} agentId`);
|
|
98
|
+
const key = `${member.source}:agent:${agentId}:${releaseId || ""}`;
|
|
99
|
+
if (seen.has(key)) throw projectError("project_team_unreadable", `This project's agent team member ${index + 1} is duplicated.`);
|
|
100
|
+
seen.add(key);
|
|
101
|
+
return {
|
|
102
|
+
entityKind: "agent", targetId: agentId, agentId, firmId: null, controllerAgentId: null,
|
|
103
|
+
source: member.source, releaseId, nameSnapshot,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (member.entityKind !== "agent" && member.entityKind !== "team") {
|
|
108
|
+
throw projectError("project_team_unreadable", `This project's agent team member ${index + 1} has an invalid entity kind.`);
|
|
109
|
+
}
|
|
110
|
+
const optionalId = (value, label) => value === null
|
|
111
|
+
? null
|
|
112
|
+
: boundedText(value, MAX_AGENT_ID_BYTES, `agent team member ${index + 1} ${label}`);
|
|
113
|
+
const targetId = boundedText(member.targetId, MAX_AGENT_ID_BYTES, `agent team member ${index + 1} targetId`);
|
|
114
|
+
const agentId = optionalId(member.agentId, "agentId");
|
|
115
|
+
const firmId = optionalId(member.firmId, "firmId");
|
|
116
|
+
const controllerAgentId = optionalId(member.controllerAgentId, "controllerAgentId");
|
|
117
|
+
if (member.entityKind === "agent" && (firmId !== null || controllerAgentId !== null || (member.source === "local" && agentId === null))) {
|
|
118
|
+
throw projectError("project_team_unreadable", `This project's agent team member ${index + 1} has inconsistent agent identity fields.`);
|
|
119
|
+
}
|
|
120
|
+
if (member.entityKind === "team" && (agentId !== null || (member.source === "local" && firmId === null))) {
|
|
121
|
+
throw projectError("project_team_unreadable", `This project's agent team member ${index + 1} has inconsistent team identity fields.`);
|
|
122
|
+
}
|
|
123
|
+
const key = `${member.source}:${member.entityKind}:${targetId}:${releaseId || ""}`;
|
|
124
|
+
if (seen.has(key)) throw projectError("project_team_unreadable", `This project's agent team member ${index + 1} is duplicated.`);
|
|
125
|
+
seen.add(key);
|
|
126
|
+
return { entityKind: member.entityKind, targetId, agentId, firmId, controllerAgentId, source: member.source, releaseId, nameSnapshot };
|
|
127
|
+
});
|
|
43
128
|
}
|
|
44
129
|
|
|
45
130
|
function resolveProjectForCwd(db, cwd) {
|
|
@@ -64,7 +149,23 @@ function resolveProjectForCwd(db, cwd) {
|
|
|
64
149
|
if (best.length !== 1) {
|
|
65
150
|
throw Object.assign(new Error("More than one Agentlas project is connected to this folder. Keep one source connection, then retry."), { code: "project_ambiguous", honestStop: true });
|
|
66
151
|
}
|
|
67
|
-
|
|
152
|
+
const row = best[0].row;
|
|
153
|
+
boundedText(row.name, MAX_PROJECT_NAME_BYTES, "name");
|
|
154
|
+
if (row.system_prompt !== null && row.system_prompt !== undefined) {
|
|
155
|
+
boundedText(row.system_prompt, MAX_SYSTEM_PROMPT_BYTES, "system prompt", { allowEmpty: true });
|
|
156
|
+
}
|
|
157
|
+
return { ...row, rootPath: best[0].root, agentPool: parseAgentPool(row.agent_pool_json) };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function exactInstalledRelease(db, agentId) {
|
|
161
|
+
try {
|
|
162
|
+
const row = db.prepare(
|
|
163
|
+
"SELECT agent_release_id FROM installed_agent_hub_bindings WHERE installed_agent_id=?",
|
|
164
|
+
).get(agentId);
|
|
165
|
+
return row && typeof row.agent_release_id === "string" ? row.agent_release_id : null;
|
|
166
|
+
} catch {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
68
169
|
}
|
|
69
170
|
|
|
70
171
|
function resolveProjectController(db, cwd) {
|
|
@@ -73,10 +174,19 @@ function resolveProjectController(db, cwd) {
|
|
|
73
174
|
throw Object.assign(new Error("This project has no agent team. Drag agents into the project in Desktop Work, then retry."), { code: "project_team_empty", honestStop: true });
|
|
74
175
|
}
|
|
75
176
|
const controllerRef = project.agentPool[0];
|
|
76
|
-
|
|
177
|
+
const controllerAgentId = controllerRef.entityKind === "team"
|
|
178
|
+
? controllerRef.controllerAgentId
|
|
179
|
+
: controllerRef.agentId;
|
|
180
|
+
if (!controllerAgentId) {
|
|
77
181
|
throw Object.assign(new Error(`The project controller “${controllerRef.nameSnapshot}” is not installed locally for Terminal execution. Install that exact release locally or reorder the project team.`), { code: "controller_not_installed", honestStop: true });
|
|
78
182
|
}
|
|
79
|
-
|
|
183
|
+
if (controllerRef.entityKind === "team" && controllerRef.source !== "local" && controllerRef.releaseId !== null) {
|
|
184
|
+
throw projectError("controller_release_unavailable", `The project controller team “${controllerRef.nameSnapshot}” needs its exact team release restored before Terminal can execute it.`);
|
|
185
|
+
}
|
|
186
|
+
if (controllerRef.entityKind === "agent" && controllerRef.releaseId !== null && exactInstalledRelease(db, controllerAgentId) !== controllerRef.releaseId) {
|
|
187
|
+
throw projectError("controller_release_unavailable", `The project controller “${controllerRef.nameSnapshot}” is pinned to an unavailable exact release. Restore that release or explicitly choose a new first agent in Desktop Work.`);
|
|
188
|
+
}
|
|
189
|
+
const controller = listRoutableAgents(db).find((agent) => agent.id === controllerAgentId) || null;
|
|
80
190
|
if (!controller) {
|
|
81
191
|
throw Object.assign(new Error(`The project controller “${controllerRef.nameSnapshot}” is unavailable. Restore that agent or explicitly choose a new first agent in Desktop Work.`), { code: "controller_unavailable", honestStop: true });
|
|
82
192
|
}
|
|
@@ -84,22 +194,30 @@ function resolveProjectController(db, cwd) {
|
|
|
84
194
|
}
|
|
85
195
|
|
|
86
196
|
function withProjectControllerContext(controller, project) {
|
|
197
|
+
const projectName = boundedText(project.name, MAX_PROJECT_NAME_BYTES, "name");
|
|
87
198
|
const team = project.agentPool.map((member, index) =>
|
|
88
199
|
`${index + 1}. ${member.nameSnapshot} (${member.source}${member.releaseId ? `, exact release ${member.releaseId}` : ""})`,
|
|
89
200
|
).join("\n");
|
|
90
|
-
const projectInstruction =
|
|
201
|
+
const projectInstruction = project.system_prompt === null || project.system_prompt === undefined
|
|
202
|
+
? ""
|
|
203
|
+
: boundedText(project.system_prompt, MAX_SYSTEM_PROMPT_BYTES, "system prompt", { allowEmpty: true });
|
|
204
|
+
const controllerPrompt = boundedText(controller.systemPrompt || "", MAX_SYSTEM_PROMPT_BYTES, "controller system prompt", { allowEmpty: true });
|
|
91
205
|
const ownership = [
|
|
92
|
-
`You are the controller for the Agentlas Work project “${
|
|
206
|
+
`You are the controller for the Agentlas Work project “${projectName}”.`,
|
|
93
207
|
"You own this task. The remaining ordered project members are eligible sub-agents for task-scoped WorkOrders only.",
|
|
94
208
|
"Do not transfer task ownership, silently substitute an unavailable member, or claim a sub-agent ran without an assignment and execution receipt.",
|
|
95
209
|
"Project team:",
|
|
96
210
|
team,
|
|
97
211
|
].join("\n");
|
|
212
|
+
const systemPrompt = [controllerPrompt, projectInstruction && `Project system prompt:\n${projectInstruction}`, ownership]
|
|
213
|
+
.filter(Boolean)
|
|
214
|
+
.join("\n\n");
|
|
215
|
+
if (Buffer.byteLength(systemPrompt, "utf8") > MAX_COMBINED_SYSTEM_PROMPT_BYTES) {
|
|
216
|
+
throw projectError("project_context_too_large", "This project's controller context is too large to execute safely.");
|
|
217
|
+
}
|
|
98
218
|
return {
|
|
99
219
|
...controller,
|
|
100
|
-
systemPrompt
|
|
101
|
-
.filter(Boolean)
|
|
102
|
-
.join("\n\n"),
|
|
220
|
+
systemPrompt,
|
|
103
221
|
};
|
|
104
222
|
}
|
|
105
223
|
|
|
@@ -10,9 +10,119 @@
|
|
|
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
|
+
|
|
20
|
+
function credentialProjectRootCli(projectPath) {
|
|
21
|
+
const requested = path.resolve(projectPath || "");
|
|
22
|
+
const root = fs.realpathSync.native(requested);
|
|
23
|
+
const stat = fs.statSync(root);
|
|
24
|
+
if (!stat.isDirectory()) throw new Error("credential project must be a real directory");
|
|
25
|
+
return root;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function ensureManagedDirectoryCli(root, relativePath, mode = 0o700) {
|
|
29
|
+
let current = root;
|
|
30
|
+
for (const segment of String(relativePath || "").split(/[\\/]+/).filter(Boolean)) {
|
|
31
|
+
current = path.join(current, segment);
|
|
32
|
+
try {
|
|
33
|
+
const stat = fs.lstatSync(current);
|
|
34
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error("credential store directories must not be symbolic links");
|
|
35
|
+
} catch (error) {
|
|
36
|
+
if (!error || error.code !== "ENOENT") throw error;
|
|
37
|
+
fs.mkdirSync(current, { recursive: false, mode });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return current;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function readManagedTextSnapshotCli(filePath, maxBytes = MAX_MANAGED_CREDENTIAL_METADATA_BYTES) {
|
|
44
|
+
let before;
|
|
45
|
+
try { before = fs.lstatSync(filePath); }
|
|
46
|
+
catch (error) {
|
|
47
|
+
if (error && error.code === "ENOENT") return { exists: false, text: "", mode: 0o600 };
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
if (!before.isFile() || before.isSymbolicLink() || before.size > maxBytes) {
|
|
51
|
+
throw new Error("credential store file must be a bounded regular non-symbolic-link file");
|
|
52
|
+
}
|
|
53
|
+
const fd = fs.openSync(filePath, fs.constants.O_RDONLY | (fs.constants.O_NOFOLLOW || 0));
|
|
54
|
+
try {
|
|
55
|
+
const opened = fs.fstatSync(fd);
|
|
56
|
+
if (!opened.isFile() || opened.size > maxBytes || opened.dev !== before.dev || opened.ino !== before.ino) {
|
|
57
|
+
throw new Error("credential store file changed while opening");
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
exists: true,
|
|
61
|
+
text: fs.readFileSync(fd, "utf8"),
|
|
62
|
+
mode: before.mode & 0o777,
|
|
63
|
+
stat: before,
|
|
64
|
+
};
|
|
65
|
+
} finally {
|
|
66
|
+
fs.closeSync(fd);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function assertManagedSnapshotUnchangedCli(filePath, snapshot) {
|
|
71
|
+
let current;
|
|
72
|
+
try { current = fs.lstatSync(filePath); }
|
|
73
|
+
catch (error) {
|
|
74
|
+
if (error && error.code === "ENOENT" && !snapshot.exists) return;
|
|
75
|
+
throw new Error("credential store file changed before replacement");
|
|
76
|
+
}
|
|
77
|
+
if (
|
|
78
|
+
!snapshot.exists || !current.isFile() || current.isSymbolicLink() || !snapshot.stat ||
|
|
79
|
+
current.dev !== snapshot.stat.dev || current.ino !== snapshot.stat.ino ||
|
|
80
|
+
current.size !== snapshot.stat.size || current.mtimeMs !== snapshot.stat.mtimeMs
|
|
81
|
+
) throw new Error("credential store file changed before replacement");
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function replaceManagedFileCli(tempPath, filePath, snapshot) {
|
|
85
|
+
assertManagedSnapshotUnchangedCli(filePath, snapshot);
|
|
86
|
+
try {
|
|
87
|
+
fs.renameSync(tempPath, filePath);
|
|
88
|
+
return;
|
|
89
|
+
} catch (error) {
|
|
90
|
+
if (
|
|
91
|
+
process.platform !== "win32" || !snapshot.exists ||
|
|
92
|
+
!["EEXIST", "EPERM", "EACCES"].includes(error && error.code)
|
|
93
|
+
) throw error;
|
|
94
|
+
}
|
|
95
|
+
assertManagedSnapshotUnchangedCli(filePath, snapshot);
|
|
96
|
+
const backup = `${filePath}.agentlas-${process.pid}-${crypto.randomUUID()}.bak`;
|
|
97
|
+
fs.renameSync(filePath, backup);
|
|
98
|
+
try {
|
|
99
|
+
fs.renameSync(tempPath, filePath);
|
|
100
|
+
} catch (error) {
|
|
101
|
+
try { if (!fs.existsSync(filePath)) fs.renameSync(backup, filePath); } catch { /* leave recoverable backup */ }
|
|
102
|
+
throw error;
|
|
103
|
+
}
|
|
104
|
+
try { fs.rmSync(backup, { force: true }); } catch { /* committed target is authoritative */ }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function writeManagedTextAtomicCli(filePath, text, options = {}) {
|
|
108
|
+
const snapshot = options.snapshot || readManagedTextSnapshotCli(filePath, options.maxBytes);
|
|
109
|
+
const bytes = Buffer.byteLength(text, "utf8");
|
|
110
|
+
if (bytes > (options.maxBytes || MAX_MANAGED_CREDENTIAL_METADATA_BYTES)) throw new Error("credential store file exceeds its safety limit");
|
|
111
|
+
const temp = path.join(path.dirname(filePath), `.${path.basename(filePath)}.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
112
|
+
try {
|
|
113
|
+
fs.writeFileSync(temp, text, { encoding: "utf8", mode: options.mode || snapshot.mode || 0o600, flag: "wx" });
|
|
114
|
+
replaceManagedFileCli(temp, filePath, snapshot);
|
|
115
|
+
} finally {
|
|
116
|
+
try { fs.rmSync(temp, { force: true }); } catch { /* best-effort cleanup */ }
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function ensureManagedTextFileCli(filePath, content, mode = 0o600) {
|
|
121
|
+
const snapshot = readManagedTextSnapshotCli(filePath);
|
|
122
|
+
if (!snapshot.exists) writeManagedTextAtomicCli(filePath, content, { snapshot, mode });
|
|
123
|
+
return snapshot.exists ? snapshot.text : content;
|
|
124
|
+
}
|
|
125
|
+
|
|
16
126
|
function localCredentialConfigCli(arch) {
|
|
17
127
|
return {
|
|
18
128
|
mapFile: arch.localCredentialsMapFile || "local-credentials.map.json",
|
|
@@ -94,24 +204,24 @@ ${credentialIndexSectionContentCli(arch)}
|
|
|
94
204
|
`;
|
|
95
205
|
}
|
|
96
206
|
function ensureSoulCredentialIndexCli(projectPath, projectName, arch) {
|
|
207
|
+
const root = credentialProjectRootCli(projectPath);
|
|
97
208
|
const memoryDir = (arch && arch.memoryDir) || ".agentlas";
|
|
98
209
|
const soulFile = (arch && arch.soulFile) || "project-soul-memory.md";
|
|
99
|
-
const dir =
|
|
210
|
+
const dir = ensureManagedDirectoryCli(root, memoryDir);
|
|
100
211
|
const soul = path.join(dir, soulFile);
|
|
101
|
-
|
|
102
|
-
if (!
|
|
103
|
-
|
|
212
|
+
const snapshot = readManagedTextSnapshotCli(soul);
|
|
213
|
+
if (!snapshot.exists) {
|
|
214
|
+
writeManagedTextAtomicCli(soul, projectSoulTemplateCli(projectName, arch), { snapshot, mode: 0o600 });
|
|
104
215
|
return soul;
|
|
105
216
|
}
|
|
106
|
-
|
|
107
|
-
try { content = fs.readFileSync(soul, "utf8"); } catch { content = ""; }
|
|
217
|
+
const content = snapshot.text;
|
|
108
218
|
if (!content.includes(CREDENTIAL_INDEX_SECTION_CLI)) {
|
|
109
219
|
const section = credentialIndexSectionContentCli(arch);
|
|
110
220
|
const marker = "\n## Project Purpose";
|
|
111
221
|
const next = content.includes(marker)
|
|
112
222
|
? content.replace(marker, `\n${section}\n## Project Purpose`)
|
|
113
223
|
: `${content.trimEnd()}\n\n${section}\n`;
|
|
114
|
-
|
|
224
|
+
writeManagedTextAtomicCli(soul, next.endsWith("\n") ? next : next + "\n", { snapshot, mode: 0o600 });
|
|
115
225
|
}
|
|
116
226
|
return soul;
|
|
117
227
|
}
|
|
@@ -157,7 +267,8 @@ Do not commit files from this folder.
|
|
|
157
267
|
`;
|
|
158
268
|
}
|
|
159
269
|
function ensureAgentlasCredentialIgnoreCli(projectPath, cfg) {
|
|
160
|
-
const
|
|
270
|
+
const root = credentialProjectRootCli(projectPath);
|
|
271
|
+
const gitignorePath = path.join(root, ".gitignore");
|
|
161
272
|
const marker = "# Agentlas local credentials";
|
|
162
273
|
const block = `${marker}
|
|
163
274
|
.env
|
|
@@ -171,45 +282,62 @@ ${cfg.credentialsDir}/*
|
|
|
171
282
|
!${cfg.credentialsDir}/
|
|
172
283
|
!${cfg.credentialsDir}/${cfg.readmeFile}
|
|
173
284
|
`;
|
|
174
|
-
|
|
175
|
-
|
|
285
|
+
const snapshot = readManagedTextSnapshotCli(gitignorePath);
|
|
286
|
+
const existing = snapshot.text;
|
|
176
287
|
if (existing.includes(marker)) {
|
|
177
|
-
if (!/^\._\*$/m.test(existing))
|
|
288
|
+
if (!/^\._\*$/m.test(existing)) {
|
|
289
|
+
writeManagedTextAtomicCli(gitignorePath, `${existing.trimEnd()}\n._*\n`, { snapshot, mode: snapshot.mode || 0o644 });
|
|
290
|
+
}
|
|
178
291
|
return;
|
|
179
292
|
}
|
|
180
293
|
const next = existing.trimEnd() ? `${existing.trimEnd()}\n\n${block}` : block;
|
|
181
|
-
|
|
294
|
+
writeManagedTextAtomicCli(gitignorePath, next.endsWith("\n") ? next : next + "\n", {
|
|
295
|
+
snapshot,
|
|
296
|
+
mode: snapshot.exists ? snapshot.mode : 0o644,
|
|
297
|
+
});
|
|
182
298
|
}
|
|
183
299
|
function ensureLocalCredentialStoreCli(projectPath, projectName, arch) {
|
|
300
|
+
const root = credentialProjectRootCli(projectPath);
|
|
184
301
|
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");
|
|
302
|
+
const dir = ensureManagedDirectoryCli(root, (arch && arch.memoryDir) || ".agentlas");
|
|
303
|
+
const signingDir = ensureManagedDirectoryCli(root, cfg.signingDir);
|
|
304
|
+
const credentialsDir = ensureManagedDirectoryCli(root, cfg.credentialsDir);
|
|
305
|
+
const envExample = path.join(root, cfg.envExampleFile);
|
|
306
|
+
ensureManagedTextFileCli(envExample, envExampleContentCli(cfg), 0o600);
|
|
307
|
+
const signingReadme = path.join(signingDir, cfg.readmeFile);
|
|
308
|
+
ensureManagedTextFileCli(signingReadme, signingReadmeContentCli(cfg), 0o600);
|
|
309
|
+
const credentialsReadme = path.join(credentialsDir, cfg.readmeFile);
|
|
310
|
+
ensureManagedTextFileCli(credentialsReadme, credentialsReadmeContentCli(cfg), 0o600);
|
|
195
311
|
const mapPath = path.join(dir, cfg.mapFile);
|
|
196
|
-
|
|
197
|
-
|
|
312
|
+
const mapText = ensureManagedTextFileCli(
|
|
313
|
+
mapPath,
|
|
314
|
+
JSON.stringify(localCredentialsMapSkeletonCli(root, projectName, cfg), null, 2) + "\n",
|
|
315
|
+
0o600,
|
|
316
|
+
);
|
|
317
|
+
let mapValue;
|
|
318
|
+
try { mapValue = JSON.parse(mapText); }
|
|
319
|
+
catch (error) { throw new Error(`credential map is invalid JSON: ${String((error && error.message) || error)}`); }
|
|
320
|
+
if (!mapValue || typeof mapValue !== "object" || Array.isArray(mapValue)) {
|
|
321
|
+
throw new Error("credential map must contain a JSON object");
|
|
198
322
|
}
|
|
199
|
-
ensureAgentlasCredentialIgnoreCli(
|
|
200
|
-
return { cfg, mapPath };
|
|
323
|
+
ensureAgentlasCredentialIgnoreCli(root, cfg);
|
|
324
|
+
return { cfg, mapPath, projectRoot: root };
|
|
201
325
|
}
|
|
202
|
-
function readJsonObjectCli(file, fallback) {
|
|
326
|
+
function readJsonObjectCli(file, fallback, options = {}) {
|
|
203
327
|
try {
|
|
204
|
-
const parsed = JSON.parse(
|
|
205
|
-
|
|
206
|
-
|
|
328
|
+
const parsed = JSON.parse(readManagedTextSnapshotCli(file).text);
|
|
329
|
+
if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
|
|
330
|
+
if (options.strict) throw new Error("credential map must contain a JSON object");
|
|
331
|
+
return fallback;
|
|
332
|
+
} catch (error) {
|
|
333
|
+
if (options.strict) throw new Error(`credential map is unreadable or invalid: ${String((error && error.message) || error)}`);
|
|
207
334
|
return fallback;
|
|
208
335
|
}
|
|
209
336
|
}
|
|
210
337
|
function upsertLocalCredentialMapCli(projectPath, projectName, arch, entry) {
|
|
211
|
-
const { cfg, mapPath } = ensureLocalCredentialStoreCli(projectPath, projectName, arch || loadArch());
|
|
212
|
-
const
|
|
338
|
+
const { cfg, mapPath, projectRoot } = ensureLocalCredentialStoreCli(projectPath, projectName, arch || loadArch());
|
|
339
|
+
const snapshot = readManagedTextSnapshotCli(mapPath);
|
|
340
|
+
const data = readJsonObjectCli(mapPath, localCredentialsMapSkeletonCli(projectPath, projectName, cfg), { strict: true });
|
|
213
341
|
const now = new Date().toISOString();
|
|
214
342
|
data.updatedAt = now;
|
|
215
343
|
if (!Array.isArray(data.entries)) data.entries = [];
|
|
@@ -230,17 +358,64 @@ function upsertLocalCredentialMapCli(projectPath, projectName, arch, entry) {
|
|
|
230
358
|
const idx = data.entries.findIndex((row) => row && row.id === id);
|
|
231
359
|
if (idx >= 0) data.entries[idx] = { ...data.entries[idx], ...clean };
|
|
232
360
|
else data.entries.push(clean);
|
|
233
|
-
|
|
361
|
+
data.projectRoot = projectRoot;
|
|
362
|
+
writeManagedTextAtomicCli(mapPath, JSON.stringify(data, null, 2) + "\n", { snapshot, mode: 0o600 });
|
|
234
363
|
}
|
|
235
364
|
// v1은 fail()로 즉시 종료했다 — v2 규칙은 throw → 명령이 ctx.err + return 1 로 변환.
|
|
236
365
|
function safeCredentialDestRelCli(destRel) {
|
|
237
366
|
const rel = path.normalize(String(destRel || "")).replace(/\\/g, "/");
|
|
238
|
-
if (!rel || path.isAbsolute(rel) || rel === "." || rel.startsWith("../") || rel
|
|
367
|
+
if (!rel || path.isAbsolute(rel) || rel === "." || rel.startsWith("../") || /[\u0000-\u001f\u007f]/.test(rel)) {
|
|
239
368
|
throw new Error("credential destination must be a relative path inside the project");
|
|
240
369
|
}
|
|
241
370
|
return rel;
|
|
242
371
|
}
|
|
243
372
|
|
|
373
|
+
function resolveCredentialDestinationCli(projectPath, destRel) {
|
|
374
|
+
const root = credentialProjectRootCli(projectPath);
|
|
375
|
+
const rel = safeCredentialDestRelCli(destRel);
|
|
376
|
+
const parentRel = path.dirname(rel);
|
|
377
|
+
const parent = parentRel === "." ? root : ensureManagedDirectoryCli(root, parentRel);
|
|
378
|
+
const realParent = fs.realpathSync.native(parent);
|
|
379
|
+
const relativeParent = path.relative(root, realParent);
|
|
380
|
+
if (path.isAbsolute(relativeParent) || relativeParent === ".." || relativeParent.startsWith(`..${path.sep}`)) {
|
|
381
|
+
throw new Error("credential destination escaped the project");
|
|
382
|
+
}
|
|
383
|
+
return path.join(realParent, path.basename(rel));
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function copyCredentialFileAtomicCli(sourcePath, destinationPath, options = {}) {
|
|
387
|
+
const source = fs.realpathSync.native(path.resolve(sourcePath));
|
|
388
|
+
const sourceStat = fs.statSync(source);
|
|
389
|
+
if (!sourceStat.isFile() || sourceStat.size <= 0 || sourceStat.size > MAX_CREDENTIAL_FILE_BYTES) {
|
|
390
|
+
throw new Error("credential source must be a non-empty regular file no larger than 16 MiB");
|
|
391
|
+
}
|
|
392
|
+
const snapshot = (() => {
|
|
393
|
+
try {
|
|
394
|
+
const stat = fs.lstatSync(destinationPath);
|
|
395
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error("credential destination must be a regular non-symbolic-link file");
|
|
396
|
+
if (!options.force) throw new Error("credential destination already exists (use --force to replace)");
|
|
397
|
+
return { exists: true, stat };
|
|
398
|
+
} catch (error) {
|
|
399
|
+
if (error && error.code === "ENOENT") return { exists: false };
|
|
400
|
+
throw error;
|
|
401
|
+
}
|
|
402
|
+
})();
|
|
403
|
+
const temp = path.join(path.dirname(destinationPath), `.${path.basename(destinationPath)}.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
404
|
+
try {
|
|
405
|
+
fs.copyFileSync(source, temp, fs.constants.COPYFILE_EXCL);
|
|
406
|
+
const copied = fs.lstatSync(temp);
|
|
407
|
+
if (!copied.isFile() || copied.isSymbolicLink() || copied.size !== sourceStat.size) {
|
|
408
|
+
throw new Error("credential copy did not produce the exact regular file");
|
|
409
|
+
}
|
|
410
|
+
try { fs.chmodSync(temp, 0o600); } catch { /* Windows/best-effort */ }
|
|
411
|
+
replaceManagedFileCli(temp, destinationPath, snapshot);
|
|
412
|
+
try { fs.chmodSync(destinationPath, 0o600); } catch { /* Windows/best-effort */ }
|
|
413
|
+
} finally {
|
|
414
|
+
try { fs.rmSync(temp, { force: true }); } catch { /* best-effort cleanup */ }
|
|
415
|
+
}
|
|
416
|
+
return destinationPath;
|
|
417
|
+
}
|
|
418
|
+
|
|
244
419
|
module.exports = {
|
|
245
420
|
CREDENTIAL_INDEX_SECTION_CLI,
|
|
246
421
|
localCredentialConfigCli,
|
|
@@ -259,4 +434,7 @@ module.exports = {
|
|
|
259
434
|
readJsonObjectCli,
|
|
260
435
|
upsertLocalCredentialMapCli,
|
|
261
436
|
safeCredentialDestRelCli,
|
|
437
|
+
credentialProjectRootCli,
|
|
438
|
+
resolveCredentialDestinationCli,
|
|
439
|
+
copyCredentialFileAtomicCli,
|
|
262
440
|
};
|