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.
Files changed (163) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/README.md +6 -4
  3. package/bin/agentlas.cjs +341 -16
  4. package/engine/acp/server.cjs +27 -1
  5. package/engine/agentlas-api-agent.cjs +151 -120
  6. package/engine/agentlas-banner.cjs +6 -2
  7. package/engine/agentlas-capabilities.cjs +17 -10
  8. package/engine/agentlas-cloud-runtime.cjs +4 -5
  9. package/engine/agentlas-config.cjs +19 -54
  10. package/engine/agentlas-core-harness.cjs +59 -5
  11. package/engine/agentlas-desktop-loadout.cjs +17 -3
  12. package/engine/agentlas-evolution.cjs +75 -18
  13. package/engine/agentlas-experience-exchange.cjs +145 -14
  14. package/engine/agentlas-experience-intake.cjs +2 -2
  15. package/engine/agentlas-i18n.cjs +0 -2
  16. package/engine/agentlas-input.cjs +273 -20
  17. package/engine/agentlas-judgment.cjs +0 -0
  18. package/engine/agentlas-mcp-env.cjs +84 -23
  19. package/engine/agentlas-mcp-wrapper.cjs +36 -4
  20. package/engine/agentlas-memory-governance.cjs +2 -1
  21. package/engine/agentlas-memory-import.cjs +49 -12
  22. package/engine/agentlas-native-host.cjs +279 -44
  23. package/engine/agentlas-onboard.cjs +23 -1
  24. package/engine/agentlas-permissions.cjs +5 -1
  25. package/engine/agentlas-sqlite-policy.cjs +40 -2
  26. package/engine/agentlas-tasks.cjs +7 -0
  27. package/engine/agentlas-tools.cjs +37 -10
  28. package/engine/agentlas-ui.cjs +26 -4
  29. package/engine/agentlas-workforce.cjs +29 -5
  30. package/engine/agentlas-workload-routing.cjs +83 -10
  31. package/engine/agentlas.cjs +10 -3
  32. package/engine/agents/builder.cjs +35 -10
  33. package/engine/agents/files.cjs +81 -10
  34. package/engine/agents/import-local.cjs +10 -2
  35. package/engine/agents/registry.cjs +10 -3
  36. package/engine/agents/router.cjs +7 -7
  37. package/engine/agents/routes.cjs +31 -9
  38. package/engine/automation/daemon.cjs +154 -40
  39. package/engine/automation/launchd.cjs +395 -16
  40. package/engine/automation/schedule.cjs +90 -3
  41. package/engine/automation/store.cjs +163 -59
  42. package/engine/browser/cdp.cjs +190 -17
  43. package/engine/browser/vault.cjs +1 -0
  44. package/engine/cli-output.cjs +80 -28
  45. package/engine/cloud/auth.cjs +131 -13
  46. package/engine/cloud/hub-client.cjs +41 -16
  47. package/engine/cloud-assets/cas.cjs +18 -9
  48. package/engine/cloud-assets/commands.cjs +108 -30
  49. package/engine/cloud-assets/package.cjs +66 -3
  50. package/engine/cloud-assets/restore.cjs +12 -9
  51. package/engine/cloud-assets/state.cjs +308 -113
  52. package/engine/commands/acp.cjs +13 -2
  53. package/engine/commands/automation.cjs +75 -24
  54. package/engine/commands/billing.cjs +6 -1
  55. package/engine/commands/browser.cjs +61 -29
  56. package/engine/commands/build.cjs +42 -12
  57. package/engine/commands/call.cjs +1 -1
  58. package/engine/commands/cd.cjs +1 -1
  59. package/engine/commands/cloud.cjs +2 -1
  60. package/engine/commands/connect.cjs +38 -13
  61. package/engine/commands/creds.cjs +74 -29
  62. package/engine/commands/doctor.cjs +17 -1
  63. package/engine/commands/document.cjs +38 -10
  64. package/engine/commands/env.cjs +6 -1
  65. package/engine/commands/firm.cjs +42 -12
  66. package/engine/commands/graph.cjs +36 -32
  67. package/engine/commands/help.cjs +11 -1
  68. package/engine/commands/hep.cjs +1 -1
  69. package/engine/commands/import.cjs +5 -4
  70. package/engine/commands/index.cjs +1 -1
  71. package/engine/commands/install.cjs +6 -4
  72. package/engine/commands/list.cjs +56 -16
  73. package/engine/commands/login.cjs +5 -0
  74. package/engine/commands/logout.cjs +6 -1
  75. package/engine/commands/mcp.cjs +8 -2
  76. package/engine/commands/memory.cjs +3 -2
  77. package/engine/commands/multimodal.cjs +12 -0
  78. package/engine/commands/native.cjs +5 -1
  79. package/engine/commands/one.cjs +78 -33
  80. package/engine/commands/plugin.cjs +13 -2
  81. package/engine/commands/project.cjs +15 -12
  82. package/engine/commands/research.cjs +18 -7
  83. package/engine/commands/roles.cjs +104 -28
  84. package/engine/commands/route.cjs +2 -2
  85. package/engine/commands/run.cjs +42 -9
  86. package/engine/commands/search.cjs +23 -2
  87. package/engine/commands/setup.cjs +6 -1
  88. package/engine/commands/storm.cjs +44 -6
  89. package/engine/commands/swarm.cjs +47 -9
  90. package/engine/commands/telegram.cjs +6 -1
  91. package/engine/commands/uninstall.cjs +12 -6
  92. package/engine/commands/update.cjs +5 -0
  93. package/engine/commands/upload.cjs +2 -1
  94. package/engine/commands/usage.cjs +6 -1
  95. package/engine/commands/version.cjs +6 -1
  96. package/engine/commands/whoami.cjs +6 -1
  97. package/engine/commands/workforce.cjs +60 -16
  98. package/engine/core/capability-grants.cjs +7 -2
  99. package/engine/core/daemon-client.cjs +36 -7
  100. package/engine/core/db.cjs +10 -2
  101. package/engine/core/desktop-core-fetch.cjs +477 -77
  102. package/engine/core/schema-ensure.cjs +23 -11
  103. package/engine/experience/build.cjs +5 -1
  104. package/engine/experience/intents.cjs +29 -82
  105. package/engine/experience/runtime.cjs +7 -1
  106. package/engine/experience/variant.cjs +58 -4
  107. package/engine/firms/orchestrate.cjs +9 -6
  108. package/engine/graph/ask-model.cjs +22 -11
  109. package/engine/graph/interview.cjs +74 -23
  110. package/engine/graph/package.cjs +63 -8
  111. package/engine/hephaestus/local-core.cjs +18 -5
  112. package/engine/hephaestus/runtime.cjs +48 -43
  113. package/engine/hub/install.cjs +230 -61
  114. package/engine/hub/plugins.cjs +391 -29
  115. package/engine/mcp/consent.cjs +25 -17
  116. package/engine/mcp/contract.cjs +229 -24
  117. package/engine/mcp/inventory.cjs +15 -5
  118. package/engine/mcp/plan.cjs +5 -1
  119. package/engine/mcp/probe.cjs +24 -9
  120. package/engine/memory-cli/curate.cjs +74 -16
  121. package/engine/oberon/common.cjs +37 -12
  122. package/engine/oberon/manifest.cjs +25 -2
  123. package/engine/oberon/outputs.cjs +49 -19
  124. package/engine/oberon/render.cjs +665 -40
  125. package/engine/project/career-graph.cjs +15 -2
  126. package/engine/project/controller.cjs +133 -15
  127. package/engine/project/credentials.cjs +212 -34
  128. package/engine/project/env-file.cjs +93 -8
  129. package/engine/project/memory-context.cjs +11 -57
  130. package/engine/project/ontology.cjs +66 -25
  131. package/engine/project/paths.cjs +42 -8
  132. package/engine/project/seed.cjs +88 -51
  133. package/engine/project/state.cjs +41 -16
  134. package/engine/project/team.cjs +26 -1
  135. package/engine/runtime-refusal.cjs +4 -1
  136. package/engine/runtimes/acp-driver.cjs +56 -9
  137. package/engine/runtimes/auth-evidence.cjs +21 -2
  138. package/engine/runtimes/detect.cjs +28 -8
  139. package/engine/runtimes/kinds.cjs +12 -4
  140. package/engine/runtimes/overrides.cjs +4 -3
  141. package/engine/runtimes/resolve.cjs +8 -7
  142. package/engine/runtimes/roles.cjs +7 -5
  143. package/engine/sessions/apply-fences.cjs +396 -56
  144. package/engine/sessions/fences.cjs +111 -27
  145. package/engine/sessions/memory-turn.cjs +62 -3
  146. package/engine/sessions/orchestrator.cjs +11 -1
  147. package/engine/sessions/prompt.cjs +66 -9
  148. package/engine/sessions/session.cjs +70 -10
  149. package/engine/sessions/sink.cjs +78 -12
  150. package/engine/sessions/store.cjs +29 -7
  151. package/engine/storm/storm.cjs +26 -3
  152. package/engine/storm/swarm.cjs +31 -3
  153. package/engine/telegram/connect.cjs +185 -22
  154. package/engine/ui/commands-catalog.cjs +1 -1
  155. package/engine/ui/palette.cjs +12 -2
  156. package/engine/ui/repl.cjs +57 -9
  157. package/engine/ui/screens.cjs +2 -4
  158. package/engine/vendor/desktop-core.manifest.json +5 -5
  159. package/engine/workforce/capture.cjs +9 -27
  160. package/engine/workforce/deps.cjs +18 -6
  161. package/engine/workforce/local-core-transport.cjs +68 -5
  162. package/package.json +1 -1
  163. package/engine/commands/career-graph.cjs +0 -51
