agentlas 1.0.65 → 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 +78 -0
- package/README.md +6 -4
- 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/browser/cdp.cjs +190 -17
- 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 +61 -29
- 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 +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 +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 +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
package/engine/project/seed.cjs
CHANGED
|
@@ -60,34 +60,83 @@ function removeLegacySuperOntologyFiles(dir) {
|
|
|
60
60
|
}
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
function privateSeedName(value, fallback) {
|
|
64
|
+
const name = String(value || fallback || "").trim();
|
|
65
|
+
if (!name || name !== path.basename(name) || name === "." || name === ".." || name.length > 180) {
|
|
66
|
+
throw new Error("Agentlas project seed contains an unsafe file name");
|
|
67
|
+
}
|
|
68
|
+
return name;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function ensurePrivateDirectory(dir) {
|
|
72
|
+
try { fs.mkdirSync(dir, { recursive: false, mode: 0o700 }); }
|
|
73
|
+
catch (error) { if (!error || error.code !== "EEXIST") throw error; }
|
|
74
|
+
const listed = fs.lstatSync(dir);
|
|
75
|
+
if (!listed.isDirectory() || listed.isSymbolicLink()) {
|
|
76
|
+
throw new Error("Agentlas project state path must be a real directory");
|
|
77
|
+
}
|
|
78
|
+
if (process.platform !== "win32") fs.chmodSync(dir, 0o700);
|
|
79
|
+
return dir;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function ensurePrivateSeedFile(file, content) {
|
|
83
|
+
let fd = null;
|
|
84
|
+
const noFollow = fs.constants.O_NOFOLLOW || 0;
|
|
85
|
+
try {
|
|
86
|
+
try {
|
|
87
|
+
fd = fs.openSync(file, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | noFollow, 0o600);
|
|
88
|
+
const bytes = Buffer.from(String(content), "utf8");
|
|
89
|
+
const written = fs.writeSync(fd, bytes, 0, bytes.length, null);
|
|
90
|
+
if (written !== bytes.length) throw new Error("Agentlas project seed write was incomplete");
|
|
91
|
+
fs.fsyncSync(fd);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
if (!error || error.code !== "EEXIST") throw error;
|
|
94
|
+
if (fd != null) { try { fs.closeSync(fd); } catch { /* ignore */ } fd = null; }
|
|
95
|
+
fd = fs.openSync(file, fs.constants.O_RDONLY | noFollow);
|
|
96
|
+
}
|
|
97
|
+
const opened = fs.fstatSync(fd);
|
|
98
|
+
const listed = fs.lstatSync(file);
|
|
99
|
+
if (
|
|
100
|
+
!opened.isFile() || opened.nlink !== 1 ||
|
|
101
|
+
!listed.isFile() || listed.isSymbolicLink() || listed.nlink !== 1 ||
|
|
102
|
+
opened.dev !== listed.dev || opened.ino !== listed.ino
|
|
103
|
+
) throw new Error("Agentlas project seed target must be a single-link regular file");
|
|
104
|
+
try { fs.fchmodSync(fd, 0o600); } catch { /* Windows/ACL-only host */ }
|
|
105
|
+
return file;
|
|
106
|
+
} finally {
|
|
107
|
+
if (fd != null) try { fs.closeSync(fd); } catch { /* ignore */ }
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
63
111
|
function ensureProjectMemoryCli(projectPath, projectName) {
|
|
64
112
|
const arch = loadArch();
|
|
65
113
|
try {
|
|
66
|
-
const
|
|
67
|
-
fs.
|
|
114
|
+
const root = path.resolve(projectPath);
|
|
115
|
+
const canonicalRoot = fs.realpathSync.native(root);
|
|
116
|
+
const rootStat = fs.lstatSync(root);
|
|
117
|
+
if (!rootStat.isDirectory() || rootStat.isSymbolicLink() || path.parse(canonicalRoot).root === canonicalRoot) return null;
|
|
118
|
+
const memoryDirName = privateSeedName(arch.memoryDir, ".agentlas");
|
|
119
|
+
const dir = ensurePrivateDirectory(path.join(root, memoryDirName));
|
|
68
120
|
removeLegacySuperOntologyFiles(dir);
|
|
69
|
-
const name = projectName || path.basename(
|
|
70
|
-
ensureLocalCredentialStoreCli(
|
|
71
|
-
ensureSoulCredentialIndexCli(
|
|
72
|
-
const sitemap = path.join(dir, arch.sitemapFile
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
const
|
|
79
|
-
const
|
|
80
|
-
const
|
|
81
|
-
const
|
|
82
|
-
const
|
|
83
|
-
const
|
|
84
|
-
const
|
|
85
|
-
const
|
|
86
|
-
const careerGraphInboxDir = arch.careerGraphInboxDir || "career-graph-inbox";
|
|
87
|
-
const careerGraphDbFile = arch.careerGraphDbFile || "career-graph.sqlite";
|
|
121
|
+
const name = String(projectName || path.basename(root) || "Project").slice(0, 200);
|
|
122
|
+
ensureLocalCredentialStoreCli(root, name, arch);
|
|
123
|
+
ensureSoulCredentialIndexCli(root, name, arch);
|
|
124
|
+
const sitemap = path.join(dir, privateSeedName(arch.sitemapFile, "sitemap.json"));
|
|
125
|
+
const now = new Date().toISOString();
|
|
126
|
+
ensurePrivateSeedFile(sitemap, JSON.stringify({ project: name, created_at: now, updated_at: now, nodes: [] }, null, 2));
|
|
127
|
+
const skillRegistryFile = privateSeedName(arch.skillRegistryFile, "skill-registry.json");
|
|
128
|
+
const skillTrialsFile = privateSeedName(arch.skillTrialsFile, "skill-trials.jsonl");
|
|
129
|
+
const curatorDecisionsFile = privateSeedName(arch.curatorDecisionsFile, "curator-decisions.jsonl");
|
|
130
|
+
const ontologyRuntimeFile = privateSeedName(arch.ontologyRuntimeFile, "ontology-runtime.json");
|
|
131
|
+
const ontologySourceManifestFile = privateSeedName(arch.ontologySourceManifestFile, "ontology-sources.json");
|
|
132
|
+
const ontologyInboxDir = privateSeedName(arch.ontologyInboxDir, "ontology-inbox");
|
|
133
|
+
const ontologyDbFile = privateSeedName(arch.ontologyDbFile, "ontology-runtime.sqlite");
|
|
134
|
+
const careerGraphConfigFile = privateSeedName(arch.careerGraphConfigFile, "career-graph.json");
|
|
135
|
+
const careerGraphSourceManifestFile = privateSeedName(arch.careerGraphSourceManifestFile, "career-graph-sources.json");
|
|
136
|
+
const careerGraphInboxDir = privateSeedName(arch.careerGraphInboxDir, "career-graph-inbox");
|
|
137
|
+
const careerGraphDbFile = privateSeedName(arch.careerGraphDbFile, "career-graph.sqlite");
|
|
88
138
|
const skillRegistry = path.join(dir, skillRegistryFile);
|
|
89
|
-
|
|
90
|
-
fs.writeFileSync(skillRegistry, JSON.stringify({
|
|
139
|
+
ensurePrivateSeedFile(skillRegistry, JSON.stringify({
|
|
91
140
|
schemaVersion: "1.0",
|
|
92
141
|
kind: "agentlas-skill-lifecycle-registry",
|
|
93
142
|
state: "local_candidate",
|
|
@@ -127,18 +176,15 @@ function ensureProjectMemoryCli(projectPath, projectName) {
|
|
|
127
176
|
lowRiskCanaryOnly: true,
|
|
128
177
|
severeFailureTolerance: 0,
|
|
129
178
|
},
|
|
130
|
-
}, null, 2)
|
|
131
|
-
|
|
132
|
-
const ontologyInbox = path.join(dir, ontologyInboxDir);
|
|
133
|
-
if (!fs.existsSync(ontologyInbox)) fs.mkdirSync(ontologyInbox, { recursive: true });
|
|
179
|
+
}, null, 2));
|
|
180
|
+
const ontologyInbox = ensurePrivateDirectory(path.join(dir, ontologyInboxDir));
|
|
134
181
|
const ontologyRuntime = path.join(dir, ontologyRuntimeFile);
|
|
135
|
-
|
|
136
|
-
fs.writeFileSync(ontologyRuntime, JSON.stringify({
|
|
182
|
+
ensurePrivateSeedFile(ontologyRuntime, JSON.stringify({
|
|
137
183
|
schemaVersion: "1.0",
|
|
138
184
|
kind: "agentlas-ontology-runtime",
|
|
139
185
|
state: "active",
|
|
140
186
|
activation: "automatic",
|
|
141
|
-
projectRoot:
|
|
187
|
+
projectRoot: root,
|
|
142
188
|
projectName: name,
|
|
143
189
|
dbPath: path.join(dir, ontologyDbFile),
|
|
144
190
|
inboxPath: ontologyInbox,
|
|
@@ -161,27 +207,22 @@ function ensureProjectMemoryCli(projectPath, projectName) {
|
|
|
161
207
|
durableWrites: "candidate-ticket-only",
|
|
162
208
|
workingMemory: "runtime-cache-only",
|
|
163
209
|
},
|
|
164
|
-
}, null, 2)
|
|
165
|
-
}
|
|
210
|
+
}, null, 2));
|
|
166
211
|
const ontologySources = path.join(dir, ontologySourceManifestFile);
|
|
167
|
-
|
|
168
|
-
fs.writeFileSync(ontologySources, JSON.stringify({
|
|
212
|
+
ensurePrivateSeedFile(ontologySources, JSON.stringify({
|
|
169
213
|
schemaVersion: "1.0",
|
|
170
214
|
kind: "agentlas-ontology-source-manifest",
|
|
171
|
-
projectRoot:
|
|
215
|
+
projectRoot: root,
|
|
172
216
|
sources: [],
|
|
173
|
-
}, null, 2)
|
|
174
|
-
|
|
175
|
-
const careerGraphInbox = path.join(dir, careerGraphInboxDir);
|
|
176
|
-
if (!fs.existsSync(careerGraphInbox)) fs.mkdirSync(careerGraphInbox, { recursive: true });
|
|
217
|
+
}, null, 2));
|
|
218
|
+
const careerGraphInbox = ensurePrivateDirectory(path.join(dir, careerGraphInboxDir));
|
|
177
219
|
const careerGraphConfig = path.join(dir, careerGraphConfigFile);
|
|
178
|
-
|
|
179
|
-
fs.writeFileSync(careerGraphConfig, JSON.stringify({
|
|
220
|
+
ensurePrivateSeedFile(careerGraphConfig, JSON.stringify({
|
|
180
221
|
schemaVersion: "1.0",
|
|
181
222
|
kind: "agentlas-career-graph",
|
|
182
223
|
state: "active",
|
|
183
224
|
model: "ledger_first_derived_index",
|
|
184
|
-
projectRoot:
|
|
225
|
+
projectRoot: root,
|
|
185
226
|
projectName: name,
|
|
186
227
|
dbPath: path.join(dir, careerGraphDbFile),
|
|
187
228
|
inboxPath: careerGraphInbox,
|
|
@@ -193,20 +234,16 @@ function ensureProjectMemoryCli(projectPath, projectName) {
|
|
|
193
234
|
neverScanHomeDirectory: true,
|
|
194
235
|
neverScanSiblingProjects: true,
|
|
195
236
|
},
|
|
196
|
-
}, null, 2)
|
|
197
|
-
}
|
|
237
|
+
}, null, 2));
|
|
198
238
|
const careerGraphSources = path.join(dir, careerGraphSourceManifestFile);
|
|
199
|
-
|
|
200
|
-
fs.writeFileSync(careerGraphSources, JSON.stringify({
|
|
239
|
+
ensurePrivateSeedFile(careerGraphSources, JSON.stringify({
|
|
201
240
|
schemaVersion: "1.0",
|
|
202
241
|
kind: "agentlas-career-graph-source-manifest",
|
|
203
|
-
projectRoot:
|
|
242
|
+
projectRoot: root,
|
|
204
243
|
sources: [],
|
|
205
|
-
}, null, 2)
|
|
206
|
-
}
|
|
244
|
+
}, null, 2));
|
|
207
245
|
for (const fileName of [skillTrialsFile, curatorDecisionsFile]) {
|
|
208
|
-
|
|
209
|
-
if (!fs.existsSync(filePath)) fs.writeFileSync(filePath, "", "utf8");
|
|
246
|
+
ensurePrivateSeedFile(path.join(dir, fileName), "");
|
|
210
247
|
}
|
|
211
248
|
return dir;
|
|
212
249
|
} catch { return null; }
|
package/engine/project/state.cjs
CHANGED
|
@@ -37,16 +37,27 @@ const projectBootstrapStates = new Map();
|
|
|
37
37
|
|
|
38
38
|
function terminalProjectCandidateCli(projectPath) {
|
|
39
39
|
try {
|
|
40
|
-
const
|
|
40
|
+
const requested = path.resolve(projectPath || process.cwd());
|
|
41
|
+
const stat = fs.statSync(requested);
|
|
42
|
+
if (!stat.isDirectory()) return null;
|
|
43
|
+
// Resolve the directory before applying broad-root exclusions. Otherwise
|
|
44
|
+
// `/tmp/project -> ~/.agentlas` bypasses the lexical checks and lets an
|
|
45
|
+
// explicit init write private project state into Agentlas' own userData.
|
|
46
|
+
const root = fs.realpathSync.native(requested);
|
|
47
|
+
const realOrResolved = (candidate) => {
|
|
48
|
+
try { return fs.realpathSync.native(candidate); } catch { return path.resolve(candidate); }
|
|
49
|
+
};
|
|
50
|
+
const home = realOrResolved(os.homedir());
|
|
51
|
+
const data = realOrResolved(userDataDir());
|
|
52
|
+
const fallback = realOrResolved(runCwd());
|
|
41
53
|
const unsafe = new Set([
|
|
42
|
-
|
|
54
|
+
home,
|
|
43
55
|
path.parse(root).root,
|
|
44
|
-
|
|
45
|
-
|
|
56
|
+
data,
|
|
57
|
+
fallback,
|
|
46
58
|
]);
|
|
47
59
|
if (unsafe.has(root)) return null;
|
|
48
|
-
|
|
49
|
-
if (!stat.isDirectory()) return null;
|
|
60
|
+
if (root.startsWith(`${data}${path.sep}`) || root.startsWith(`${fallback}${path.sep}`)) return null;
|
|
50
61
|
return root;
|
|
51
62
|
} catch {
|
|
52
63
|
return null;
|
|
@@ -67,6 +78,10 @@ function assertNoSymlinkInAgentlasStateCli(stateDir) {
|
|
|
67
78
|
if (stat.isSymbolicLink()) throw new Error(".agentlas local state must not contain symbolic links");
|
|
68
79
|
if (stat.isDirectory()) {
|
|
69
80
|
for (const entry of fs.readdirSync(current)) pending.push(path.join(current, entry));
|
|
81
|
+
} else if (stat.isFile()) {
|
|
82
|
+
if (stat.nlink !== 1) throw new Error(".agentlas local state must not contain hard-linked files");
|
|
83
|
+
} else {
|
|
84
|
+
throw new Error(".agentlas local state must contain only directories and regular files");
|
|
70
85
|
}
|
|
71
86
|
}
|
|
72
87
|
if (pending.length) throw new Error(".agentlas local state exceeds the safe bootstrap inspection limit");
|
|
@@ -78,7 +93,9 @@ function readRegularUtf8FileNoFollowCli(filePath, maxBytes = AGENTLAS_GITIGNORE_
|
|
|
78
93
|
if (error && error.code === "ENOENT") return { exists: false, content: "", mode: 0o644, stat: null };
|
|
79
94
|
throw error;
|
|
80
95
|
}
|
|
81
|
-
if (before.isSymbolicLink() || !before.isFile()
|
|
96
|
+
if (before.isSymbolicLink() || !before.isFile() || before.nlink !== 1) {
|
|
97
|
+
throw new Error(".gitignore must be a single-link regular non-symbolic-link file");
|
|
98
|
+
}
|
|
82
99
|
if (before.size > maxBytes) throw new Error(`.gitignore exceeds the ${maxBytes}-byte safe bootstrap limit`);
|
|
83
100
|
|
|
84
101
|
const noFollow = fs.constants.O_NOFOLLOW || 0;
|
|
@@ -91,7 +108,7 @@ function readRegularUtf8FileNoFollowCli(filePath, maxBytes = AGENTLAS_GITIGNORE_
|
|
|
91
108
|
}
|
|
92
109
|
try {
|
|
93
110
|
const opened = fs.fstatSync(fd);
|
|
94
|
-
if (!opened.isFile()) throw new Error(".gitignore changed type during bootstrap");
|
|
111
|
+
if (!opened.isFile() || opened.nlink !== 1) throw new Error(".gitignore changed type during bootstrap");
|
|
95
112
|
if (opened.size > maxBytes) throw new Error(`.gitignore exceeds the ${maxBytes}-byte safe bootstrap limit`);
|
|
96
113
|
if (
|
|
97
114
|
Number.isFinite(before.dev) && Number.isFinite(before.ino) &&
|
|
@@ -110,7 +127,9 @@ function readRegularUtf8FileNoFollowCli(filePath, maxBytes = AGENTLAS_GITIGNORE_
|
|
|
110
127
|
}
|
|
111
128
|
if (total > maxBytes) throw new Error(`.gitignore exceeds the ${maxBytes}-byte safe bootstrap limit`);
|
|
112
129
|
const after = fs.fstatSync(fd);
|
|
113
|
-
if (after.size !== opened.size || after.mtimeMs !== opened.mtimeMs)
|
|
130
|
+
if (after.nlink !== 1 || after.size !== opened.size || after.mtimeMs !== opened.mtimeMs) {
|
|
131
|
+
throw new Error(".gitignore changed while it was being read");
|
|
132
|
+
}
|
|
114
133
|
let content;
|
|
115
134
|
try { content = new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks, total)); } catch {
|
|
116
135
|
throw new Error(".gitignore must contain valid UTF-8 text");
|
|
@@ -132,7 +151,7 @@ function assertFileSnapshotUnchangedCli(filePath, snapshot) {
|
|
|
132
151
|
}
|
|
133
152
|
}
|
|
134
153
|
const current = fs.lstatSync(filePath);
|
|
135
|
-
if (current.isSymbolicLink() || !current.isFile()) throw new Error(".gitignore changed type during bootstrap");
|
|
154
|
+
if (current.isSymbolicLink() || !current.isFile() || current.nlink !== 1) throw new Error(".gitignore changed type during bootstrap");
|
|
136
155
|
const original = snapshot.stat;
|
|
137
156
|
if (
|
|
138
157
|
!original || current.dev !== original.dev || current.ino !== original.ino ||
|
|
@@ -227,14 +246,20 @@ function hardenAgentlasProjectStateCli(projectPath) {
|
|
|
227
246
|
visited += 1;
|
|
228
247
|
try {
|
|
229
248
|
const stat = fs.lstatSync(current);
|
|
230
|
-
if (stat.isSymbolicLink())
|
|
249
|
+
if (stat.isSymbolicLink()) throw new Error(".agentlas local state must not contain symbolic links");
|
|
231
250
|
if (stat.isDirectory()) {
|
|
232
251
|
try { fs.chmodSync(current, 0o700); } catch { /* Windows/best effort */ }
|
|
233
252
|
for (const entry of fs.readdirSync(current)) pending.push(path.join(current, entry));
|
|
234
253
|
} else if (stat.isFile()) {
|
|
254
|
+
if (stat.nlink !== 1) throw new Error(".agentlas local state must not contain hard-linked files");
|
|
235
255
|
try { fs.chmodSync(current, 0o600); } catch { /* Windows/best effort */ }
|
|
256
|
+
} else {
|
|
257
|
+
throw new Error(".agentlas local state must contain only directories and regular files");
|
|
236
258
|
}
|
|
237
|
-
} catch {
|
|
259
|
+
} catch (error) {
|
|
260
|
+
if (error && error.code === "ENOENT") continue;
|
|
261
|
+
throw error;
|
|
262
|
+
}
|
|
238
263
|
}
|
|
239
264
|
}
|
|
240
265
|
|
|
@@ -314,10 +339,10 @@ function recordCliFolderVisit(db, projectPath, options = {}) {
|
|
|
314
339
|
const activate = options.activate === true;
|
|
315
340
|
try {
|
|
316
341
|
if (!activate) {
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
return { activated: Boolean(
|
|
342
|
+
// A historical DB row or an arbitrary `.agentlas` entry is not proof
|
|
343
|
+
// that today's private-state boundary is intact. Revalidate the marker,
|
|
344
|
+
// soul file, entry types, and link identities on every passive use.
|
|
345
|
+
return { activated: Boolean(initializedAgentlasProjectPathCli(root)) };
|
|
321
346
|
}
|
|
322
347
|
|
|
323
348
|
ensureCoreProjectCli(root, { reason: options.reason || "terminal-first-contact", coreRoot: options.coreRoot });
|
package/engine/project/team.cjs
CHANGED
|
@@ -18,24 +18,49 @@
|
|
|
18
18
|
* 갱신하고, 없으면 만든다(데스크탑이 만든 프로젝트도 그대로 이어받는다).
|
|
19
19
|
*/
|
|
20
20
|
const crypto = require("node:crypto");
|
|
21
|
+
const os = require("node:os");
|
|
21
22
|
const path = require("node:path");
|
|
22
23
|
const fs = require("node:fs");
|
|
23
24
|
const { runWriteTransaction } = require("../agentlas-sqlite-policy.cjs");
|
|
24
25
|
const { findAgent } = require("../agents/registry.cjs");
|
|
26
|
+
const { userDataDir } = require("../core/paths.cjs");
|
|
25
27
|
|
|
26
28
|
function canonical(p) {
|
|
27
29
|
const resolved = path.resolve(String(p || ""));
|
|
28
|
-
|
|
30
|
+
let root;
|
|
31
|
+
try { root = fs.realpathSync.native(resolved); }
|
|
32
|
+
catch { throw Object.assign(new Error(`project folder does not exist: ${resolved}`), { code: "project_folder_invalid", honestStop: true }); }
|
|
33
|
+
const stat = fs.lstatSync(root);
|
|
34
|
+
let home = path.resolve(os.homedir());
|
|
35
|
+
let data = path.resolve(userDataDir());
|
|
36
|
+
try { home = fs.realpathSync.native(home); } catch { /* compare unresolved */ }
|
|
37
|
+
try { data = fs.realpathSync.native(data); } catch { /* compare unresolved */ }
|
|
38
|
+
if (
|
|
39
|
+
!stat.isDirectory() || stat.isSymbolicLink() ||
|
|
40
|
+
path.parse(root).root === root || root === home ||
|
|
41
|
+
root === data || root.startsWith(`${data}${path.sep}`)
|
|
42
|
+
) {
|
|
43
|
+
throw Object.assign(new Error(`refusing unsafe project folder: ${resolved}`), { code: "project_folder_invalid", honestStop: true });
|
|
44
|
+
}
|
|
45
|
+
return root;
|
|
29
46
|
}
|
|
30
47
|
|
|
31
48
|
/** slug/이름 목록 → 검증된 순서 풀. 못 찾은 이름은 정직하게 실패. */
|
|
32
49
|
function resolveTeam(db, tokens) {
|
|
50
|
+
if (tokens.length > 64) {
|
|
51
|
+
throw Object.assign(new Error("a project team may contain at most 64 agents"), { code: "team_too_large", honestStop: true });
|
|
52
|
+
}
|
|
33
53
|
const pool = [];
|
|
54
|
+
const seen = new Set();
|
|
34
55
|
for (const token of tokens) {
|
|
35
56
|
const agent = findAgent(db, token);
|
|
36
57
|
if (!agent) {
|
|
37
58
|
throw Object.assign(new Error(`agent not found: ${token}`), { code: "team_agent_not_found", honestStop: true, token });
|
|
38
59
|
}
|
|
60
|
+
if (seen.has(agent.id)) {
|
|
61
|
+
throw Object.assign(new Error(`duplicate project team agent: ${token}`), { code: "team_agent_duplicate", honestStop: true, token });
|
|
62
|
+
}
|
|
63
|
+
seen.add(agent.id);
|
|
39
64
|
// 설치 에이전트는 로컬이다(installed_agents에 있음 = 이 머신에서 실행 가능).
|
|
40
65
|
pool.push({
|
|
41
66
|
agentId: agent.id,
|
|
@@ -27,7 +27,10 @@ const MAX_NOTICE_LENGTH = 400;
|
|
|
27
27
|
const NOTICE_PATTERNS = [
|
|
28
28
|
/\byou'?ve hit\b/i, // "You've hit your weekly/usage limit" (claude·codex 실측)
|
|
29
29
|
/\busage limit\b/i,
|
|
30
|
-
/\brate.?limit(
|
|
30
|
+
/\brate.?limit (?:exceeded|reached)\b/i,
|
|
31
|
+
/^(?:error:\s*)?rate.?limit (?:has been )?(?:exceeded|reached)\b/i,
|
|
32
|
+
/\b(?:your|the) rate.?limit (?:has been|is) (?:exceeded|reached)\b/i,
|
|
33
|
+
/\byou (?:are (?:currently )?|have been )rate.?limited\b/i,
|
|
31
34
|
/\bquota (exceeded|reached)\b/i,
|
|
32
35
|
/\bresets? (at|on)\b/i, // "resets Aug 8 at 6pm"
|
|
33
36
|
/\btry again (at|later)\b/i,
|
|
@@ -14,6 +14,48 @@ const permissions = require("../agentlas-permissions.cjs");
|
|
|
14
14
|
|
|
15
15
|
// 정본(runtimes/kinds.cjs)의 ACP 3종에서 파생 — resolve.cjs의 ACP_CLI_KINDS와 같은 원소.
|
|
16
16
|
const ACP_KINDS = new Set(require("./kinds.cjs").ACP_CLI_KINDS);
|
|
17
|
+
const ACP_HISTORY_MAX_ENTRIES = 40;
|
|
18
|
+
const ACP_HISTORY_MAX_BYTES = 512 * 1024;
|
|
19
|
+
const ACP_HISTORY_MAX_ENTRY_BYTES = 64 * 1024;
|
|
20
|
+
|
|
21
|
+
function boundedHistoryText(value, maxBytes) {
|
|
22
|
+
const text = String(value || "");
|
|
23
|
+
const encoded = Buffer.from(text, "utf8");
|
|
24
|
+
if (encoded.length <= maxBytes) return text;
|
|
25
|
+
let bounded = encoded.subarray(0, maxBytes).toString("utf8");
|
|
26
|
+
// A byte cut in the middle of a multibyte code point can decode to U+FFFD,
|
|
27
|
+
// whose three bytes would otherwise make the decoded value exceed maxBytes.
|
|
28
|
+
while (Buffer.byteLength(bounded, "utf8") > maxBytes) bounded = bounded.slice(0, -1);
|
|
29
|
+
return bounded;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/*
|
|
33
|
+
* Provider history is supplied by the local chat store, where old rows may be
|
|
34
|
+
* larger than the ACP request budget. Keep the newest messages, retain their
|
|
35
|
+
* original order, and cap both message count and UTF-8 text bytes before the
|
|
36
|
+
* request is serialized by the provider runner.
|
|
37
|
+
*/
|
|
38
|
+
function boundedAcpHistory(history) {
|
|
39
|
+
if (!Array.isArray(history)) return [];
|
|
40
|
+
const bounded = [];
|
|
41
|
+
let totalBytes = 0;
|
|
42
|
+
for (let index = history.length - 1;
|
|
43
|
+
index >= 0 && bounded.length < ACP_HISTORY_MAX_ENTRIES && totalBytes < ACP_HISTORY_MAX_BYTES;
|
|
44
|
+
index -= 1) {
|
|
45
|
+
const entry = history[index];
|
|
46
|
+
const remainingBytes = ACP_HISTORY_MAX_BYTES - totalBytes;
|
|
47
|
+
const text = boundedHistoryText(
|
|
48
|
+
entry && (entry.text ?? entry.content),
|
|
49
|
+
Math.min(ACP_HISTORY_MAX_ENTRY_BYTES, remainingBytes),
|
|
50
|
+
);
|
|
51
|
+
bounded.unshift({
|
|
52
|
+
role: entry && entry.role === "assistant" ? "assistant" : "user",
|
|
53
|
+
text,
|
|
54
|
+
});
|
|
55
|
+
totalBytes += Buffer.byteLength(text, "utf8");
|
|
56
|
+
}
|
|
57
|
+
return bounded;
|
|
58
|
+
}
|
|
17
59
|
|
|
18
60
|
/** 이 머신에서 ACP 드라이버를 쓸 수 있는가. { ok, reason?, module? } */
|
|
19
61
|
function acpDriverAvailability() {
|
|
@@ -48,13 +90,14 @@ async function runAcpTurn(req) {
|
|
|
48
90
|
return { text: "", session: req.session || {}, error: `runtime '${kind}' is not an ACP agent in this core`, errorKind: "unsupported", errorSource: "marker" };
|
|
49
91
|
}
|
|
50
92
|
const runner = mod.createAcpRunner(spec);
|
|
93
|
+
const permission = permissions.normalize(req.permission);
|
|
51
94
|
let mcpConfigPath;
|
|
52
95
|
try {
|
|
53
96
|
// ACP runner consumes the same Claude-compatible MCP config shape as Desktop.
|
|
54
97
|
// Reuse native-host's content-addressed, credential-isolating materializer so
|
|
55
98
|
// Cursor/Grok/Kimi receive exactly the already-consented Terminal allowlist.
|
|
56
99
|
const nativeHost = require("../agentlas-native-host.cjs");
|
|
57
|
-
const allowedMcpServers =
|
|
100
|
+
const allowedMcpServers = permission === "full"
|
|
58
101
|
? (req.mcpServers || [])
|
|
59
102
|
: [];
|
|
60
103
|
mcpConfigPath = nativeHost.cliMcpConfigPath(allowedMcpServers, {
|
|
@@ -92,16 +135,11 @@ async function runAcpTurn(req) {
|
|
|
92
135
|
try {
|
|
93
136
|
const result = await runner({
|
|
94
137
|
systemPrompt: req.systemPrompt || "",
|
|
95
|
-
history:
|
|
96
|
-
? req.history.map((entry) => ({
|
|
97
|
-
role: entry && entry.role === "assistant" ? "assistant" : "user",
|
|
98
|
-
text: String((entry && (entry.text ?? entry.content)) || ""),
|
|
99
|
-
}))
|
|
100
|
-
: [],
|
|
138
|
+
history: boundedAcpHistory(req.history),
|
|
101
139
|
userPrompt: req.prompt || "",
|
|
102
140
|
backendLabel: spec.label,
|
|
103
141
|
locale,
|
|
104
|
-
permission
|
|
142
|
+
permission,
|
|
105
143
|
runtimeSource: bin,
|
|
106
144
|
cwd: req.cwd,
|
|
107
145
|
env: req.env || process.env,
|
|
@@ -132,4 +170,13 @@ async function runAcpTurn(req) {
|
|
|
132
170
|
}
|
|
133
171
|
}
|
|
134
172
|
|
|
135
|
-
module.exports = {
|
|
173
|
+
module.exports = {
|
|
174
|
+
ACP_KINDS,
|
|
175
|
+
ACP_HISTORY_MAX_ENTRIES,
|
|
176
|
+
ACP_HISTORY_MAX_BYTES,
|
|
177
|
+
ACP_HISTORY_MAX_ENTRY_BYTES,
|
|
178
|
+
boundedAcpHistory,
|
|
179
|
+
acpDriverAvailability,
|
|
180
|
+
acpSpecFor,
|
|
181
|
+
runAcpTurn,
|
|
182
|
+
};
|
|
@@ -23,13 +23,32 @@ const path = require("node:path");
|
|
|
23
23
|
const { spawnSync } = require("node:child_process");
|
|
24
24
|
|
|
25
25
|
const home = () => os.homedir();
|
|
26
|
+
const MAX_AUTH_EVIDENCE_BYTES = 256 * 1024;
|
|
27
|
+
const NOFOLLOW = fs.constants.O_NOFOLLOW || 0;
|
|
26
28
|
|
|
27
29
|
function fileEvidence(rel, label) {
|
|
28
30
|
const p = path.join(home(), ...rel);
|
|
31
|
+
let fd = null;
|
|
29
32
|
try {
|
|
30
|
-
|
|
33
|
+
const before = fs.lstatSync(p);
|
|
34
|
+
if (
|
|
35
|
+
before.isSymbolicLink() || !before.isFile() || before.nlink !== 1 ||
|
|
36
|
+
before.size <= 0 || before.size > MAX_AUTH_EVIDENCE_BYTES
|
|
37
|
+
) return null;
|
|
38
|
+
fd = fs.openSync(p, fs.constants.O_RDONLY | NOFOLLOW);
|
|
39
|
+
const after = fs.fstatSync(fd);
|
|
40
|
+
if (
|
|
41
|
+
after.isSymbolicLink() || !after.isFile() || after.nlink !== 1 ||
|
|
42
|
+
after.size <= 0 || after.size > MAX_AUTH_EVIDENCE_BYTES ||
|
|
43
|
+
after.dev !== before.dev || after.ino !== before.ino
|
|
44
|
+
) return null;
|
|
45
|
+
return { status: "evidence", detail: label || p };
|
|
31
46
|
} catch { /* unreadable == no evidence */ }
|
|
32
|
-
|
|
47
|
+
finally {
|
|
48
|
+
if (fd !== null) {
|
|
49
|
+
try { fs.closeSync(fd); } catch { /* already closed */ }
|
|
50
|
+
}
|
|
51
|
+
}
|
|
33
52
|
}
|
|
34
53
|
|
|
35
54
|
function envEvidence(names) {
|
|
@@ -6,21 +6,41 @@
|
|
|
6
6
|
* 여기서는 "실행 파일이 있는가"만 결정론적으로 본다. 모델 응답 여부는 판정하지 않는다
|
|
7
7
|
* (없으면 no_runtime 정직 정지 — 폴백 금지는 상위 계층의 계약).
|
|
8
8
|
*/
|
|
9
|
-
const
|
|
9
|
+
const fs = require("node:fs");
|
|
10
|
+
const path = require("node:path");
|
|
10
11
|
// kind 목록/실행 파일 이름의 정본은 runtimes/kinds.cjs 하나다 — 여기서 다시 적지 않는다.
|
|
11
12
|
const { RUNTIME_BIN, CLI_KINDS } = require("./kinds.cjs");
|
|
12
13
|
|
|
13
14
|
const CLI_RUNTIMES = CLI_KINDS;
|
|
14
15
|
|
|
15
16
|
function whichSync(bin) {
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
17
|
+
const name = typeof bin === "string" ? bin.trim() : "";
|
|
18
|
+
if (!name || name.includes("\0") || path.basename(name) !== name) return null;
|
|
19
|
+
const pathValue = String(process.env.PATH || "");
|
|
20
|
+
if (!pathValue) return null;
|
|
21
|
+
const windows = process.platform === "win32";
|
|
22
|
+
const configuredExts = windows
|
|
23
|
+
? String(process.env.PATHEXT || ".COM;.EXE;.BAT;.CMD").split(";").filter(Boolean)
|
|
24
|
+
: [""];
|
|
25
|
+
const hasWindowsExt = windows && configuredExts.some((ext) => name.toLowerCase().endsWith(ext.toLowerCase()));
|
|
26
|
+
const extensions = hasWindowsExt ? [""] : configuredExts;
|
|
27
|
+
for (const rawDir of pathValue.split(path.delimiter)) {
|
|
28
|
+
const unquoted = rawDir.length >= 2 && rawDir.startsWith('"') && rawDir.endsWith('"')
|
|
29
|
+
? rawDir.slice(1, -1)
|
|
30
|
+
: rawDir;
|
|
31
|
+
const directory = path.resolve(unquoted || ".");
|
|
32
|
+
for (const ext of extensions) {
|
|
33
|
+
const candidate = path.join(directory, name + ext);
|
|
34
|
+
try {
|
|
35
|
+
const stat = fs.statSync(candidate);
|
|
36
|
+
if (!stat.isFile()) continue;
|
|
37
|
+
fs.accessSync(candidate, windows ? fs.constants.F_OK : fs.constants.X_OK);
|
|
38
|
+
return candidate;
|
|
39
|
+
} catch {
|
|
40
|
+
// Continue to the next PATH entry just like a shell command lookup.
|
|
41
|
+
}
|
|
22
42
|
}
|
|
23
|
-
}
|
|
43
|
+
}
|
|
24
44
|
return null;
|
|
25
45
|
}
|
|
26
46
|
|
|
@@ -94,15 +94,23 @@ const API_BACKEND_SPECS = ["anthropic", "openai", "google", "ollama", "upstage"]
|
|
|
94
94
|
const RUNTIME_SPECS = [...NATIVE_CLI_KINDS, ...API_BACKEND_SPECS];
|
|
95
95
|
|
|
96
96
|
/**
|
|
97
|
-
* 저장 계약(automation 등)이 허용하는 kind
|
|
98
|
-
*
|
|
97
|
+
* 저장 계약(automation 등)이 허용하는 kind 전체.
|
|
98
|
+
*
|
|
99
|
+
* 공유 SQLite의 소유자는 Desktop이므로 Desktop shared/runtime-kinds.ts의 저장
|
|
100
|
+
* 어휘를 모두 받아야 한다. 이 Terminal이 아직 실행하지 못하는 kind도 구조적으로
|
|
101
|
+
* 정상인 핀은 "손상"이 아니라 pinned-runtime-unavailable로 정직하게 멈춘다.
|
|
102
|
+
* agy/gemini는 이전 Terminal·Desktop 행의 전진 호환 별칭이다.
|
|
99
103
|
*/
|
|
100
|
-
const CONTRACT_RUNTIME_KINDS = [
|
|
104
|
+
const CONTRACT_RUNTIME_KINDS = [
|
|
105
|
+
"claude-code", "codex", "antigravity", "kimi", "grok", "cursor",
|
|
106
|
+
"byok", "ollama", "lmstudio", "mlx", "acp", "agentlas",
|
|
107
|
+
"agy", "gemini",
|
|
108
|
+
];
|
|
101
109
|
|
|
102
110
|
/** 저장 계약이 허용하는 LLM 백엔드 — 데스크탑 shared/runtime-backends.ts 와 동일 15종. */
|
|
103
111
|
const CONTRACT_RUNTIME_BACKENDS = [
|
|
104
112
|
"anthropic", "openai", "google", "ollama", "lmstudio", "mlx", "upstage", "custom", "glm",
|
|
105
|
-
"kimi", "deepseek", "minimax", "xai", "openrouter", "cursor",
|
|
113
|
+
"kimi", "deepseek", "minimax", "xai", "openrouter", "cursor", "agentlas",
|
|
106
114
|
];
|
|
107
115
|
|
|
108
116
|
module.exports = {
|
|
@@ -7,9 +7,10 @@
|
|
|
7
7
|
* 스키마 소유권은 데스크탑에 있으므로 여기서는 방어적으로만 읽는다(tableExists/
|
|
8
8
|
* columnExists) — 구버전 DB에서 열이 없으면 "오버라이드 없음"으로 조용히 전진.
|
|
9
9
|
*
|
|
10
|
-
* 해석 사다리(오너 계약): 명시(--runtime) > 에이전트별 오버라이드 >
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* 해석 사다리(오너 계약): 명시(--runtime) > 에이전트별 오버라이드 > 역할별
|
|
11
|
+
* Dashboard 후보 풀 > prefs > active_runtime > detected. 역할 풀은 사용자가
|
|
12
|
+
* Orchestrator/Worker별로 정한 실행 우선순위이므로 전역 legacy prefs보다 앞선다.
|
|
13
|
+
* resolve.cjs 는 건드리지 않는다 — 이 모듈이 앞의 두 층을 얹는다.
|
|
13
14
|
*/
|
|
14
15
|
const { tableExists, columnExists } = require("../core/db.cjs");
|
|
15
16
|
const { RUNTIME_BIN, whichSync } = require("./detect.cjs");
|
|
@@ -63,7 +63,7 @@ function sharedRuntimeKind(row) {
|
|
|
63
63
|
// never discard `agy` and silently fall through to Gemini or another CLI.
|
|
64
64
|
const selectedBin = path.posix.basename(path.win32.basename(String(row.source || ""))).toLowerCase();
|
|
65
65
|
if (row.kind === "gemini" && /^agy(?:\.exe|\.cmd)?$/i.test(selectedBin)) return "agy";
|
|
66
|
-
return row.kind;
|
|
66
|
+
return KINDS.canonicalRuntimeKind(row.kind);
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
class NoRuntimeError extends Error {
|
|
@@ -79,21 +79,22 @@ class NoRuntimeError extends Error {
|
|
|
79
79
|
*/
|
|
80
80
|
function resolveRuntime({ db, prefs, explicit }) {
|
|
81
81
|
if (explicit) {
|
|
82
|
-
const
|
|
82
|
+
const explicitKind = KINDS.canonicalRuntimeKind(explicit);
|
|
83
|
+
const api = apiRuntime(explicitKind, null, "explicit");
|
|
83
84
|
if (api) return api;
|
|
84
|
-
const bin = RUNTIME_BIN[
|
|
85
|
+
const bin = RUNTIME_BIN[explicitKind];
|
|
85
86
|
if (!bin) throw new NoRuntimeError(`unknown runtime: ${explicit}`);
|
|
86
|
-
if (!CLI_EXECUTABLE_KINDS.has(
|
|
87
|
-
const acpHint = ACP_CLI_KINDS.has(
|
|
87
|
+
if (!CLI_EXECUTABLE_KINDS.has(explicitKind)) {
|
|
88
|
+
const acpHint = ACP_CLI_KINDS.has(explicitKind)
|
|
88
89
|
? ` — its ACP driver needs the desktop core with electron/runtime/acp.js (npm run vendor:core / agentlas doctor)`
|
|
89
90
|
: "";
|
|
90
91
|
throw new NoRuntimeError(`runtime '${explicit}' has no v2 streaming driver yet (available: ${[...EXECUTABLE_KINDS].join(", ")})${acpHint}`);
|
|
91
92
|
}
|
|
92
93
|
const p = whichSync(bin);
|
|
93
94
|
if (!p) throw new NoRuntimeError(`runtime '${explicit}' requested but '${bin}' is not on PATH`);
|
|
94
|
-
return { kind:
|
|
95
|
+
return { kind: explicitKind, bin: p, source: "explicit" };
|
|
95
96
|
}
|
|
96
|
-
const pref = prefs && prefs.runtime;
|
|
97
|
+
const pref = KINDS.canonicalRuntimeKind(prefs && prefs.runtime);
|
|
97
98
|
if (pref && API_EXECUTABLE_KINDS.has(pref)) {
|
|
98
99
|
return apiRuntime(pref, null, "prefs");
|
|
99
100
|
}
|