@@ -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 dir = path.join(projectPath, arch.memoryDir || ".agentlas");
67
- fs.mkdirSync(dir, { recursive: true });
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(projectPath) || "Project";
70
- ensureLocalCredentialStoreCli(projectPath, name, arch);
71
- ensureSoulCredentialIndexCli(projectPath, name, arch);
72
- const sitemap = path.join(dir, arch.sitemapFile || "sitemap.json");
73
- if (!fs.existsSync(sitemap)) {
74
- const now = new Date().toISOString();
75
- fs.writeFileSync(sitemap, JSON.stringify({ project: name, created_at: now, updated_at: now, nodes: [] }, null, 2), "utf8");
76
- }
77
- const skillRegistryFile = arch.skillRegistryFile || "skill-registry.json";
78
- const skillTrialsFile = arch.skillTrialsFile || "skill-trials.jsonl";
79
- const curatorDecisionsFile = arch.curatorDecisionsFile || "curator-decisions.jsonl";
80
- const ontologyRuntimeFile = arch.ontologyRuntimeFile || "ontology-runtime.json";
81
- const ontologySourceManifestFile = arch.ontologySourceManifestFile || "ontology-sources.json";
82
- const ontologyInboxDir = arch.ontologyInboxDir || "ontology-inbox";
83
- const ontologyDbFile = arch.ontologyDbFile || "ontology-runtime.sqlite";
84
- const careerGraphConfigFile = arch.careerGraphConfigFile || "career-graph.json";
85
- const careerGraphSourceManifestFile = arch.careerGraphSourceManifestFile || "career-graph-sources.json";
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
- if (!fs.existsSync(skillRegistry)) {
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), "utf8");
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
- if (!fs.existsSync(ontologyRuntime)) {
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: projectPath,
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), "utf8");
165
- }
210
+ }, null, 2));
166
211
  const ontologySources = path.join(dir, ontologySourceManifestFile);
167
- if (!fs.existsSync(ontologySources)) {
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: projectPath,
215
+ projectRoot: root,
172
216
  sources: [],
173
- }, null, 2), "utf8");
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
- if (!fs.existsSync(careerGraphConfig)) {
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: projectPath,
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), "utf8");
197
- }
237
+ }, null, 2));
198
238
  const careerGraphSources = path.join(dir, careerGraphSourceManifestFile);
199
- if (!fs.existsSync(careerGraphSources)) {
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: projectPath,
242
+ projectRoot: root,
204
243
  sources: [],
205
- }, null, 2), "utf8");
206
- }
244
+ }, null, 2));
207
245
  for (const fileName of [skillTrialsFile, curatorDecisionsFile]) {
208
- const filePath = path.join(dir, fileName);
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; }
@@ -37,16 +37,27 @@ const projectBootstrapStates = new Map();
37
37
 
38
38
  function terminalProjectCandidateCli(projectPath) {
39
39
  try {
40
- const root = path.resolve(projectPath || process.cwd());
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
- path.resolve(os.homedir()),
54
+ home,
43
55
  path.parse(root).root,
44
- path.resolve(userDataDir()),
45
- path.resolve(runCwd()),
56
+ data,
57
+ fallback,
46
58
  ]);
47
59
  if (unsafe.has(root)) return null;
48
- const stat = fs.statSync(root);
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()) throw new Error(".gitignore must be a regular non-symbolic-link file");
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) throw new Error(".gitignore changed while it was being read");
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()) continue;
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 { /* disappearing files and ACL-only hosts are best effort */ }
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
- const row = tableExists(db, "folder_activity")
318
- ? db.prepare("SELECT activated_at FROM folder_activity WHERE path=?").get(root)
319
- : null;
320
- return { activated: Boolean(row && row.activated_at) || fs.existsSync(path.join(root, ".agentlas")) };
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 });
@@ -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
- try { return fs.realpathSync.native(resolved); } catch { return resolved; }
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(ed)?\b/i,
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 = permissions.normalize(req.permission) === "full"
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: Array.isArray(req.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: req.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 = { ACP_KINDS, acpDriverAvailability, acpSpecFor, runAcpTurn };
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
- if (fs.existsSync(p) && fs.statSync(p).size > 0) return { status: "evidence", detail: label || p };
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
- return null;
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 { spawnSync } = require("node:child_process");
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 cmd = process.platform === "win32" ? "where" : "which";
17
- try {
18
- const res = spawnSync(cmd, [bin], { encoding: "utf8", timeout: 4000 });
19
- if (res.status === 0) {
20
- const line = String(res.stdout || "").split(/\r?\n/).find((l) => l.trim());
21
- return line ? line.trim() : null;
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
- } catch { /* not found */ }
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 전체 — CLI + 로컬/BYOK 실행 kind.
98
- * 데스크탑 shared/runtime-kinds.ts 와 동형(터미널 표기: antigravity→agy, acp 미지원).
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 = [...CLI_KINDS, "byok", "ollama", "lmstudio", "mlx"];
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) > 에이전트별 오버라이드 > prefs >
11
- * active_runtime > detected. resolve.cjs 건드리지 않는다 모듈이
12
- * 오버라이드 층만 앞에 얹고 나머지는 resolve.cjs 위임한다.
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 api = apiRuntime(explicit, null, "explicit");
82
+ const explicitKind = KINDS.canonicalRuntimeKind(explicit);
83
+ const api = apiRuntime(explicitKind, null, "explicit");
83
84
  if (api) return api;
84
- const bin = RUNTIME_BIN[explicit];
85
+ const bin = RUNTIME_BIN[explicitKind];
85
86
  if (!bin) throw new NoRuntimeError(`unknown runtime: ${explicit}`);
86
- if (!CLI_EXECUTABLE_KINDS.has(explicit)) {
87
- const acpHint = ACP_CLI_KINDS.has(explicit)
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: explicit, bin: p, source: "explicit" };
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
  }