agentlas 0.6.0 → 0.9.1
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 +190 -0
- package/README.md +220 -4
- package/bin/agentlas.cjs +8 -8
- package/engine/agentlas-capabilities.cjs +34 -3
- package/engine/agentlas-core-harness.cjs +205 -0
- package/engine/agentlas-desktop-loadout.cjs +527 -0
- package/engine/agentlas-doctor.cjs +1 -1
- package/engine/agentlas-experience-exchange.cjs +2151 -0
- package/engine/agentlas-experience-intake.cjs +444 -0
- package/engine/agentlas-experience-mcp.cjs +1709 -0
- package/engine/agentlas-i18n.cjs +10 -10
- package/engine/agentlas-input.cjs +5 -4
- package/engine/agentlas-mcp-env.cjs +219 -0
- package/engine/agentlas-mcp-wrapper.cjs +51 -0
- package/engine/agentlas-memory-governance.cjs +1029 -0
- package/engine/agentlas-native-host.cjs +129 -39
- package/engine/agentlas-parity.cjs +339 -154
- package/engine/agentlas-repl.cjs +327 -44
- package/engine/agentlas-workforce.cjs +2991 -0
- package/engine/agentlas-workload-routing.cjs +523 -0
- package/engine/agentlas.cjs +1886 -270
- package/engine/bootstrap-schema.sql +1 -1
- package/engine/experience-taxonomy-v1.json +49 -0
- package/package.json +8 -4
- package/scripts/gen-bootstrap-schema.sh +0 -23
- package/test/bootstrap-race.cjs +0 -47
- package/test/capture-runtime-guard.cjs +0 -122
- package/test/cloud-asset-restore.cjs +0 -423
- package/test/cloud-cas-client.cjs +0 -333
- package/test/cloud-owner-restore.cjs +0 -183
- package/test/cloud-runtime-paths.cjs +0 -40
- package/test/cloud-save-publish.cjs +0 -453
- package/test/credential-env-regression.cjs +0 -52
- package/test/login-loopback-security.cjs +0 -115
- package/test/mcp-config-isolation.cjs +0 -36
- package/test/permission-mapping.cjs +0 -180
- package/test/route-regression.cjs +0 -121
- package/test/run-api-regression.cjs +0 -322
- package/test/runtime-env-protection.cjs +0 -45
- package/test/semver-precedence.cjs +0 -39
- package/test/smoke.sh +0 -90
- package/test/sqlite-driver-probe.cjs +0 -22
- package/test/terminal-ui-regression.cjs +0 -472
- package/test/timeout-regression.cjs +0 -218
- package/test/tool-workspace-boundary.cjs +0 -165
- package/test/update-safety.cjs +0 -376
package/engine/agentlas.cjs
CHANGED
|
@@ -29,6 +29,13 @@ const fs = require("node:fs");
|
|
|
29
29
|
const { spawn } = require("node:child_process");
|
|
30
30
|
const crypto = require("node:crypto");
|
|
31
31
|
const { compareSemVer, normalizeSemVer, parseSemVer } = require("./semver.cjs");
|
|
32
|
+
const terminalAssets = require("./agentlas-experience-mcp.cjs");
|
|
33
|
+
const terminalExperienceExchange = require("./agentlas-experience-exchange.cjs");
|
|
34
|
+
const desktopOntologyLoadout = require("./agentlas-desktop-loadout.cjs");
|
|
35
|
+
const workloadRouting = require("./agentlas-workload-routing.cjs");
|
|
36
|
+
const terminalExperienceIntake = require("./agentlas-experience-intake.cjs");
|
|
37
|
+
const terminalMemoryGovernance = require("./agentlas-memory-governance.cjs");
|
|
38
|
+
const { captureCoreJsonSync, resolveCoreRuntimeRoot } = require("./agentlas-core-harness.cjs");
|
|
32
39
|
|
|
33
40
|
// ── 앱과 동일한 userData 경로 (electron app.getPath('userData')와 일치) ──
|
|
34
41
|
function userDataDir() {
|
|
@@ -93,6 +100,19 @@ function openNodeSqliteDb(p) {
|
|
|
93
100
|
run: (...args) => stmt.run(...args),
|
|
94
101
|
};
|
|
95
102
|
},
|
|
103
|
+
// better-sqlite3 API 패리티 — 폴백 경로에서도 db.exec/db.pragma가 있어야 한다.
|
|
104
|
+
// 누락 시 ensureMemoryContextColumn 등 ALTER TABLE(exec)이 TypeError로 조용히 죽어
|
|
105
|
+
// (try/catch 삼킴) context_json 컬럼 마이그레이션이 되지 않고 memory 조회가 깨진다.
|
|
106
|
+
exec: (sql) => db.exec(sql),
|
|
107
|
+
pragma: (source) => {
|
|
108
|
+
const rows = db.prepare(`PRAGMA ${source}`).all();
|
|
109
|
+
// better-sqlite3 pragma()의 단일값 반환 관례를 근사(단일 컬럼·단일 행 → 스칼라).
|
|
110
|
+
if (rows.length === 1) {
|
|
111
|
+
const keys = Object.keys(rows[0]);
|
|
112
|
+
if (keys.length === 1) return rows[0][keys[0]];
|
|
113
|
+
}
|
|
114
|
+
return rows;
|
|
115
|
+
},
|
|
96
116
|
transaction(fn) {
|
|
97
117
|
return (...args) => {
|
|
98
118
|
db.exec("BEGIN");
|
|
@@ -241,7 +261,7 @@ function saveMultimodalSettingsCli(db, patch) {
|
|
|
241
261
|
db.prepare("INSERT INTO meta(key,value) VALUES(?,?) ON CONFLICT(key) DO UPDATE SET value=excluded.value")
|
|
242
262
|
.run(MULTIMODAL_META_KEY, JSON.stringify(next));
|
|
243
263
|
} catch (e) {
|
|
244
|
-
fail("multimodal settings
|
|
264
|
+
fail("Failed to save multimodal settings: " + e.message);
|
|
245
265
|
}
|
|
246
266
|
return next;
|
|
247
267
|
}
|
|
@@ -280,7 +300,17 @@ const AGENT_BUILD_TERMS = [
|
|
|
280
300
|
const BUILD_ENTITY_RE = /(에이전트|agent|팀|team|회사|company)/i;
|
|
281
301
|
const BUILD_VERB_RE = /(만들|만든|생성|구축|구성해|꾸려|세팅|패키징|scaffold|build|create|\bmake\b|set\s?up|spin\s?up)/i;
|
|
282
302
|
function isAgentBuildIntent(prompt) {
|
|
283
|
-
|
|
303
|
+
// 경로/파일 참조는 빌드 의도의 증거가 아니다 — "/Users/x/agent-tools/notes.md 요약본
|
|
304
|
+
// 만들어줘"의 디렉터리명이나 "agent-notes.md" 같은 파일명이 BUILD_ENTITY_RE를 때려
|
|
305
|
+
// 메타빌더(score 1000)로 직행하던 우회로 차단. 빌드 의도는 산문에서만 읽는다.
|
|
306
|
+
// ⚠️ 남은 슬래시는 통째로 지우지 않고 공백으로만 벌린다 — "에이전트/팀 만들어줘"의
|
|
307
|
+
// 슬래시-엔티티("에이전트/팀")를 삭제하면 BUILD_ENTITY_RE가 못 맞아 빌드 의도를 놓친다.
|
|
308
|
+
// 진짜 경로는 이미 routeStripPaths(마지막 세그먼트만)+확장자 제거가 처리했다.
|
|
309
|
+
const p = routeNormalize(
|
|
310
|
+
routeStripPaths(prompt)
|
|
311
|
+
.replace(/\S+\.[A-Za-z0-9]{1,6}(?=\s|$)/g, " ")
|
|
312
|
+
.replace(/[\\/]+/g, " "),
|
|
313
|
+
);
|
|
284
314
|
if (!p.trim() || isTrivialRoutePrompt(p)) return false;
|
|
285
315
|
if (AGENT_BUILD_TERMS.some((term) => p.includes(routeNormalize(term)))) return true;
|
|
286
316
|
// 예: "단일 에이전트 하나만 만들어줘", "팀 좀 꾸려줘", "make me an agent"
|
|
@@ -302,7 +332,9 @@ function resolveMetaBuilder(db) {
|
|
|
302
332
|
}
|
|
303
333
|
// "ai"/"llm" 같은 초범용 토큰은 모든 에이전트 프롬프트에 나오므로 판별력이 0이다 —
|
|
304
334
|
// 이런 단어 하나로 전문 에이전트가 선택되던 오라우팅(예: 일반 맥 질문 → Pitch Deck Architect)을 막는다.
|
|
305
|
-
|
|
335
|
+
// "local"/"imported"/"team"은 임포터 보일러플레이트("Imported local team")와 slug 접두/접미에
|
|
336
|
+
// 편재해 판별력이 없다 — 'team' 한 단어가 아무 임포트 팀의 slug 부분문자열(+6 strong)을 때리던 구멍.
|
|
337
|
+
const ROUTE_STOP_WORDS = new Set(["the", "and", "for", "with", "this", "that", "from", "into", "make", "build", "create", "agent", "agents", "team", "please", "ai", "llm", "local", "imported", "인공지능", "에이아이", "좀", "해주세요", "해줘", "만들어", "붙여", "연결", "작업", "요청"]);
|
|
306
338
|
const ROUTE_HINTS = [
|
|
307
339
|
{
|
|
308
340
|
slug: "agentlas-app-builder",
|
|
@@ -360,24 +392,46 @@ const ROUTE_HINTS = [
|
|
|
360
392
|
function routeNormalize(value) {
|
|
361
393
|
return String(value || "").toLowerCase().replace(/[_/]+/g, "-");
|
|
362
394
|
}
|
|
395
|
+
// 경로 디렉터리 성분은 라우팅 의도가 아니다 — 마지막 세그먼트(파일/폴더명)만 남긴다.
|
|
396
|
+
// 사고(2026-07-12): "/Users/example/Projects/…/Appbridge_Template.이 …" 프롬프트의 경로 토큰
|
|
397
|
+
// ("users","example","projects","users-example-projects-")이 임포트 에이전트 system_prompt 속
|
|
398
|
+
// 절대경로와 맞아떨어져 +2씩 쌓이고 라우팅 근거에까지 노출됐다. 프롬프트/헤이스택 양쪽에
|
|
399
|
+
// 대칭 적용해 경로↔경로 우연 일치를 차단한다. 파일/폴더명은 실제 의도라서 보존한다.
|
|
400
|
+
// 규칙: 공백/인용부호/괄호 뒤(또는 문자열 시작)에서 시작하고, "세그먼트+구분자"가 2회 이상
|
|
401
|
+
// 이어지는 절대·홈·드라이브·UNC 경로만 경로로 본다 — "and/or", "서울/부산", 날짜(2026/07/12),
|
|
402
|
+
// "https://…"(콜론 뒤 //는 시작 조건 불충족)는 건드리지 않는다. 세그먼트 안의 단일 공백은
|
|
403
|
+
// 뒤가 대문자로 시작할 때만 허용해 "Mobile Documents"/"Application Support"는 접되,
|
|
404
|
+
// "/tmp/out 기획/디자인 …" 같은 한글 프로즈를 경로로 삼켜버리지 않는다. 상대경로는
|
|
405
|
+
// 확장자 있는 파일 참조("docs/plan/roadmap.md")만 접는다 — 디렉터리명("plan")이 힌트/이름
|
|
406
|
+
// strong 채널을 때리는 것을 막으면서 "서울/부산/대구" 같은 나열은 보존한다.
|
|
407
|
+
const ROUTE_PATH_RE = /(^|[\s"'`(<\[{])((?:~|[A-Za-z]:)?[\\/]{1,2}(?:[^\s\\/]+(?: [A-Z][^\s\\/]*)?[\\/]+){2,}[^\s\\/]*|(?:[^\s\\/]+[\\/]+){2,}[^\s\\/]+\.[A-Za-z0-9]{1,6})/g;
|
|
408
|
+
function routeStripPaths(value) {
|
|
409
|
+
return String(value || "").replace(ROUTE_PATH_RE, (whole, pre, p) => {
|
|
410
|
+
const segs = p.split(/[\\/]+/).filter(Boolean);
|
|
411
|
+
return pre + (segs.length ? segs[segs.length - 1] : "");
|
|
412
|
+
});
|
|
413
|
+
}
|
|
363
414
|
function routeTokenize(value) {
|
|
364
|
-
|
|
415
|
+
// 매치가 영숫자로 끝나도록 강제해 "users-mason-documents-" 같은 후행 하이픈 토큰을 원천 차단.
|
|
416
|
+
const matches = routeNormalize(routeStripPaths(value)).match(/[a-z0-9][a-z0-9-]*[a-z0-9]|[가-힣]{2,}/g) || [];
|
|
365
417
|
const expanded = matches.flatMap((term) => term.split("-").filter(Boolean).concat(term));
|
|
366
418
|
return [...new Set(expanded.filter((term) => term.length >= 2 && !ROUTE_STOP_WORDS.has(term)))];
|
|
367
419
|
}
|
|
368
420
|
// 정체성 존(slug/이름/태그라인) — 여기 적중은 강한 라우팅 신호. system_prompt 본문 적중은 약한 신호.
|
|
421
|
+
// 임포터 보일러플레이트 태그라인("Imported local team/agent")의 세 단어는 전부 스톱워드라
|
|
422
|
+
// 프롬프트 토큰이 될 수 없다 — 별도 필터 불필요.
|
|
369
423
|
function routeIdentityHaystack(agent) {
|
|
370
|
-
return routeNormalize([agent.slug, agent.name, agent.name_en, agent.tagline, agent.tagline_en].join("\n"));
|
|
424
|
+
return routeNormalize(routeStripPaths([agent.slug, agent.name, agent.name_en, agent.tagline, agent.tagline_en].join("\n")));
|
|
371
425
|
}
|
|
372
426
|
function routeHaystack(agent) {
|
|
373
|
-
return routeNormalize([
|
|
427
|
+
return routeNormalize(routeStripPaths([
|
|
374
428
|
agent.slug,
|
|
375
429
|
agent.name,
|
|
376
430
|
agent.name_en,
|
|
377
431
|
agent.tagline,
|
|
378
432
|
agent.tagline_en,
|
|
379
433
|
String(agent.system_prompt || "").slice(0, 3500),
|
|
380
|
-
].join("\n"));
|
|
434
|
+
].join("\n")));
|
|
381
435
|
}
|
|
382
436
|
const APP_BUILDER_EXPLICIT_TERMS = [
|
|
383
437
|
"apps generate", "app builder", "make an app", "build an app", "create an app",
|
|
@@ -420,7 +474,7 @@ function isTrivialRoutePrompt(promptText) {
|
|
|
420
474
|
return words.length <= 3 && TRIVIAL_ROUTE_PROMPTS.has(stripped);
|
|
421
475
|
}
|
|
422
476
|
function isAppBuilderWorthyRoutePrompt(prompt) {
|
|
423
|
-
const promptText = routeNormalize(prompt);
|
|
477
|
+
const promptText = routeNormalize(routeStripPaths(prompt));
|
|
424
478
|
if (!promptText.trim() || isTrivialRoutePrompt(promptText)) return false;
|
|
425
479
|
const explicit = routeMatchedTerms(promptText, APP_BUILDER_EXPLICIT_TERMS);
|
|
426
480
|
if (explicit.length) return true;
|
|
@@ -442,8 +496,10 @@ function routeHint(promptText, agent, lang) {
|
|
|
442
496
|
if (!terms.length) return { score: 0, terms: [], reason: "" };
|
|
443
497
|
return { score: 12 + terms.length * 3, terms, reason: lang === "ko" ? hint.reasonKo : hint.reasonEn };
|
|
444
498
|
}
|
|
445
|
-
function scoreRouteAgent(prompt, promptTerms, agent, lang) {
|
|
446
|
-
|
|
499
|
+
function scoreRouteAgent(prompt, promptTerms, agent, lang, pre) {
|
|
500
|
+
// 대칭 스트리핑 필수: promptText는 이름(+20)·힌트(+12↑) strong 채널의 입력이라, 여기서
|
|
501
|
+
// 경로를 안 벗기면 "/Users/x/project-plan/…"의 디렉터리명이 strong 게이트를 그대로 뚫는다.
|
|
502
|
+
const promptText = routeNormalize(routeStripPaths(prompt));
|
|
447
503
|
if (agent.slug === "agentlas-app-builder" && !isAppBuilderWorthyRoutePrompt(promptText)) {
|
|
448
504
|
return {
|
|
449
505
|
agent,
|
|
@@ -452,24 +508,33 @@ function scoreRouteAgent(prompt, promptTerms, agent, lang) {
|
|
|
452
508
|
? "전용 App을 만들 만큼 반복·상태·편집·자동화가 뚜렷하지 않아 App Builder 라우트를 보류했습니다"
|
|
453
509
|
: "the request does not clearly need a dedicated App with durable workflow, state, editing, or automation",
|
|
454
510
|
terms: [],
|
|
511
|
+
strong: false,
|
|
455
512
|
};
|
|
456
513
|
}
|
|
457
|
-
|
|
458
|
-
const
|
|
514
|
+
// 헤이스택은 설치/임포트 시에만 변하므로 autoRouteAgent가 미리 계산해 넘긴다(중복 계산 제거).
|
|
515
|
+
const identityHay = (pre && pre.identityHay) || routeIdentityHaystack(agent);
|
|
516
|
+
const haystack = (pre && pre.haystack) || routeHaystack(agent);
|
|
459
517
|
let score = 0;
|
|
518
|
+
let strong = false; // 이름 언급/정체성 적중/큐레이션 힌트 — 데스크탑처럼 "이름/힌트급 증거"가 있어야 위임한다
|
|
460
519
|
const terms = [];
|
|
520
|
+
const seenNames = new Set();
|
|
461
521
|
for (const name of [agent.slug, agent.name, agent.name_en].filter(Boolean)) {
|
|
462
522
|
const n = routeNormalize(name);
|
|
463
523
|
// 4자 미만 일반 단어("team","agent" 등)가 프롬프트에 우연히 들어가 +20을 독식하지 않도록 가드.
|
|
464
|
-
|
|
524
|
+
// name === name_en 인 임포트 에이전트(appbridge 등)가 +20을 두 번 받지 않도록 정규화 기준 dedupe.
|
|
525
|
+
if (!n || n.length < 4 || seenNames.has(n)) continue;
|
|
526
|
+
seenNames.add(n);
|
|
527
|
+
if (promptText.includes(n)) {
|
|
465
528
|
score += 20;
|
|
466
529
|
terms.push(name);
|
|
530
|
+
strong = true;
|
|
467
531
|
}
|
|
468
532
|
}
|
|
469
533
|
for (const term of promptTerms) {
|
|
470
534
|
if (identityHay.includes(term)) {
|
|
471
535
|
score += 6; // 이름/태그라인 적중 = 그 에이전트의 정체성 자체를 부른 것
|
|
472
536
|
terms.push(term);
|
|
537
|
+
strong = true;
|
|
473
538
|
} else if (haystack.includes(term)) {
|
|
474
539
|
score += term.length >= 5 ? 3 : 2;
|
|
475
540
|
terms.push(term);
|
|
@@ -477,6 +542,7 @@ function scoreRouteAgent(prompt, promptTerms, agent, lang) {
|
|
|
477
542
|
}
|
|
478
543
|
const hint = routeHint(promptText, agent, lang);
|
|
479
544
|
score += hint.score;
|
|
545
|
+
if (hint.score) strong = true;
|
|
480
546
|
terms.push(...hint.terms);
|
|
481
547
|
const unique = [...new Set(terms)].slice(0, 6);
|
|
482
548
|
const reason = hint.reason || (lang === "ko"
|
|
@@ -486,13 +552,14 @@ function scoreRouteAgent(prompt, promptTerms, agent, lang) {
|
|
|
486
552
|
: unique.length
|
|
487
553
|
? `request terms ${unique.map((term) => `"${term}"`).join(", ")} best match this agent's role/triggers`
|
|
488
554
|
: "no specialist matched clearly, so the default project coordinator is safest");
|
|
489
|
-
return { agent, score, reason, terms: unique };
|
|
490
|
-
}
|
|
491
|
-
// 라우팅 확신
|
|
492
|
-
//
|
|
493
|
-
//
|
|
494
|
-
//
|
|
495
|
-
|
|
555
|
+
return { agent, score, reason, terms: unique, strong };
|
|
556
|
+
}
|
|
557
|
+
// 라우팅 확신 임계값 — 데스크탑 auto-router의 MIN_SPECIALIST_SCORE(10)와 동일 기준.
|
|
558
|
+
// 위임에는 점수뿐 아니라 strong 신호(이름 포함 +20 / 정체성 적중 +6 / 큐레이션 힌트 +12↑)가
|
|
559
|
+
// 반드시 있어야 한다. system_prompt 본문의 약한 단어 적중(+2~3)이 몇 개 쌓여도, strong 신호가
|
|
560
|
+
// 없으면 절대 위임하지 않는다. 미달이면 "직답"(에이전트·능력 라우팅 없음) — 일반 질문이
|
|
561
|
+
// Pitch Deck Architect 같은 무관 페르소나 + gemini 이미지 런타임으로 끌려가던 사고의 근본 수리.
|
|
562
|
+
const MIN_ROUTE_SCORE = 10;
|
|
496
563
|
function directRouteChoice(lang) {
|
|
497
564
|
const resolvedLang = lang || prefsLang();
|
|
498
565
|
return {
|
|
@@ -500,6 +567,7 @@ function directRouteChoice(lang) {
|
|
|
500
567
|
agent: null,
|
|
501
568
|
score: 0,
|
|
502
569
|
terms: [],
|
|
570
|
+
strong: false,
|
|
503
571
|
reason: resolvedLang === "ko"
|
|
504
572
|
? "특정 전문 에이전트가 필요 없는 일반 요청입니다"
|
|
505
573
|
: "this is a general request that needs no specialist agent",
|
|
@@ -521,6 +589,7 @@ function autoRouteAgent(db, prompt, lang) {
|
|
|
521
589
|
return {
|
|
522
590
|
agent: meta,
|
|
523
591
|
score: 1000,
|
|
592
|
+
strong: true,
|
|
524
593
|
reason:
|
|
525
594
|
resolvedLang === "ko"
|
|
526
595
|
? "새 에이전트/팀/회사를 만드는 요청이라 메타에이전트(빌더)로 라우팅했습니다"
|
|
@@ -532,13 +601,19 @@ function autoRouteAgent(db, prompt, lang) {
|
|
|
532
601
|
const agents = listRoutableAgents(db).filter((agent) => !NON_GENERIC_ROUTE_SLUGS.has(agent.slug));
|
|
533
602
|
if (!agents.length) return directRouteChoice(resolvedLang);
|
|
534
603
|
let terms = routeTokenize(prompt);
|
|
604
|
+
// 헤이스택은 한 번만 계산해 IDF와 스코어링 양쪽에서 재사용한다.
|
|
605
|
+
const hays = agents.map((agent) => ({ identityHay: routeIdentityHaystack(agent), haystack: routeHaystack(agent) }));
|
|
535
606
|
// IDF 근사 — 설치 에이전트 절반 이상의 haystack에 나오는 단어("ai","도구" 등)는 판별력이 없어 제외.
|
|
536
607
|
if (agents.length >= 3) {
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
608
|
+
terms = terms.filter((term) => hays.filter((h) => h.haystack.includes(term)).length * 2 <= agents.length);
|
|
609
|
+
}
|
|
610
|
+
const ranked = agents
|
|
611
|
+
.map((agent, i) => scoreRouteAgent(prompt, terms, agent, resolvedLang, hays[i]))
|
|
612
|
+
.sort((a, b) => b.score - a.score);
|
|
613
|
+
// 1위가 아니라 "임계값+strong을 모두 만족하는 최고 순위"를 뽑는다 — 장황한 프롬프트의
|
|
614
|
+
// 약한 단어 적중이 점수 1위를 먹어도, 자격 있는 전문 에이전트가 직답으로 밀려나지 않는다.
|
|
615
|
+
const pick = ranked.find((r) => r.score >= MIN_ROUTE_SCORE && r.strong);
|
|
616
|
+
if (pick) return pick;
|
|
542
617
|
return directRouteChoice(resolvedLang);
|
|
543
618
|
}
|
|
544
619
|
function autoRouteNote(choice, lang) {
|
|
@@ -597,6 +672,40 @@ function agentFolder(agent) {
|
|
|
597
672
|
if (exists(path.join(cloudRoot, CLOUD_RESTORE_MARKER_PATH))) return cloudRoot;
|
|
598
673
|
return path.join(userDataDir(), "agents", agent.slug);
|
|
599
674
|
}
|
|
675
|
+
function exactAgentBaseForExecution(db, agent, runtimeExperience = null) {
|
|
676
|
+
if (!agent || agent.builtin) return null;
|
|
677
|
+
const portableId = /^[A-Za-z0-9][A-Za-z0-9._:/@-]{2,255}$/;
|
|
678
|
+
let binding = null;
|
|
679
|
+
try {
|
|
680
|
+
if (tableExists(db, "installed_agent_hub_bindings")) {
|
|
681
|
+
binding = db.prepare(
|
|
682
|
+
"SELECT agent_definition_id,agent_release_id FROM installed_agent_hub_bindings WHERE installed_agent_id=?",
|
|
683
|
+
).get(agent.id) || null;
|
|
684
|
+
}
|
|
685
|
+
} catch { binding = null; }
|
|
686
|
+
const route = routesMap()[agent.id] || {};
|
|
687
|
+
const markerResult = terminalExperienceExchange.readExactLocalBaseMarker(agentFolder(agent), agent.slug);
|
|
688
|
+
const marker = markerResult.marker;
|
|
689
|
+
const rawHash = String(marker?.packageHash || route.packageHash || route.definitionHash || "").replace(/^sha256:/i, "").toLowerCase();
|
|
690
|
+
const packageHash = /^[a-f0-9]{64}$/.test(rawHash) ? `sha256:${rawHash}` : null;
|
|
691
|
+
const explicitDefinition = String(runtimeExperience?.agentDefinitionId || "");
|
|
692
|
+
const explicitRelease = String(runtimeExperience?.baseAgentReleaseId || "");
|
|
693
|
+
if (portableId.test(explicitDefinition) && portableId.test(explicitRelease)) {
|
|
694
|
+
return { agentDefinitionId: explicitDefinition, agentReleaseId: explicitRelease, packageHash, authority: "explicit-runtime-binding" };
|
|
695
|
+
}
|
|
696
|
+
if (binding && portableId.test(String(binding.agent_definition_id)) && portableId.test(String(binding.agent_release_id))) {
|
|
697
|
+
return { agentDefinitionId: binding.agent_definition_id, agentReleaseId: binding.agent_release_id, packageHash, authority: "installed-hub-binding" };
|
|
698
|
+
}
|
|
699
|
+
if (!packageHash) return null;
|
|
700
|
+
const definitionDigest = sha(`terminal-local-definition\0${agent.id}\0${agent.slug}`);
|
|
701
|
+
const releaseDigest = sha(`terminal-local-release\0${definitionDigest}\0${packageHash}`);
|
|
702
|
+
return {
|
|
703
|
+
agentDefinitionId: `local-agent-definition:${definitionDigest.slice(0, 32)}`,
|
|
704
|
+
agentReleaseId: `local-agent-release:${releaseDigest.slice(0, 32)}`,
|
|
705
|
+
packageHash,
|
|
706
|
+
authority: "exact-local-package-hash",
|
|
707
|
+
};
|
|
708
|
+
}
|
|
600
709
|
function agentSystemPromptCli(agent) {
|
|
601
710
|
return agent && agent.system_prompt ? agent.system_prompt : `You are ${agent?.name || "an Agentlas agent"}.`;
|
|
602
711
|
}
|
|
@@ -704,7 +813,7 @@ function buildImportSystemPrompt(dir, name, kind) {
|
|
|
704
813
|
}
|
|
705
814
|
function importLocalFolderCli(db, absPath) {
|
|
706
815
|
const dir = path.resolve(absPath);
|
|
707
|
-
if (!isDir(dir)) fail(
|
|
816
|
+
if (!isDir(dir)) fail(`Not a directory: ${absPath}`);
|
|
708
817
|
const labels = detectRuntimeLabels(dir);
|
|
709
818
|
const runtime = labels[0];
|
|
710
819
|
const kind = detectKind(dir);
|
|
@@ -754,9 +863,14 @@ function importLocalFolderCli(db, absPath) {
|
|
|
754
863
|
).run(id, slug, name, name, tagline, tagline, systemPrompt, envReqsJson, now, tone);
|
|
755
864
|
}
|
|
756
865
|
}
|
|
866
|
+
// detectKind 결과를 DB에도 기록 — needsImage의 팀 body-veto 등 능력 판정이
|
|
867
|
+
// 데스크탑이 써준 entity_kind에 무임승차하지 않고 터미널 단독 임포트에서도 성립한다.
|
|
868
|
+
if (columnExists(db, "installed_agents", "entity_kind")) {
|
|
869
|
+
db.prepare("UPDATE installed_agents SET entity_kind=? WHERE id=?").run(kind, id);
|
|
870
|
+
}
|
|
757
871
|
// 라우트 저장
|
|
758
872
|
routes[id] = { agentId: id, path: dir, runtime, labels, kind, importedAt: now };
|
|
759
|
-
|
|
873
|
+
writeJsonPrivateAtomicCli(path.join(userDataDir(), "agent-routes.json"), routes);
|
|
760
874
|
|
|
761
875
|
// 팀이면 회사(firm)로도 등록 → 앱 FIRMS 목록 + `agentlas firm <slug>` 사용 가능. slug 기준 멱등.
|
|
762
876
|
let firm = null;
|
|
@@ -804,15 +918,15 @@ function upsertLocalTeamFirmCli(db, dir, ceoAgentId, agentSlug, name, tagline) {
|
|
|
804
918
|
return { id, slug: firmSlug };
|
|
805
919
|
}
|
|
806
920
|
function cmdImport(db, absPath) {
|
|
807
|
-
if (!absPath) fail("
|
|
921
|
+
if (!absPath) fail("Usage: agentlas import <folder-path>");
|
|
808
922
|
const r = importLocalFolderCli(db, absPath);
|
|
809
|
-
out(`${r.updated ? "
|
|
923
|
+
out(`${r.updated ? "Updated" : "Imported"}: ${r.name} (${r.kind})`);
|
|
810
924
|
out(` slug: ${r.slug}`);
|
|
811
925
|
out(` runtime: ${r.runtime} [${r.labels.join(", ")}]`);
|
|
812
926
|
out(` path: ${r.path}`);
|
|
813
|
-
if (r.firmSlug) out(` firm: ${r.firmSlug} (
|
|
927
|
+
if (r.firmSlug) out(` firm: ${r.firmSlug} (registered in Firms — Desktop sidebar + 'agentlas firm ${r.firmSlug}')`);
|
|
814
928
|
out("");
|
|
815
|
-
out(
|
|
929
|
+
out(`Run: agentlas ${r.slug} "..." · agentlas run ${r.slug} "..." (run from the target project folder)`);
|
|
816
930
|
}
|
|
817
931
|
|
|
818
932
|
// ── Agentlas Cloud packaging / marketplace ────────────────────────────────
|
|
@@ -831,6 +945,7 @@ const CLOUD_AGENT_FILES = new Set(["AGENT.md", "AGENTS.md", "CLAUDE.md", "GEMINI
|
|
|
831
945
|
const CLOUD_SKIP_DIRS = new Set([".git", ".next", ".studio-runtime", ".turbo", "build", "coverage", "dist", "node_modules", "out", "release"]);
|
|
832
946
|
const CLOUD_BLOCKED_FILE_RE = [/^\.env(?:\..*)?$/i, /^id_rsa(?:\.pub)?$/i, /^credentials(?:\..*)?$/i, /^secrets?(?:\..*)?$/i, /^cloud-asset-state\.v1\.json$/i, /(?:^|[._-])service-account(?:[._-]|$)/i, /\.(?:key|pem|p12|pfx|mobileprovision)$/i];
|
|
833
947
|
const CLOUD_ROUTING_CARD_PATH = ".agentlas/routing-card.json";
|
|
948
|
+
const CLOUD_LOCAL_EXPERIENCE_LINEAGE_PATH = ".agentlas/experience-relations.jsonl";
|
|
834
949
|
const CLOUD_ROUTING_CARD_CAPABILITY_RE = /^[a-z][a-z0-9]*(_[a-z0-9]+)+$/;
|
|
835
950
|
const CLOUD_ROUTING_CARD_STATUSES = new Set(["draft", "searchable", "candidate", "routing_ready", "trusted"]);
|
|
836
951
|
const CLOUD_SECRET_RE = [
|
|
@@ -887,7 +1002,7 @@ function hubTimeoutError(kind, ms) {
|
|
|
887
1002
|
/** Hub/Cloud fetch + body reader. Headers 전 connect, chunk 사이 idle, 전 구간 total timeout. */
|
|
888
1003
|
async function fetchHubCli(url, init = {}, options = {}) {
|
|
889
1004
|
const fetchImpl = options.fetch || globalThis.fetch;
|
|
890
|
-
if (typeof fetchImpl !== "function") throw new Error("
|
|
1005
|
+
if (typeof fetchImpl !== "function") throw new Error("fetch is unavailable in this runtime.");
|
|
891
1006
|
const timeout = options.timeoutConfig ? directHubTimeoutConfig(options.timeoutConfig) : hubTimeoutConfig(options.env || process.env);
|
|
892
1007
|
const controller = new AbortController();
|
|
893
1008
|
const upstreamSignal = init.signal;
|
|
@@ -951,7 +1066,7 @@ async function fetchHubCli(url, init = {}, options = {}) {
|
|
|
951
1066
|
} else {
|
|
952
1067
|
const raw = Buffer.from(await Promise.race([response.arrayBuffer(), terminal]));
|
|
953
1068
|
bytes = raw.length;
|
|
954
|
-
if (bytes > HUB_RESPONSE_MAX_BYTES) throw new Error(`Hub
|
|
1069
|
+
if (bytes > HUB_RESPONSE_MAX_BYTES) throw new Error(`Hub response exceeds the allowed size (${HUB_RESPONSE_MAX_BYTES} bytes).`);
|
|
955
1070
|
chunks.push(raw);
|
|
956
1071
|
}
|
|
957
1072
|
if (idleTimer) clearTimeout(idleTimer);
|
|
@@ -976,7 +1091,7 @@ function parseHubJsonCli(response, label) {
|
|
|
976
1091
|
try {
|
|
977
1092
|
return JSON.parse(response.text || "null");
|
|
978
1093
|
} catch {
|
|
979
|
-
throw new Error(`${label}
|
|
1094
|
+
throw new Error(`${label} returned invalid JSON.`);
|
|
980
1095
|
}
|
|
981
1096
|
}
|
|
982
1097
|
|
|
@@ -1050,6 +1165,7 @@ async function cmdCloud(db, args, runtimeOverride) {
|
|
|
1050
1165
|
" list [--json] list packages in your private Agent Cloud",
|
|
1051
1166
|
" restore <slug> [--json] restore an owned Cloud package on this machine",
|
|
1052
1167
|
" install <slug> compatibility alias: install from the public Hub",
|
|
1168
|
+
" plugin add <slug> install a Hub plugin (MCP servers)",
|
|
1053
1169
|
" delete <slug> [--scope owner-private|hub-public] [--json]",
|
|
1054
1170
|
" conditionally delete one exact observed Cloud revision",
|
|
1055
1171
|
" search \"<what you need>\" [--limit 10]",
|
|
@@ -1068,7 +1184,7 @@ async function cmdCloud(db, args, runtimeOverride) {
|
|
|
1068
1184
|
const result = await listOwnedCloudAgentsCli(Number(flags.limit || 100));
|
|
1069
1185
|
if (flags.json) return out(JSON.stringify(result, null, 2));
|
|
1070
1186
|
const agents = Array.isArray(result.results) ? result.results : [];
|
|
1071
|
-
if (!agents.length) return out("Private Agent Cloud
|
|
1187
|
+
if (!agents.length) return out("No agents are stored in Private Agent Cloud.");
|
|
1072
1188
|
for (const agent of agents) out(`${agent.slug}\t${agent.name || agent.nameEn || agent.slug}\t${agent.entityKind || "agent"}`);
|
|
1073
1189
|
return;
|
|
1074
1190
|
}
|
|
@@ -1164,8 +1280,8 @@ async function cmdCloud(db, args, runtimeOverride) {
|
|
|
1164
1280
|
async function packageCloudAgentCli(db, root, opts) {
|
|
1165
1281
|
const requestedRoot = path.resolve(root);
|
|
1166
1282
|
let st;
|
|
1167
|
-
try { st = fs.lstatSync(requestedRoot); } catch { throw new Error(
|
|
1168
|
-
if (!st.isDirectory() || st.isSymbolicLink()) throw new Error(
|
|
1283
|
+
try { st = fs.lstatSync(requestedRoot); } catch { throw new Error(`Folder not found: ${root}`); }
|
|
1284
|
+
if (!st.isDirectory() || st.isSymbolicLink()) throw new Error(`Not a real directory: ${root}`);
|
|
1169
1285
|
const rootPath = fs.realpathSync.native(requestedRoot);
|
|
1170
1286
|
const visibility = opts.visibility || "private-link";
|
|
1171
1287
|
const isPublicHubPublish = visibility === "marketplace";
|
|
@@ -1620,6 +1736,12 @@ function scanCloudFolderCli(rootPath) {
|
|
|
1620
1736
|
if (entry.name.startsWith("._")) continue;
|
|
1621
1737
|
const abs = path.join(dir, entry.name);
|
|
1622
1738
|
const rel = path.relative(rootPath, abs).split(path.sep).join("/");
|
|
1739
|
+
if (cloudIsLocalExperienceLineagePath(rel)) {
|
|
1740
|
+
let bytes = 0;
|
|
1741
|
+
try { bytes = Number(fs.lstatSync(abs).size) || 0; } catch { /* excluded local state */ }
|
|
1742
|
+
files.push({ path: rel, bytes, sha256: "", kind: "text", included: false, reason: "experience-lineage-separate-asset" });
|
|
1743
|
+
continue;
|
|
1744
|
+
}
|
|
1623
1745
|
if (cloudPortablePathKey(rel) === cloudPortablePathKey(CLOUD_RESTORE_MARKER_PATH)) {
|
|
1624
1746
|
// Local restore/CAS metadata is runtime state, never portable asset
|
|
1625
1747
|
// data, but it must be captured with the same no-follow stability gate.
|
|
@@ -1858,19 +1980,19 @@ function cloudCasResponseErrorCli(response, label) {
|
|
|
1858
1980
|
let body = null;
|
|
1859
1981
|
try { body = JSON.parse(response.text || "null"); } catch { /* generic below */ }
|
|
1860
1982
|
const code = body && typeof body.code === "string" ? body.code : "cloud_request_failed";
|
|
1861
|
-
let message = `${label}
|
|
1983
|
+
let message = `${label} failed with HTTP ${response.status}`;
|
|
1862
1984
|
if (response.status === 412 && code === "cloud_agent_revision_conflict") {
|
|
1863
1985
|
const current = body && body.current ? body.current : body && body.conflict && body.conflict.current;
|
|
1864
1986
|
message = current
|
|
1865
1987
|
? `다른 PC에서 이 Agent Cloud 자산이 변경되었습니다. 자동 덮어쓰기는 중단했습니다. \`agentlas cloud list\`로 최신 revision을 확인하고 \`agentlas cloud restore ${current.slug || "<slug>"}\`로 복원한 뒤 변경 사항을 병합하세요.`
|
|
1866
1988
|
: "이 Agent Cloud 자산은 다른 PC에서 삭제되었거나 다른 식별자로 다시 생성되었습니다. 자동 재생성은 중단했습니다. `agentlas cloud list`로 현재 상태를 확인하세요.";
|
|
1867
1989
|
} else if (response.status === 428 && code === "client_upgrade_required") {
|
|
1868
|
-
message = "
|
|
1990
|
+
message = "No base revision is available to safely update the existing Cloud asset. The server revision will not be copied automatically. Check `agentlas cloud list`, restore with `agentlas cloud restore <slug>`, then save again.";
|
|
1869
1991
|
} else if (response.status === 503 && code === "cloud_mutations_maintenance") {
|
|
1870
1992
|
const retryAfter = response.headers && typeof response.headers.get === "function" ? response.headers.get("retry-after") : null;
|
|
1871
|
-
message = `Agent Cloud
|
|
1993
|
+
message = `Agent Cloud save/delete is temporarily under maintenance${retryAfter ? ` (retry in about ${retryAfter} seconds)` : ""}. Read, list, and restore remain available.`;
|
|
1872
1994
|
} else if (body && typeof body.error === "string") {
|
|
1873
|
-
message = `${label}
|
|
1995
|
+
message = `${label} failed with HTTP ${response.status}: ${body.error.slice(0, 300)}`;
|
|
1874
1996
|
}
|
|
1875
1997
|
const error = new Error(message);
|
|
1876
1998
|
error.code = code;
|
|
@@ -1882,8 +2004,8 @@ function cloudCasResponseErrorCli(response, label) {
|
|
|
1882
2004
|
|
|
1883
2005
|
async function registerCloudAgentCli(manifest, bundlePath, review, visibility, options = {}) {
|
|
1884
2006
|
const cookie = await cloudSessionCookieCli();
|
|
1885
|
-
if (!cookie) fail("
|
|
1886
|
-
if (typeof fetch !== "function") fail("
|
|
2007
|
+
if (!cookie) fail("Agent Cloud sign-in is required. Sign in through Desktop or set AGENTLAS_SESSION.");
|
|
2008
|
+
if (typeof fetch !== "function") fail("fetch is unavailable in this runtime (run through the app runtime).");
|
|
1887
2009
|
const base = (process.env.AGENTLAS_WEB_BASE_URL || "https://agentlas.cloud").replace(/\/$/, "");
|
|
1888
2010
|
const bundle = JSON.parse(fs.readFileSync(bundlePath, "utf8"));
|
|
1889
2011
|
const expectedScope = cloudScopeForVisibility(visibility);
|
|
@@ -1956,8 +2078,8 @@ async function deleteCloudAgentCli(slug, options = {}) {
|
|
|
1956
2078
|
const safeSlug = String(slug || "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
|
|
1957
2079
|
if (!safeSlug) fail("usage: agentlas cloud delete <slug> [--json]");
|
|
1958
2080
|
const cookie = await cloudSessionCookieCli();
|
|
1959
|
-
if (!cookie) fail("
|
|
1960
|
-
if (typeof fetch !== "function") fail("
|
|
2081
|
+
if (!cookie) fail("Agent Cloud sign-in is required. Sign in through Desktop or set AGENTLAS_SESSION.");
|
|
2082
|
+
if (typeof fetch !== "function") fail("fetch is unavailable in this runtime (run through the app runtime).");
|
|
1961
2083
|
const scope = options.scope == null ? null : normalizeCloudScopeFlagCli(options.scope);
|
|
1962
2084
|
if (options.scope != null && !scope) throw new Error("--scope must be owner-private or hub-public");
|
|
1963
2085
|
const localEntry = findCloudAssetDescriptorCli(safeSlug, scope);
|
|
@@ -2060,28 +2182,121 @@ async function cloudSessionCookieCli() {
|
|
|
2060
2182
|
async function cmdCloudInstall(db, slug) {
|
|
2061
2183
|
if (!slug) fail("usage: agentlas cloud install <slug>");
|
|
2062
2184
|
const listing = await fetchCloudManifestCli(slug);
|
|
2063
|
-
if (!listing) fail(`Hub agent
|
|
2185
|
+
if (!listing) fail(`Hub agent not found: ${slug}`);
|
|
2064
2186
|
if (listing.delivery && listing.delivery.mode === "call_only") {
|
|
2065
|
-
fail(
|
|
2187
|
+
fail(`This Hub agent is call-only and cannot be installed from source. Run: agentlas call ${slug}`);
|
|
2066
2188
|
}
|
|
2067
2189
|
const agent = persistCloudListingCli(db, listing);
|
|
2068
2190
|
out(`✓ Hub installed ${agent.slug} — ${agent.name}`);
|
|
2069
2191
|
if (agent.localPath) out(` files: ${agent.localPath}`);
|
|
2070
2192
|
}
|
|
2071
2193
|
|
|
2194
|
+
// ── Hub 플러그인 설치 ────────────────────────────────────────────────────────
|
|
2195
|
+
// 서버는 처음부터 준비돼 있었다: /api/plugins/<slug>가 agentlas.plugin/v1 매니페스트를 주고,
|
|
2196
|
+
// 그 라우트 주석이 이 CLI(`agentlas plugin add <slug>`)를 소비자로 지목한다. 그런데 이 명령이
|
|
2197
|
+
// 구현된 적이 없어서, 카탈로그 146개가 전부 "존재하지 않는 설치 명령"을 광고하고 있었다.
|
|
2198
|
+
// (`agentlas install`은 marketplace.get_manifest{kind:"agent"} 고정이라 플러그인엔 안 먹는다.)
|
|
2199
|
+
async function fetchPluginManifestCli(slug) {
|
|
2200
|
+
const base = (process.env.AGENTLAS_WEB_BASE_URL || "https://agentlas.cloud").replace(/\/$/, "");
|
|
2201
|
+
const resp = await fetchHubCli(`${base}/api/plugins/${encodeURIComponent(slug)}`, {
|
|
2202
|
+
headers: { accept: "application/json" },
|
|
2203
|
+
});
|
|
2204
|
+
if (resp.status === 404) return null;
|
|
2205
|
+
if (!resp.ok) fail(`plugin lookup failed with HTTP ${resp.status}`);
|
|
2206
|
+
const manifest = parseHubJsonCli(resp, "plugin manifest");
|
|
2207
|
+
if (!manifest || manifest.schema !== "agentlas.plugin/v1") {
|
|
2208
|
+
fail(`Unexpected plugin manifest schema for ${slug}.`);
|
|
2209
|
+
}
|
|
2210
|
+
return manifest;
|
|
2211
|
+
}
|
|
2212
|
+
|
|
2213
|
+
/** 매니페스트의 mcp[] 항목을 mcp_servers 행으로 정규화. stdio(command)와 remote(url)를 구분한다. */
|
|
2214
|
+
function pluginMcpRowCli(slug, entry, index) {
|
|
2215
|
+
const source = typeof entry?.source === "string" ? entry.source.trim() : "";
|
|
2216
|
+
const name = (typeof entry?.name === "string" && entry.name.trim()) || `${slug}-${index + 1}`;
|
|
2217
|
+
const remote = /^https?:\/\//i.test(source);
|
|
2218
|
+
// 원격은 URL, stdio는 실행 커맨드다. 둘을 섞으면 codex config.toml 스키마 위반으로
|
|
2219
|
+
// 런타임이 통째로 죽는다(Runtime Doctor가 반복해서 잡던 사고 계열).
|
|
2220
|
+
if (!remote && !source) return null;
|
|
2221
|
+
const argv = remote ? [] : source.split(/\s+/).filter(Boolean);
|
|
2222
|
+
return {
|
|
2223
|
+
id: require("node:crypto").randomUUID(),
|
|
2224
|
+
catalogId: `hub:${slug}:${name}`,
|
|
2225
|
+
name,
|
|
2226
|
+
transport: remote ? "http" : "stdio",
|
|
2227
|
+
command: remote ? null : (argv[0] ?? null),
|
|
2228
|
+
argsJson: JSON.stringify(remote ? [] : argv.slice(1)),
|
|
2229
|
+
url: remote ? source : null,
|
|
2230
|
+
envKeysJson: JSON.stringify(
|
|
2231
|
+
Array.isArray(entry?.envKeys) ? entry.envKeys.filter((key) => typeof key === "string") : [],
|
|
2232
|
+
),
|
|
2233
|
+
};
|
|
2234
|
+
}
|
|
2235
|
+
|
|
2236
|
+
async function cmdPluginAdd(db, slug) {
|
|
2237
|
+
if (!slug) fail('usage: agentlas plugin add <slug> (run agentlas plugin list first)');
|
|
2238
|
+
const manifest = await fetchPluginManifestCli(slug);
|
|
2239
|
+
if (!manifest) fail(`Hub plugin not found: ${slug}`);
|
|
2240
|
+
const entries = Array.isArray(manifest.mcp) ? manifest.mcp : [];
|
|
2241
|
+
const rows = entries.map((entry, index) => pluginMcpRowCli(slug, entry, index)).filter(Boolean);
|
|
2242
|
+
if (!rows.length) {
|
|
2243
|
+
// 설치할 MCP 서버가 없으면 조용히 성공했다고 하지 않는다 — 사용자는 이 플러그인이
|
|
2244
|
+
// 붙었다고 믿고 도구를 기대하게 된다.
|
|
2245
|
+
fail(
|
|
2246
|
+
`${slug} ships no MCP server to install (skills-only or source-link plugin). ` +
|
|
2247
|
+
`Nothing was registered. See: ${manifest.source?.repo || manifest.source?.homepage || "the plugin page"}`,
|
|
2248
|
+
);
|
|
2249
|
+
}
|
|
2250
|
+
let installed = 0;
|
|
2251
|
+
let reused = 0;
|
|
2252
|
+
for (const row of rows) {
|
|
2253
|
+
const existing = db.prepare("SELECT id FROM mcp_servers WHERE catalog_id = ? LIMIT 1").get(row.catalogId);
|
|
2254
|
+
if (existing) { reused += 1; continue; } // 멱등: 재설치가 중복 행을 만들지 않는다
|
|
2255
|
+
db.prepare(
|
|
2256
|
+
`INSERT INTO mcp_servers (id, catalog_id, name, name_en, transport, command, args_json, url, env_keys_json, enabled, installed_at)
|
|
2257
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, 1, ?)`,
|
|
2258
|
+
).run(
|
|
2259
|
+
row.id, row.catalogId, row.name, row.name, row.transport,
|
|
2260
|
+
row.command, row.argsJson, row.url, row.envKeysJson, new Date().toISOString(),
|
|
2261
|
+
);
|
|
2262
|
+
installed += 1;
|
|
2263
|
+
}
|
|
2264
|
+
out(`✓ Plugin installed ${manifest.slug} — ${manifest.name}`);
|
|
2265
|
+
out(` MCP servers: ${installed} added${reused ? `, ${reused} already present` : ""}`);
|
|
2266
|
+
const authKind = manifest.auth?.kind;
|
|
2267
|
+
if (authKind && authKind !== "none") {
|
|
2268
|
+
out(` ⚠ Requires ${authKind} — set credentials before use (agentlas creds).`);
|
|
2269
|
+
}
|
|
2270
|
+
if (Array.isArray(manifest.skills) && manifest.skills.length) {
|
|
2271
|
+
out(` skills declared: ${manifest.skills.map((skill) => skill.name).filter(Boolean).join(", ")}`);
|
|
2272
|
+
}
|
|
2273
|
+
out(" Only full-access turns wire active stdio servers into the runtime (agentlas mcp).");
|
|
2274
|
+
}
|
|
2275
|
+
|
|
2276
|
+
async function cmdPluginList() {
|
|
2277
|
+
const result = await callAgentlasMcpToolCli("marketplace.list_plugins", {});
|
|
2278
|
+
const plugins = (result && (result.plugins || result.results)) || [];
|
|
2279
|
+
if (!plugins.length) return out("No Hub plugins are available.");
|
|
2280
|
+
for (const plugin of plugins.slice(0, 60)) {
|
|
2281
|
+
out(`${String(plugin.slug || "").padEnd(32).slice(0, 32)} ${String(plugin.name || "").slice(0, 44)}`);
|
|
2282
|
+
}
|
|
2283
|
+
out("");
|
|
2284
|
+
out("Install: agentlas plugin add <slug>");
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2072
2287
|
async function callAgentlasMcpToolCli(name, args, { requireSession = false } = {}) {
|
|
2073
|
-
if (typeof fetch !== "function") fail("
|
|
2288
|
+
if (typeof fetch !== "function") fail("fetch is unavailable in this runtime (run through the app runtime).");
|
|
2074
2289
|
const base = process.env.AGENTLAS_MCP_BASE_URL || "https://agentlas.cloud/api/mcp/v1";
|
|
2075
2290
|
const headers = { "content-type": "application/json" };
|
|
2076
2291
|
const cookie = await cloudSessionCookieCli();
|
|
2077
|
-
if (requireSession && !cookie) fail("Agent Cloud
|
|
2292
|
+
if (requireSession && !cookie) fail("Agent Cloud sign-in is required. Run `agentlas login` first.");
|
|
2078
2293
|
if (cookie) headers.cookie = cookie;
|
|
2079
2294
|
const resp = await fetchHubCli(`${base.replace(/\/$/, "")}/tools/call`, {
|
|
2080
2295
|
method: "POST",
|
|
2081
2296
|
headers,
|
|
2082
2297
|
body: JSON.stringify({ method: name, params: { name, arguments: args || {} } }),
|
|
2083
2298
|
});
|
|
2084
|
-
if (!resp.ok) fail(`${name}
|
|
2299
|
+
if (!resp.ok) fail(`${name} failed with HTTP ${resp.status}`);
|
|
2085
2300
|
const json = parseHubJsonCli(resp, name);
|
|
2086
2301
|
if (json.error) fail(`${name}: ${json.error.message || "unknown error"}`);
|
|
2087
2302
|
return json.result || null;
|
|
@@ -2364,6 +2579,17 @@ function persistCloudListingCli(db, listing) {
|
|
|
2364
2579
|
throw error;
|
|
2365
2580
|
}
|
|
2366
2581
|
const localPath = restore?.path || null;
|
|
2582
|
+
// entity_kind 기록 — needsImage의 팀 body-veto가 로컬 폴더 임포트(detectKind)뿐 아니라
|
|
2583
|
+
// 클라우드/Hub 소스 설치 팀에도 걸리게 한다. 안 하면 팀 CEO 두뇌의 부서 키워드
|
|
2584
|
+
// ("Design HQ" 등)로 needsImage가 참이 되어 세션 런타임이 통째로 gemini로 하이재킹된다.
|
|
2585
|
+
// Hub가 준 entityKind를 우선하고, 없으면 materialize된 팩 폴더 구조로 판정한다.
|
|
2586
|
+
if (columnExists(db, "installed_agents", "entity_kind")) {
|
|
2587
|
+
let kind = String(listing.entityKind || "").toLowerCase();
|
|
2588
|
+
if (kind !== "team" && kind !== "agent") {
|
|
2589
|
+
kind = localPath && fs.existsSync(localPath) ? detectKind(localPath) : "agent";
|
|
2590
|
+
}
|
|
2591
|
+
db.prepare("UPDATE installed_agents SET entity_kind=? WHERE id=?").run(kind, id);
|
|
2592
|
+
}
|
|
2367
2593
|
return existing
|
|
2368
2594
|
? { ...existing, slug, name: dbExpected.name, ...(localPath ? { localPath } : {}) }
|
|
2369
2595
|
: { id, slug, name: dbExpected.name, ...(localPath ? { localPath } : {}) };
|
|
@@ -2709,6 +2935,13 @@ function printCloudPackageResult(result) {
|
|
|
2709
2935
|
function cloudPackageSnapshot(files) {
|
|
2710
2936
|
return new Map(files.map((file) => [file.path, file]));
|
|
2711
2937
|
}
|
|
2938
|
+
function cloudIsLocalExperienceLineagePath(value) {
|
|
2939
|
+
const normalized = cloudPortablePathKey(String(value || "").replace(/\\/g, "/"));
|
|
2940
|
+
const canonical = cloudPortablePathKey(CLOUD_LOCAL_EXPERIENCE_LINEAGE_PATH);
|
|
2941
|
+
return normalized === canonical
|
|
2942
|
+
|| normalized.startsWith(`${canonical}.`)
|
|
2943
|
+
|| normalized.startsWith(cloudPortablePathKey(".agentlas/.experience-relations.jsonl."));
|
|
2944
|
+
}
|
|
2712
2945
|
function cloudReadPublicCareerCard(snapshot, findings) {
|
|
2713
2946
|
const relativePath = ".agentlas/public-career-card.json";
|
|
2714
2947
|
const file = snapshot.get(relativePath);
|
|
@@ -2901,7 +3134,7 @@ function cloudHashPackage(files, version = CLOUD_PACKAGE_HASH_V1) {
|
|
|
2901
3134
|
// 서버 package-contract.ts와 바이트 동일해야 한다: 경로 코드포인트 순 정렬.
|
|
2902
3135
|
// 정렬 없이 스캔 순서로 해시하면 대소문자 혼합 경로 패키지(AGENTS.md + agents/…)가
|
|
2903
3136
|
// 전부 package_hash_mismatch로 거절된다(2026-07-02 근본 수정).
|
|
2904
|
-
for (const file of [...files].sort(cloudCodePointPathOrder)) {
|
|
3137
|
+
for (const file of [...files].filter((file) => !cloudIsLocalExperienceLineagePath(file.path)).sort(cloudCodePointPathOrder)) {
|
|
2905
3138
|
h.update(file.path);
|
|
2906
3139
|
h.update("\0");
|
|
2907
3140
|
h.update(file.sha256);
|
|
@@ -6986,6 +7219,21 @@ function readJsonSafeCli(filePath, fallback) {
|
|
|
6986
7219
|
function writeJsonSafeCli(filePath, value) {
|
|
6987
7220
|
fs.writeFileSync(filePath, JSON.stringify(value, null, 2) + "\n", "utf8");
|
|
6988
7221
|
}
|
|
7222
|
+
// 원자적(temp+rename) + 소유자 전용(0600) JSON 쓰기. 세션 ID/경로 등 민감 상태 파일용:
|
|
7223
|
+
// (1) 크래시 중간 쓰기로 JSON이 깨져 routesMap()이 {}를 돌려주며 임포트 매핑을 통째로 잃던 사고,
|
|
7224
|
+
// (2) 기본 umask(0644)로 cli-sessions.json/agent-routes.json이 world-readable이던 정보 노출을 함께 막는다.
|
|
7225
|
+
function writeJsonPrivateAtomicCli(filePath, value) {
|
|
7226
|
+
const dir = path.dirname(filePath);
|
|
7227
|
+
const tmp = path.join(dir, `.${path.basename(filePath)}.${process.pid}.tmp`);
|
|
7228
|
+
fs.writeFileSync(tmp, JSON.stringify(value, null, 2) + "\n", { encoding: "utf8", mode: 0o600 });
|
|
7229
|
+
try {
|
|
7230
|
+
fs.renameSync(tmp, filePath);
|
|
7231
|
+
} catch (e) {
|
|
7232
|
+
try { fs.unlinkSync(tmp); } catch { /* ignore */ }
|
|
7233
|
+
throw e;
|
|
7234
|
+
}
|
|
7235
|
+
try { fs.chmodSync(filePath, 0o600); } catch { /* 일부 FS는 chmod 미지원 — best-effort */ }
|
|
7236
|
+
}
|
|
6989
7237
|
|
|
6990
7238
|
function ontologySourceManifestSkeletonCli(root) {
|
|
6991
7239
|
return {
|
|
@@ -7528,38 +7776,320 @@ function contextLine(json) {
|
|
|
7528
7776
|
return "";
|
|
7529
7777
|
}
|
|
7530
7778
|
}
|
|
7531
|
-
|
|
7532
|
-
|
|
7533
|
-
|
|
7534
|
-
|
|
7779
|
+
const AGENTLAS_PROJECT_STATE_IGNORE_START = "# >>> agentlas local project state >>>";
|
|
7780
|
+
const AGENTLAS_PROJECT_STATE_IGNORE_END = "# <<< agentlas local project state <<<";
|
|
7781
|
+
const AGENTLAS_GITIGNORE_MAX_BYTES = 1024 * 1024;
|
|
7782
|
+
const projectBootstrapStates = new Map();
|
|
7783
|
+
|
|
7784
|
+
function terminalProjectCandidateCli(projectPath) {
|
|
7535
7785
|
try {
|
|
7536
|
-
const
|
|
7537
|
-
|
|
7538
|
-
|
|
7539
|
-
|
|
7540
|
-
|
|
7541
|
-
|
|
7542
|
-
|
|
7543
|
-
|
|
7786
|
+
const root = path.resolve(projectPath || process.cwd());
|
|
7787
|
+
const unsafe = new Set([
|
|
7788
|
+
path.resolve(os.homedir()),
|
|
7789
|
+
path.parse(root).root,
|
|
7790
|
+
path.resolve(userDataDir()),
|
|
7791
|
+
path.resolve(runCwd()),
|
|
7792
|
+
]);
|
|
7793
|
+
if (unsafe.has(root)) return null;
|
|
7794
|
+
const stat = fs.statSync(root);
|
|
7795
|
+
if (!stat.isDirectory()) return null;
|
|
7796
|
+
return root;
|
|
7797
|
+
} catch {
|
|
7798
|
+
return null;
|
|
7799
|
+
}
|
|
7800
|
+
}
|
|
7801
|
+
|
|
7802
|
+
function assertNoSymlinkInAgentlasStateCli(stateDir) {
|
|
7803
|
+
const pending = [stateDir];
|
|
7804
|
+
let visited = 0;
|
|
7805
|
+
while (pending.length && visited < 4096) {
|
|
7806
|
+
const current = pending.pop();
|
|
7807
|
+
visited += 1;
|
|
7808
|
+
let stat;
|
|
7809
|
+
try { stat = fs.lstatSync(current); } catch (error) {
|
|
7810
|
+
if (error && error.code === "ENOENT") continue;
|
|
7811
|
+
throw error;
|
|
7544
7812
|
}
|
|
7545
|
-
if (
|
|
7546
|
-
|
|
7547
|
-
|
|
7548
|
-
activatedAt = now;
|
|
7813
|
+
if (stat.isSymbolicLink()) throw new Error(".agentlas local state must not contain symbolic links");
|
|
7814
|
+
if (stat.isDirectory()) {
|
|
7815
|
+
for (const entry of fs.readdirSync(current)) pending.push(path.join(current, entry));
|
|
7549
7816
|
}
|
|
7550
|
-
|
|
7551
|
-
|
|
7817
|
+
}
|
|
7818
|
+
if (pending.length) throw new Error(".agentlas local state exceeds the safe bootstrap inspection limit");
|
|
7552
7819
|
}
|
|
7553
|
-
|
|
7554
|
-
function
|
|
7820
|
+
|
|
7821
|
+
function readRegularUtf8FileNoFollowCli(filePath, maxBytes = AGENTLAS_GITIGNORE_MAX_BYTES) {
|
|
7822
|
+
let before;
|
|
7823
|
+
try { before = fs.lstatSync(filePath); } catch (error) {
|
|
7824
|
+
if (error && error.code === "ENOENT") return { exists: false, content: "", mode: 0o644, stat: null };
|
|
7825
|
+
throw error;
|
|
7826
|
+
}
|
|
7827
|
+
if (before.isSymbolicLink() || !before.isFile()) throw new Error(".gitignore must be a regular non-symbolic-link file");
|
|
7828
|
+
if (before.size > maxBytes) throw new Error(`.gitignore exceeds the ${maxBytes}-byte safe bootstrap limit`);
|
|
7829
|
+
|
|
7830
|
+
const noFollow = fs.constants.O_NOFOLLOW || 0;
|
|
7831
|
+
let fd;
|
|
7555
7832
|
try {
|
|
7556
|
-
|
|
7557
|
-
|
|
7558
|
-
|
|
7559
|
-
|
|
7560
|
-
}
|
|
7833
|
+
fd = fs.openSync(filePath, fs.constants.O_RDONLY | noFollow);
|
|
7834
|
+
} catch (error) {
|
|
7835
|
+
if (process.platform !== "win32" || !noFollow || !["EINVAL", "ENOTSUP"].includes(error && error.code)) throw error;
|
|
7836
|
+
fd = fs.openSync(filePath, fs.constants.O_RDONLY);
|
|
7837
|
+
}
|
|
7838
|
+
try {
|
|
7839
|
+
const opened = fs.fstatSync(fd);
|
|
7840
|
+
if (!opened.isFile()) throw new Error(".gitignore changed type during bootstrap");
|
|
7841
|
+
if (opened.size > maxBytes) throw new Error(`.gitignore exceeds the ${maxBytes}-byte safe bootstrap limit`);
|
|
7842
|
+
if (
|
|
7843
|
+
Number.isFinite(before.dev) && Number.isFinite(before.ino) &&
|
|
7844
|
+
(before.dev !== opened.dev || before.ino !== opened.ino)
|
|
7845
|
+
) {
|
|
7846
|
+
throw new Error(".gitignore changed during bootstrap");
|
|
7847
|
+
}
|
|
7848
|
+
const chunks = [];
|
|
7849
|
+
let total = 0;
|
|
7850
|
+
while (total <= maxBytes) {
|
|
7851
|
+
const buffer = Buffer.allocUnsafe(Math.min(64 * 1024, maxBytes + 1 - total));
|
|
7852
|
+
const count = fs.readSync(fd, buffer, 0, buffer.length, null);
|
|
7853
|
+
if (!count) break;
|
|
7854
|
+
chunks.push(buffer.subarray(0, count));
|
|
7855
|
+
total += count;
|
|
7856
|
+
}
|
|
7857
|
+
if (total > maxBytes) throw new Error(`.gitignore exceeds the ${maxBytes}-byte safe bootstrap limit`);
|
|
7858
|
+
const after = fs.fstatSync(fd);
|
|
7859
|
+
if (after.size !== opened.size || after.mtimeMs !== opened.mtimeMs) throw new Error(".gitignore changed while it was being read");
|
|
7860
|
+
let content;
|
|
7861
|
+
try { content = new TextDecoder("utf-8", { fatal: true }).decode(Buffer.concat(chunks, total)); } catch {
|
|
7862
|
+
throw new Error(".gitignore must contain valid UTF-8 text");
|
|
7863
|
+
}
|
|
7864
|
+
return { exists: true, content, mode: before.mode & 0o777, stat: before };
|
|
7865
|
+
} finally {
|
|
7866
|
+
fs.closeSync(fd);
|
|
7867
|
+
}
|
|
7868
|
+
}
|
|
7869
|
+
|
|
7870
|
+
function assertFileSnapshotUnchangedCli(filePath, snapshot) {
|
|
7871
|
+
if (!snapshot.exists) {
|
|
7872
|
+
try {
|
|
7873
|
+
fs.lstatSync(filePath);
|
|
7874
|
+
throw new Error(".gitignore appeared during bootstrap");
|
|
7875
|
+
} catch (error) {
|
|
7876
|
+
if (error && error.code === "ENOENT") return;
|
|
7877
|
+
throw error;
|
|
7878
|
+
}
|
|
7879
|
+
}
|
|
7880
|
+
const current = fs.lstatSync(filePath);
|
|
7881
|
+
if (current.isSymbolicLink() || !current.isFile()) throw new Error(".gitignore changed type during bootstrap");
|
|
7882
|
+
const original = snapshot.stat;
|
|
7883
|
+
if (
|
|
7884
|
+
!original || current.dev !== original.dev || current.ino !== original.ino ||
|
|
7885
|
+
current.size !== original.size || current.mtimeMs !== original.mtimeMs
|
|
7886
|
+
) {
|
|
7887
|
+
throw new Error(".gitignore changed during bootstrap");
|
|
7888
|
+
}
|
|
7889
|
+
}
|
|
7890
|
+
|
|
7891
|
+
function replaceRegularFileCli(tempPath, destinationPath, snapshot) {
|
|
7892
|
+
try {
|
|
7893
|
+
fs.renameSync(tempPath, destinationPath);
|
|
7894
|
+
return;
|
|
7895
|
+
} catch (error) {
|
|
7896
|
+
if (process.platform !== "win32" || !snapshot.exists || !["EEXIST", "EPERM", "EACCES"].includes(error && error.code)) {
|
|
7897
|
+
throw error;
|
|
7898
|
+
}
|
|
7899
|
+
}
|
|
7900
|
+
|
|
7901
|
+
// Windows can reject replacement of an existing file. Keep a same-directory
|
|
7902
|
+
// rollback copy so an interrupted replacement never silently loses user rules.
|
|
7903
|
+
assertFileSnapshotUnchangedCli(destinationPath, snapshot);
|
|
7904
|
+
const backup = `${destinationPath}.agentlas-${process.pid}-${crypto.randomUUID()}.bak`;
|
|
7905
|
+
fs.renameSync(destinationPath, backup);
|
|
7906
|
+
try {
|
|
7907
|
+
fs.renameSync(tempPath, destinationPath);
|
|
7908
|
+
} catch (error) {
|
|
7909
|
+
try {
|
|
7910
|
+
if (!fs.existsSync(destinationPath)) fs.renameSync(backup, destinationPath);
|
|
7911
|
+
} catch { /* preserve the original error and leave the backup recoverable */ }
|
|
7912
|
+
throw error;
|
|
7913
|
+
}
|
|
7914
|
+
try { fs.unlinkSync(backup); } catch { /* a harmless rollback copy may remain on locked Windows hosts */ }
|
|
7915
|
+
}
|
|
7916
|
+
|
|
7917
|
+
function ensureAgentlasProjectStateIgnoreCli(projectPath) {
|
|
7918
|
+
const root = terminalProjectCandidateCli(projectPath);
|
|
7919
|
+
if (!root) throw new Error("refusing to initialize an unsafe Agentlas project root");
|
|
7920
|
+
const stateDir = path.join(root, ".agentlas");
|
|
7921
|
+
let stateExists = false;
|
|
7922
|
+
try {
|
|
7923
|
+
const state = fs.lstatSync(stateDir);
|
|
7924
|
+
stateExists = true;
|
|
7925
|
+
if (state.isSymbolicLink() || !state.isDirectory()) throw new Error(".agentlas must be a real directory");
|
|
7926
|
+
assertNoSymlinkInAgentlasStateCli(stateDir);
|
|
7927
|
+
} catch (error) {
|
|
7928
|
+
if (error && error.code !== "ENOENT") throw error;
|
|
7929
|
+
}
|
|
7930
|
+
|
|
7931
|
+
const gitignorePath = path.join(root, ".gitignore");
|
|
7932
|
+
const snapshot = readRegularUtf8FileNoFollowCli(gitignorePath);
|
|
7933
|
+
const existing = snapshot.content;
|
|
7934
|
+
const mode = snapshot.mode || 0o644;
|
|
7935
|
+
|
|
7936
|
+
let next = existing;
|
|
7937
|
+
const start = existing.indexOf(AGENTLAS_PROJECT_STATE_IGNORE_START);
|
|
7938
|
+
const end = start >= 0 ? existing.indexOf(AGENTLAS_PROJECT_STATE_IGNORE_END, start) : -1;
|
|
7939
|
+
if (start >= 0 && end >= 0) {
|
|
7940
|
+
const blockEnd = end + AGENTLAS_PROJECT_STATE_IGNORE_END.length;
|
|
7941
|
+
const block = existing.slice(start, blockEnd);
|
|
7942
|
+
if (!/^\.agentlas\/$/m.test(block)) {
|
|
7943
|
+
next = `${existing.slice(0, start)}${block.replace(AGENTLAS_PROJECT_STATE_IGNORE_START, `${AGENTLAS_PROJECT_STATE_IGNORE_START}\n.agentlas/`)}${existing.slice(blockEnd)}`;
|
|
7944
|
+
}
|
|
7945
|
+
} else {
|
|
7946
|
+
const block = `${AGENTLAS_PROJECT_STATE_IGNORE_START}\n.agentlas/\n${AGENTLAS_PROJECT_STATE_IGNORE_END}\n`;
|
|
7947
|
+
next = existing.trimEnd() ? `${existing.trimEnd()}\n\n${block}` : block;
|
|
7948
|
+
}
|
|
7949
|
+
if (next !== existing) {
|
|
7950
|
+
const temp = path.join(root, `.gitignore.agentlas-${process.pid}-${crypto.randomUUID()}.tmp`);
|
|
7951
|
+
fs.writeFileSync(temp, next.endsWith("\n") ? next : `${next}\n`, { encoding: "utf8", mode, flag: "wx" });
|
|
7952
|
+
try {
|
|
7953
|
+
assertFileSnapshotUnchangedCli(gitignorePath, snapshot);
|
|
7954
|
+
replaceRegularFileCli(temp, gitignorePath, snapshot);
|
|
7955
|
+
} catch (error) {
|
|
7956
|
+
try { fs.unlinkSync(temp); } catch { /* ignore */ }
|
|
7957
|
+
throw error;
|
|
7958
|
+
}
|
|
7959
|
+
}
|
|
7960
|
+
if (!stateExists) fs.mkdirSync(stateDir, { recursive: false, mode: 0o700 });
|
|
7961
|
+
assertNoSymlinkInAgentlasStateCli(stateDir);
|
|
7962
|
+
try { fs.chmodSync(stateDir, 0o700); } catch { /* Windows/best effort */ }
|
|
7963
|
+
}
|
|
7964
|
+
|
|
7965
|
+
function hardenAgentlasProjectStateCli(projectPath) {
|
|
7966
|
+
const root = terminalProjectCandidateCli(projectPath);
|
|
7967
|
+
if (!root) return;
|
|
7968
|
+
const stateDir = path.join(root, ".agentlas");
|
|
7969
|
+
const pending = [stateDir];
|
|
7970
|
+
let visited = 0;
|
|
7971
|
+
while (pending.length && visited < 4096) {
|
|
7972
|
+
const current = pending.pop();
|
|
7973
|
+
visited += 1;
|
|
7974
|
+
try {
|
|
7975
|
+
const stat = fs.lstatSync(current);
|
|
7976
|
+
if (stat.isSymbolicLink()) continue;
|
|
7977
|
+
if (stat.isDirectory()) {
|
|
7978
|
+
try { fs.chmodSync(current, 0o700); } catch { /* Windows/best effort */ }
|
|
7979
|
+
for (const entry of fs.readdirSync(current)) pending.push(path.join(current, entry));
|
|
7980
|
+
} else if (stat.isFile()) {
|
|
7981
|
+
try { fs.chmodSync(current, 0o600); } catch { /* Windows/best effort */ }
|
|
7982
|
+
}
|
|
7983
|
+
} catch { /* disappearing files and ACL-only hosts are best effort */ }
|
|
7984
|
+
}
|
|
7985
|
+
}
|
|
7986
|
+
|
|
7987
|
+
function ensureCoreProjectCli(projectPath, options = {}) {
|
|
7988
|
+
const root = terminalProjectCandidateCli(projectPath);
|
|
7989
|
+
if (!root) throw new Error("Agentlas project bootstrap requires a real project directory");
|
|
7990
|
+
fs.accessSync(root, fs.constants.R_OK | fs.constants.W_OK);
|
|
7991
|
+
ensureAgentlasProjectStateIgnoreCli(root);
|
|
7992
|
+
const cached = projectBootstrapStates.get(root);
|
|
7993
|
+
if (cached && fs.existsSync(path.join(root, ".agentlas", "project-soul-memory.md"))) {
|
|
7994
|
+
return cached === "core";
|
|
7995
|
+
}
|
|
7996
|
+
projectBootstrapStates.delete(root);
|
|
7997
|
+
const coreRoot = resolveCoreRuntimeRoot(options.coreRoot);
|
|
7998
|
+
const hasCanonicalBootstrap = Boolean(
|
|
7999
|
+
coreRoot && fs.existsSync(path.join(coreRoot, "agentlas_cloud", "project_bootstrap.py")),
|
|
8000
|
+
);
|
|
8001
|
+
if (hasCanonicalBootstrap) {
|
|
8002
|
+
const result = captureCoreJsonSync(
|
|
8003
|
+
"agentlas_cloud",
|
|
8004
|
+
["project", "ensure", "--project", root, "--reason", options.reason || "terminal-first-contact"],
|
|
8005
|
+
{ cwd: root },
|
|
8006
|
+
coreRoot,
|
|
8007
|
+
);
|
|
8008
|
+
const canonical = Boolean(
|
|
8009
|
+
result
|
|
8010
|
+
&& result.schemaVersion === "agentlas.project-bootstrap.v1"
|
|
8011
|
+
&& ["active", "privacy_warning"].includes(result.status)
|
|
8012
|
+
&& result.mergeOnly === true
|
|
8013
|
+
&& result.privacyBlockInstalled === true
|
|
8014
|
+
&& result.privateModeCompliant === true
|
|
8015
|
+
&& Array.isArray(result.missing)
|
|
8016
|
+
&& result.missing.length === 0
|
|
8017
|
+
&& Array.isArray(result.overwritten)
|
|
8018
|
+
&& result.overwritten.length === 0
|
|
8019
|
+
&& Array.isArray(result.permissionIssues)
|
|
8020
|
+
&& result.permissionIssues.length === 0
|
|
8021
|
+
);
|
|
8022
|
+
if (canonical) {
|
|
8023
|
+
// Core owns the canonical seed. Terminal adds one intentionally broader
|
|
8024
|
+
// guard so future local memory files are private without a release update.
|
|
8025
|
+
ensureAgentlasProjectStateIgnoreCli(root);
|
|
8026
|
+
hardenAgentlasProjectStateCli(root);
|
|
8027
|
+
projectBootstrapStates.set(root, "core");
|
|
8028
|
+
return true;
|
|
8029
|
+
}
|
|
8030
|
+
throw new Error("Agentlas Core returned an incomplete project bootstrap contract");
|
|
8031
|
+
}
|
|
8032
|
+
// A just-updated Terminal can briefly see the previous Core. The legacy
|
|
8033
|
+
// merge-only seed remains local-only and Core is retried next process.
|
|
8034
|
+
ensureProjectMemoryCli(root);
|
|
8035
|
+
if (!fs.existsSync(path.join(root, ".agentlas"))) {
|
|
8036
|
+
throw new Error("Agentlas project bootstrap could not create private local state");
|
|
8037
|
+
}
|
|
8038
|
+
ensureAgentlasProjectStateIgnoreCli(root);
|
|
8039
|
+
hardenAgentlasProjectStateCli(root);
|
|
8040
|
+
projectBootstrapStates.set(root, "fallback");
|
|
8041
|
+
return false;
|
|
7561
8042
|
}
|
|
7562
|
-
|
|
8043
|
+
|
|
8044
|
+
// Passive checks never increment visits or touch the project. Activation is
|
|
8045
|
+
// reserved for an actual write/full Terminal execution or an explicit ensure.
|
|
8046
|
+
function recordCliFolderVisit(db, projectPath, options = {}) {
|
|
8047
|
+
const root = terminalProjectCandidateCli(projectPath);
|
|
8048
|
+
if (!root) return { activated: false };
|
|
8049
|
+
const activate = options.activate === true;
|
|
8050
|
+
try {
|
|
8051
|
+
if (!activate) {
|
|
8052
|
+
const row = tableExists(db, "folder_activity")
|
|
8053
|
+
? db.prepare("SELECT activated_at FROM folder_activity WHERE path=?").get(root)
|
|
8054
|
+
: null;
|
|
8055
|
+
return { activated: Boolean(row && row.activated_at) || fs.existsSync(path.join(root, ".agentlas")) };
|
|
8056
|
+
}
|
|
8057
|
+
|
|
8058
|
+
ensureCoreProjectCli(root, { reason: options.reason || "terminal-first-contact", coreRoot: options.coreRoot });
|
|
8059
|
+
if (!tableExists(db, "folder_activity")) return { activated: true };
|
|
8060
|
+
const now = new Date().toISOString();
|
|
8061
|
+
const row = db.prepare("SELECT visits FROM folder_activity WHERE path=?").get(root);
|
|
8062
|
+
if (row) {
|
|
8063
|
+
db.prepare("UPDATE folder_activity SET visits=?, activated_at=COALESCE(activated_at,?), last_seen=? WHERE path=?")
|
|
8064
|
+
.run(Number(row.visits || 0) + 1, now, now, root);
|
|
8065
|
+
} else {
|
|
8066
|
+
db.prepare("INSERT INTO folder_activity (path, visits, activated_at, first_seen, last_seen) VALUES (?,?,?,?,?)")
|
|
8067
|
+
.run(root, 1, now, now, now);
|
|
8068
|
+
}
|
|
8069
|
+
return { activated: true };
|
|
8070
|
+
} catch (error) {
|
|
8071
|
+
// An activation failure can mean that the project-local privacy boundary
|
|
8072
|
+
// could not be established (for example, a symlinked or oversized
|
|
8073
|
+
// .gitignore). Never continue a write/full execution in that state.
|
|
8074
|
+
if (activate) throw error;
|
|
8075
|
+
return { activated: false };
|
|
8076
|
+
}
|
|
8077
|
+
}
|
|
8078
|
+
|
|
8079
|
+
function activeProjectPath(db, options = {}) {
|
|
8080
|
+
const root = terminalProjectCandidateCli(options.projectPath || process.cwd());
|
|
8081
|
+
if (!root) return null;
|
|
8082
|
+
const result = recordCliFolderVisit(db, root, options);
|
|
8083
|
+
return result.activated ? root : null;
|
|
8084
|
+
}
|
|
8085
|
+
|
|
8086
|
+
function ensureTerminalProjectForExecutionCli(db, projectPath, permission = PERMISSION, reason = "terminal-first-contact") {
|
|
8087
|
+
const root = terminalProjectCandidateCli(projectPath);
|
|
8088
|
+
if (!root) return null;
|
|
8089
|
+
if (permission === "read") return activeProjectPath(db, { projectPath: root });
|
|
8090
|
+
return activeProjectPath(db, { projectPath: root, activate: true, reason });
|
|
8091
|
+
}
|
|
8092
|
+
function cliMemoryContext(db, projectPath, agentId = null) {
|
|
7563
8093
|
const sections = [];
|
|
7564
8094
|
const arch = loadArch();
|
|
7565
8095
|
ensureMemoryContextColumn(db);
|
|
@@ -7575,14 +8105,49 @@ function cliMemoryContext(db, projectPath) {
|
|
|
7575
8105
|
}
|
|
7576
8106
|
if (tableExists(db, "memory_entries")) {
|
|
7577
8107
|
try {
|
|
7578
|
-
|
|
7579
|
-
|
|
7580
|
-
|
|
7581
|
-
|
|
8108
|
+
// New writes are read through a scoped global<->project timeline. The
|
|
8109
|
+
// project key is a digest, and team/agent lanes additionally require the
|
|
8110
|
+
// current owner id, so project B cannot recall project A's local memory.
|
|
8111
|
+
const governed = terminalMemoryGovernance.listScopedTimeline(db, {
|
|
8112
|
+
projectPath,
|
|
8113
|
+
agentId,
|
|
8114
|
+
limit: 16,
|
|
8115
|
+
});
|
|
8116
|
+
const seen = new Set(governed.map((row) => row.id));
|
|
8117
|
+
// Legacy rows predate the timeline. Keep only intentional user-global
|
|
8118
|
+
// rows, this exact project, and this exact agent/team owner. In
|
|
8119
|
+
// particular, do not revive the old global team-memory leakage query.
|
|
8120
|
+
const legacy = projectPath
|
|
8121
|
+
? db.prepare(`
|
|
8122
|
+
SELECT id,kind,content,context_json,created_at
|
|
8123
|
+
FROM memory_entries
|
|
8124
|
+
WHERE superseded_at IS NULL AND (
|
|
8125
|
+
(scope='user_identity' AND project_path IS NULL)
|
|
8126
|
+
OR (scope='project' AND project_path=?)
|
|
8127
|
+
OR (scope IN ('team_memory','agent_team','agent_repo') AND agent_id=? AND (project_path IS NULL OR project_path=?))
|
|
8128
|
+
)
|
|
8129
|
+
ORDER BY created_at DESC LIMIT 16
|
|
8130
|
+
`).all(projectPath, agentId, projectPath)
|
|
8131
|
+
: db.prepare(`
|
|
8132
|
+
SELECT id,kind,content,context_json,created_at
|
|
8133
|
+
FROM memory_entries
|
|
8134
|
+
WHERE superseded_at IS NULL AND (
|
|
8135
|
+
(scope='user_identity' AND project_path IS NULL)
|
|
8136
|
+
OR (scope IN ('team_memory','agent_team','agent_repo') AND agent_id=? AND project_path IS NULL)
|
|
8137
|
+
)
|
|
8138
|
+
ORDER BY created_at DESC LIMIT 16
|
|
8139
|
+
`).all(agentId);
|
|
8140
|
+
const rows = [...governed, ...legacy.filter((row) => !seen.has(row.id))].slice(0, 16);
|
|
8141
|
+
if (rows.length) {
|
|
8142
|
+
sections.push(
|
|
8143
|
+
(projectPath ? "### Scoped global + current-project memory timeline\n" : "### Curated user-global memory\n") +
|
|
8144
|
+
rows.map((r) => `- [${r.kind}] ${r.content}${contextLine(r.context_json)}`).join("\n"),
|
|
8145
|
+
);
|
|
8146
|
+
}
|
|
7582
8147
|
} catch { /* ignore */ }
|
|
7583
8148
|
}
|
|
7584
8149
|
if (!sections.length) return "";
|
|
7585
|
-
return "## Agentlas memory (read before answering;
|
|
8150
|
+
return "## Agentlas memory (read before answering; governed scope recall)\n\n" + sections.join("\n\n");
|
|
7586
8151
|
}
|
|
7587
8152
|
function parseMemoryEventsCli(text) {
|
|
7588
8153
|
const heading = loadArch().eventsHeading;
|
|
@@ -7600,11 +8165,16 @@ function parseMemoryEventsCli(text) {
|
|
|
7600
8165
|
function curateCliReply(db, text, ctx) {
|
|
7601
8166
|
const { events, cleaned } = parseMemoryEventsCli(text);
|
|
7602
8167
|
const style = require("./agentlas-style.cjs");
|
|
8168
|
+
if (ctx && ctx.permission === "read") return style.sanitizeAssistantText(cleaned);
|
|
7603
8169
|
if (!events.length || !tableExists(db, "memory_entries")) return style.sanitizeAssistantText(cleaned);
|
|
7604
8170
|
ensureMemoryContextColumn(db);
|
|
7605
8171
|
const arch = loadArch();
|
|
7606
8172
|
const { randomUUID } = require("node:crypto");
|
|
7607
8173
|
const now = new Date().toISOString();
|
|
8174
|
+
const rememberCurated = (memory) => {
|
|
8175
|
+
if (!ctx || !Array.isArray(ctx.curatedMemories) || !memory) return;
|
|
8176
|
+
if (!ctx.curatedMemories.some((item) => item.id === memory.id)) ctx.curatedMemories.push(memory);
|
|
8177
|
+
};
|
|
7608
8178
|
for (const ev of events) {
|
|
7609
8179
|
const content = ev && typeof ev.content === "string" ? ev.content.trim() : "";
|
|
7610
8180
|
if (!content) continue;
|
|
@@ -7620,9 +8190,16 @@ function curateCliReply(db, text, ctx) {
|
|
|
7620
8190
|
const ppath = scope === "project" ? ctx.projectPath : null;
|
|
7621
8191
|
const requestContext = normalizeRequestContext(ev, ctx, ppath);
|
|
7622
8192
|
try {
|
|
7623
|
-
const dup = db.prepare("SELECT
|
|
7624
|
-
if (dup)
|
|
7625
|
-
|
|
8193
|
+
const dup = db.prepare("SELECT id,scope,kind,content,confidence,sensitivity,context_json FROM memory_entries WHERE scope=? AND kind=? AND lower(trim(content))=? AND superseded_at IS NULL AND (project_path IS ? OR project_path=?) LIMIT 1").get(scope, kind, content.toLowerCase(), ppath, ppath);
|
|
8194
|
+
if (dup) {
|
|
8195
|
+
rememberCurated({ ...dup, requestContext });
|
|
8196
|
+
continue;
|
|
8197
|
+
}
|
|
8198
|
+
const memoryId = randomUUID();
|
|
8199
|
+
const confidence = ev.confidence || "medium";
|
|
8200
|
+
const sensitivity = ev.sensitivity || "internal";
|
|
8201
|
+
db.prepare("INSERT INTO memory_entries (id,scope,kind,content,project_id,project_path,agent_id,chat_id,confidence,sensitivity,evidence_json,context_json,superseded_at,created_at) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,NULL,?)").run(memoryId, scope, kind, content, ctx.projectId || null, ppath, ctx.agentId || null, null, confidence, sensitivity, JSON.stringify(Array.isArray(ev.evidence_refs) ? ev.evidence_refs : []), JSON.stringify(requestContext), now);
|
|
8202
|
+
rememberCurated({ id: memoryId, scope, kind, content, confidence, sensitivity, requestContext });
|
|
7626
8203
|
logCli(ctx.projectPath, { action: "written", scope, kind, content, request_context: requestContext, at: now });
|
|
7627
8204
|
} catch { /* ignore */ }
|
|
7628
8205
|
}
|
|
@@ -7641,7 +8218,50 @@ function prefsLang() {
|
|
|
7641
8218
|
}
|
|
7642
8219
|
}
|
|
7643
8220
|
|
|
7644
|
-
|
|
8221
|
+
const TERMINAL_MEMORY_CORE_MAX_TOKENS = 150;
|
|
8222
|
+
const TERMINAL_MEMORY_CORE = [
|
|
8223
|
+
"## Memory governance",
|
|
8224
|
+
"End every completed reply with hidden `## Memory Events` plus fenced JSON:",
|
|
8225
|
+
'{"turn_id":"<stable-id>","observation":{"outcome":"completed","summary":"safe short outcome"},"candidates":[]}',
|
|
8226
|
+
"Candidates 0..N: memory_kind,content,suggested_scope,confidence.",
|
|
8227
|
+
"Scopes: user_global|team|agent|project|session|discard.",
|
|
8228
|
+
"No raw prompts/transcripts, secrets, logs, or absolute paths. Curator suggests; deterministic gates decide writes.",
|
|
8229
|
+
].join("\n");
|
|
8230
|
+
const MEMORY_DETAIL_RE = /\b(?:remember|memory|save this|record this|memory event)\b|기억|메모리|저장해|기록해|남겨/i;
|
|
8231
|
+
const CREDENTIAL_INDEX_RE = /\b(?:deploy|release|billing|auth|oauth|credential|api key|secret key|cloud)\b|배포|릴리스|출시|결제|인증|자격 증명|API\s*키|시크릿|클라우드/i;
|
|
8232
|
+
|
|
8233
|
+
function approximatePromptTokens(text) {
|
|
8234
|
+
return Math.ceil(Buffer.byteLength(String(text || ""), "utf8") / 3);
|
|
8235
|
+
}
|
|
8236
|
+
if (approximatePromptTokens(TERMINAL_MEMORY_CORE) > TERMINAL_MEMORY_CORE_MAX_TOKENS) {
|
|
8237
|
+
throw new Error("Terminal always-on memory core exceeds 150 tokens");
|
|
8238
|
+
}
|
|
8239
|
+
|
|
8240
|
+
function memoryEmitterPromptFor(request, arch = loadArch(), turnId = null, permission = "write") {
|
|
8241
|
+
const stableId = String(turnId || "").replace(/[^A-Za-z0-9:._-]/g, "").slice(0, 160);
|
|
8242
|
+
let prompt = TERMINAL_MEMORY_CORE;
|
|
8243
|
+
if (stableId) prompt += `\nUse turn_id=${stableId}. permission=${permission === "read" ? "receipt-only" : "curated-write"}.`;
|
|
8244
|
+
if (!MEMORY_DETAIL_RE.test(String(request || ""))) return prompt;
|
|
8245
|
+
const kinds = Array.isArray(arch?.kinds) && arch.kinds.length ? arch.kinds.join("|") : "fact|decision|preference|risk|procedure";
|
|
8246
|
+
prompt += [
|
|
8247
|
+
"",
|
|
8248
|
+
`Allowed memory_kind: ${kinds}.`,
|
|
8249
|
+
"Global requires explicit owner authorization; suggest only, never promote.",
|
|
8250
|
+
"Do not emit request_context; put only a safe, short outcome in observation.",
|
|
8251
|
+
].join("\n");
|
|
8252
|
+
return prompt;
|
|
8253
|
+
}
|
|
8254
|
+
|
|
8255
|
+
function credentialIndexReminderFor(request) {
|
|
8256
|
+
if (!CREDENTIAL_INDEX_RE.test(String(request || ""))) return "";
|
|
8257
|
+
return [
|
|
8258
|
+
"## Local credential lookup (triggered)",
|
|
8259
|
+
"Before saying a deploy, release, billing, auth, API, or cloud credential is missing, read `.agentlas/local-credentials.map.json` and the Local Credential Index in `.agentlas/project-soul-memory.md`.",
|
|
8260
|
+
"Use only env names and local relative references; never copy credential values into memory or output.",
|
|
8261
|
+
].join("\n");
|
|
8262
|
+
}
|
|
8263
|
+
|
|
8264
|
+
function augmentSystem(db, baseSystem, ctx, withEmitter, request = "") {
|
|
7645
8265
|
const arch = loadArch();
|
|
7646
8266
|
let sys = baseSystem || "";
|
|
7647
8267
|
// 언어/말투 지시를 맨 앞에 둔다. imported/cloud/company agents도 같은 전역 계약을 따른다.
|
|
@@ -7649,12 +8269,133 @@ function augmentSystem(db, baseSystem, ctx, withEmitter) {
|
|
|
7649
8269
|
sys = langDirective(lang) + (sys ? "\n\n" + sys : "");
|
|
7650
8270
|
const connectionSkill = loadGlobalConnectionSkill();
|
|
7651
8271
|
if (connectionSkill) sys += "\n\n" + connectionSkill;
|
|
7652
|
-
const mem = cliMemoryContext(db, ctx && ctx.projectPath);
|
|
8272
|
+
const mem = cliMemoryContext(db, ctx && ctx.projectPath, ctx && ctx.agentId);
|
|
7653
8273
|
if (mem) sys += "\n\n" + mem;
|
|
7654
|
-
if (withEmitter
|
|
8274
|
+
if (withEmitter) {
|
|
8275
|
+
sys += "\n\n" + memoryEmitterPromptFor(request, arch, ctx && ctx.turnId, ctx && ctx.permission);
|
|
8276
|
+
const credentialReminder = credentialIndexReminderFor(request);
|
|
8277
|
+
if (credentialReminder) sys += "\n\n" + credentialReminder;
|
|
8278
|
+
}
|
|
7655
8279
|
return sys;
|
|
7656
8280
|
}
|
|
7657
8281
|
|
|
8282
|
+
function curatorRuntimeDirCli() {
|
|
8283
|
+
const root = path.join(userDataDir(), "memory-governance", "curator-runtime");
|
|
8284
|
+
fs.mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
8285
|
+
try { fs.chmodSync(path.dirname(root), 0o700); } catch { /* Windows/ACL-only host */ }
|
|
8286
|
+
try { fs.chmodSync(root, 0o700); } catch { /* Windows/ACL-only host */ }
|
|
8287
|
+
return root;
|
|
8288
|
+
}
|
|
8289
|
+
|
|
8290
|
+
function curatorRuntimeEnvCli(source = process.env) {
|
|
8291
|
+
// The semantic Curator has no tools and receives only pre-gated candidates.
|
|
8292
|
+
// Keep its process environment equally narrow: subscription CLIs can locate
|
|
8293
|
+
// their normal file-backed auth, but project/provider secret env is absent.
|
|
8294
|
+
const allowed = new Set([
|
|
8295
|
+
"PATH", "HOME", "USER", "LOGNAME", "SHELL", "TMPDIR", "TMP", "TEMP",
|
|
8296
|
+
"LANG", "LC_ALL", "LC_CTYPE", "TERM", "COLORTERM", "NO_COLOR",
|
|
8297
|
+
"CODEX_HOME", "CLAUDE_CONFIG_DIR", "XDG_CONFIG_HOME", "USERPROFILE",
|
|
8298
|
+
"APPDATA", "LOCALAPPDATA", "SYSTEMROOT", "SystemRoot", "COMSPEC", "ComSpec", "PATHEXT",
|
|
8299
|
+
]);
|
|
8300
|
+
const env = {};
|
|
8301
|
+
for (const [key, value] of Object.entries(source || {})) {
|
|
8302
|
+
if (allowed.has(key) || key.startsWith("LC_")) env[key] = value;
|
|
8303
|
+
}
|
|
8304
|
+
env.AGENTLAS_MEMORY_CURATOR = "1";
|
|
8305
|
+
return env;
|
|
8306
|
+
}
|
|
8307
|
+
|
|
8308
|
+
function ensureGeminiNoToolsPolicyCli() {
|
|
8309
|
+
const dir = curatorRuntimeDirCli();
|
|
8310
|
+
const file = path.join(dir, "gemini-no-tools-policy.toml");
|
|
8311
|
+
const content = [
|
|
8312
|
+
"# Managed by Agentlas Terminal for the semantic Memory Curator.",
|
|
8313
|
+
"[[rule]]",
|
|
8314
|
+
'toolName = "*"',
|
|
8315
|
+
'decision = "deny"',
|
|
8316
|
+
"priority = 999",
|
|
8317
|
+
"",
|
|
8318
|
+
].join("\n");
|
|
8319
|
+
let current = null;
|
|
8320
|
+
try { current = fs.readFileSync(file, "utf8"); } catch { /* first write */ }
|
|
8321
|
+
if (current !== content) {
|
|
8322
|
+
const temp = path.join(dir, `.gemini-no-tools-policy.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
8323
|
+
fs.writeFileSync(temp, content, { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
8324
|
+
fs.renameSync(temp, file);
|
|
8325
|
+
}
|
|
8326
|
+
try { fs.chmodSync(file, 0o600); } catch { /* Windows/ACL-only host */ }
|
|
8327
|
+
return file;
|
|
8328
|
+
}
|
|
8329
|
+
|
|
8330
|
+
async function invokeMemoryCuratorCli(db, runtime, model, payload, systemPrompt) {
|
|
8331
|
+
const serialized = JSON.stringify(payload);
|
|
8332
|
+
if (
|
|
8333
|
+
terminalMemoryGovernance.hasSecret(serialized) ||
|
|
8334
|
+
terminalMemoryGovernance.hasAbsolutePath(serialized) ||
|
|
8335
|
+
terminalMemoryGovernance.hasTranscriptBody(serialized)
|
|
8336
|
+
) {
|
|
8337
|
+
throw new Error("Memory Curator payload failed the pre-invocation privacy gate");
|
|
8338
|
+
}
|
|
8339
|
+
if (runtime.mode === "cli") {
|
|
8340
|
+
return captureRuntime(runtime.kind, systemPrompt, serialized, {
|
|
8341
|
+
cwd: curatorRuntimeDirCli(),
|
|
8342
|
+
env: curatorRuntimeEnvCli(),
|
|
8343
|
+
permission: "read",
|
|
8344
|
+
model: model || runtime.model || null,
|
|
8345
|
+
effort: "low",
|
|
8346
|
+
authorityMode: "no-authority",
|
|
8347
|
+
noToolsPolicyPath: runtime.kind === "gemini" ? ensureGeminiNoToolsPolicyCli() : null,
|
|
8348
|
+
outputLimitBytes: 64 * 1024,
|
|
8349
|
+
timeoutConfig: { idleMs: 60_000, totalMs: 120_000, killGraceMs: 2_000 },
|
|
8350
|
+
});
|
|
8351
|
+
}
|
|
8352
|
+
return runApi(runtime.backend, model || runtime.model, systemPrompt, serialized);
|
|
8353
|
+
}
|
|
8354
|
+
|
|
8355
|
+
function beginMemoryTurnCli(db, prompt, ctx = {}) {
|
|
8356
|
+
return terminalMemoryGovernance.beginTurn(db, {
|
|
8357
|
+
prompt,
|
|
8358
|
+
projectPath: ctx.projectPath,
|
|
8359
|
+
agentId: ctx.agentId,
|
|
8360
|
+
permission: ctx.permission,
|
|
8361
|
+
surface: ctx.surface || "terminal-normal-turn",
|
|
8362
|
+
conversationRef: ctx.conversationRef,
|
|
8363
|
+
priorContextDigest: ctx.priorContextDigest,
|
|
8364
|
+
stableTurnId: ctx.stableTurnId || process.env.AGENTLAS_TURN_ID,
|
|
8365
|
+
});
|
|
8366
|
+
}
|
|
8367
|
+
|
|
8368
|
+
async function completeMemoryTurnCli(db, text, ctx, runtime, options = {}) {
|
|
8369
|
+
const turnId = ctx?.memoryTurn?.turnId || ctx?.turnId;
|
|
8370
|
+
const arch = loadArch();
|
|
8371
|
+
const runtimeInfo = runtime || {};
|
|
8372
|
+
const completion = {
|
|
8373
|
+
turnId,
|
|
8374
|
+
mainOutput: text,
|
|
8375
|
+
requestText: options.requestText,
|
|
8376
|
+
permission: ctx && ctx.permission,
|
|
8377
|
+
projectPath: ctx && ctx.projectPath,
|
|
8378
|
+
agentId: ctx && ctx.agentId,
|
|
8379
|
+
eventsHeading: arch.eventsHeading,
|
|
8380
|
+
outcome: options.outcome || "completed",
|
|
8381
|
+
coreFiles: {
|
|
8382
|
+
memoryDir: arch.memoryDir || ".agentlas",
|
|
8383
|
+
ticketFile: arch.memoryTicketsFile || "memory-tickets.jsonl",
|
|
8384
|
+
decisionFile: arch.curatorDecisionsFile || "curator-decisions.jsonl",
|
|
8385
|
+
},
|
|
8386
|
+
};
|
|
8387
|
+
if (options.invokeCurator !== false) {
|
|
8388
|
+
completion.invokeCurator = (payload, systemPrompt) => invokeMemoryCuratorCli(
|
|
8389
|
+
db,
|
|
8390
|
+
runtimeInfo,
|
|
8391
|
+
options.model || runtimeInfo.model || null,
|
|
8392
|
+
payload,
|
|
8393
|
+
systemPrompt,
|
|
8394
|
+
);
|
|
8395
|
+
}
|
|
8396
|
+
return terminalMemoryGovernance.completeTurn(db, completion);
|
|
8397
|
+
}
|
|
8398
|
+
|
|
7658
8399
|
function loadGlobalConnectionSkill() {
|
|
7659
8400
|
try {
|
|
7660
8401
|
return require("../dist/electron/runtime/global-skill.js").GLOBAL_CONNECTION_SKILL || "";
|
|
@@ -7676,19 +8417,78 @@ const RUNTIME_BIN = {
|
|
|
7676
8417
|
|
|
7677
8418
|
// 활성 런타임 → 실행 방식 결정. CLI(claude/codex/gemini) 또는 API(BYOK/Ollama).
|
|
7678
8419
|
function resolveRuntime(db, override) {
|
|
8420
|
+
const ar = activeRuntime(db);
|
|
8421
|
+
const activeCli = ar && RUNTIME_BIN[ar.kind]
|
|
8422
|
+
? {
|
|
8423
|
+
mode: "cli",
|
|
8424
|
+
kind: ar.kind,
|
|
8425
|
+
model: ar.model || null,
|
|
8426
|
+
capabilities: ["code", "tools", ...(ar.long_context ? ["long-context"] : [])],
|
|
8427
|
+
efforts: [],
|
|
8428
|
+
}
|
|
8429
|
+
: null;
|
|
7679
8430
|
if (override) {
|
|
7680
|
-
if (!RUNTIME_BIN[override]) fail(
|
|
7681
|
-
return { mode: "cli", kind: override };
|
|
8431
|
+
if (!RUNTIME_BIN[override]) fail(`Unknown runtime: ${override} (claude-code|codex|gemini)`);
|
|
8432
|
+
return activeCli && activeCli.kind === override ? activeCli : { mode: "cli", kind: override };
|
|
7682
8433
|
}
|
|
7683
|
-
|
|
7684
|
-
if (ar && RUNTIME_BIN[ar.kind]) return { mode: "cli", kind: ar.kind };
|
|
8434
|
+
if (activeCli) return activeCli;
|
|
7685
8435
|
if (ar && ar.kind === "byok" && ar.backend) return { mode: "api", backend: ar.backend, model: ar.model };
|
|
7686
8436
|
if (ar && ar.kind === "ollama") return { mode: "api", backend: "ollama", model: ar.model };
|
|
7687
8437
|
// 폴백: 설치된 CLI 탐지
|
|
7688
8438
|
for (const kind of Object.keys(RUNTIME_BIN)) {
|
|
7689
8439
|
if (which(RUNTIME_BIN[kind])) return { mode: "cli", kind };
|
|
7690
8440
|
}
|
|
7691
|
-
fail("
|
|
8441
|
+
fail("No runtime is available. Install a CLI (claude/codex/gemini) or configure an API key/Ollama in the app.");
|
|
8442
|
+
}
|
|
8443
|
+
|
|
8444
|
+
// Build the executable runtime inventory for the parent allocator. It is
|
|
8445
|
+
// intentionally local to this host: a Terminal/Codex/Claude plugin never
|
|
8446
|
+
// pretends it can schedule a runtime that is not installed and connected here.
|
|
8447
|
+
function listAvailableRuntimes(db, fallbackRuntime = null) {
|
|
8448
|
+
const routing = require("./agentlas-workload-routing.cjs");
|
|
8449
|
+
const active = fallbackRuntime || resolveRuntime(db);
|
|
8450
|
+
const candidates = [];
|
|
8451
|
+
const add = (runtime) => {
|
|
8452
|
+
if (!runtime) return;
|
|
8453
|
+
const key = runtime.mode === "cli" ? `cli:${runtime.kind}` : `api:${runtime.backend}:${runtime.model || ""}`;
|
|
8454
|
+
if (candidates.some((item) => item.key === key)) return;
|
|
8455
|
+
const discovered = routing.defaultAvailableModels(runtime);
|
|
8456
|
+
const availableModels = [...discovered];
|
|
8457
|
+
if (runtime.model && !availableModels.some((model) => model.id === runtime.model)) {
|
|
8458
|
+
availableModels.push({
|
|
8459
|
+
id: runtime.model,
|
|
8460
|
+
tier: runtime.modelTier || runtime.tier || null,
|
|
8461
|
+
capabilities: runtime.capabilities || [],
|
|
8462
|
+
contextWindow: runtime.contextWindow || null,
|
|
8463
|
+
efforts: runtime.efforts || [],
|
|
8464
|
+
description: runtime.modelDescription || "host-selected current model",
|
|
8465
|
+
});
|
|
8466
|
+
}
|
|
8467
|
+
candidates.push({ ...runtime, key, availableModels });
|
|
8468
|
+
};
|
|
8469
|
+
add(active);
|
|
8470
|
+
for (const kind of Object.keys(RUNTIME_BIN)) {
|
|
8471
|
+
if (!which(RUNTIME_BIN[kind])) continue;
|
|
8472
|
+
add({ mode: "cli", kind });
|
|
8473
|
+
}
|
|
8474
|
+
return candidates
|
|
8475
|
+
.filter((runtime) => runtime.availableModels.length)
|
|
8476
|
+
.map(({ key, ...runtime }, index) => ({ ...runtime, runtimeId: `runtime-${index + 1}` }));
|
|
8477
|
+
}
|
|
8478
|
+
|
|
8479
|
+
function currentRuntimeInventoryCli(db, runtime) {
|
|
8480
|
+
const candidates = listAvailableRuntimes(db, runtime);
|
|
8481
|
+
const current = candidates.find((candidate) =>
|
|
8482
|
+
candidate.mode === runtime.mode &&
|
|
8483
|
+
(runtime.mode === "cli"
|
|
8484
|
+
? candidate.kind === runtime.kind
|
|
8485
|
+
: candidate.backend === runtime.backend && candidate.model === runtime.model));
|
|
8486
|
+
if (current) return current;
|
|
8487
|
+
return {
|
|
8488
|
+
...runtime,
|
|
8489
|
+
runtimeId: "runtime-current",
|
|
8490
|
+
availableModels: workloadRouting.defaultAvailableModels(runtime),
|
|
8491
|
+
};
|
|
7692
8492
|
}
|
|
7693
8493
|
|
|
7694
8494
|
// ── API 러너 (BYOK / Ollama) — 비스트리밍, 최종 텍스트 반환 ──
|
|
@@ -7728,14 +8528,14 @@ function normalizeCustomApiBaseUrl(raw) {
|
|
|
7728
8528
|
try {
|
|
7729
8529
|
parsed = new URL(value);
|
|
7730
8530
|
} catch {
|
|
7731
|
-
throw new Error("Custom API base URL
|
|
8531
|
+
throw new Error("Custom API base URL is invalid.");
|
|
7732
8532
|
}
|
|
7733
8533
|
const host = parsed.hostname.toLowerCase();
|
|
7734
8534
|
const isLoopback = host === "localhost" || host === "127.0.0.1" || host === "::1" || host === "[::1]";
|
|
7735
8535
|
const isPrivateLan =
|
|
7736
8536
|
/^10\./.test(host) || /^192\.168\./.test(host) || /^172\.(1[6-9]|2\d|3[01])\./.test(host);
|
|
7737
8537
|
if (parsed.protocol !== "https:" && !(parsed.protocol === "http:" && (isLoopback || isPrivateLan))) {
|
|
7738
|
-
throw new Error("Custom API base URL
|
|
8538
|
+
throw new Error("Custom API base URL must use HTTPS or HTTP on localhost/LAN.");
|
|
7739
8539
|
}
|
|
7740
8540
|
return value.replace(/\/+$/, "");
|
|
7741
8541
|
}
|
|
@@ -7761,7 +8561,7 @@ function readCustomApiBaseUrl() {
|
|
|
7761
8561
|
raw = "";
|
|
7762
8562
|
}
|
|
7763
8563
|
} catch (e) {
|
|
7764
|
-
throw new Error(`Custom API base URL
|
|
8564
|
+
throw new Error(`Could not read the Custom API base URL from the shared database: ${(e && e.message) || e}`);
|
|
7765
8565
|
} finally {
|
|
7766
8566
|
try { if (db && typeof db.close === "function") db.close(); } catch { /* ignore close failure */ }
|
|
7767
8567
|
}
|
|
@@ -7777,22 +8577,22 @@ async function runApi(backend, model, system, prompt, options) {
|
|
|
7777
8577
|
options = options || {};
|
|
7778
8578
|
model = model || DEFAULT_API_MODEL[backend];
|
|
7779
8579
|
const fetchImpl = options.fetch || globalThis.fetch;
|
|
7780
|
-
if (typeof fetchImpl !== "function") throw new Error("
|
|
8580
|
+
if (typeof fetchImpl !== "function") throw new Error("fetch is unavailable in this runtime (run through the app runtime).");
|
|
7781
8581
|
if (backend === "ollama") {
|
|
7782
8582
|
const resp = await fetchImpl("http://127.0.0.1:11434/api/chat", {
|
|
7783
8583
|
method: "POST",
|
|
7784
8584
|
headers: { "content-type": "application/json" },
|
|
7785
8585
|
body: JSON.stringify({ model, stream: false, messages: [{ role: "system", content: system }, { role: "user", content: prompt }] }),
|
|
7786
8586
|
});
|
|
7787
|
-
if (!resp.ok) throw new Error(`Ollama ${resp.status} — 'ollama serve'
|
|
8587
|
+
if (!resp.ok) throw new Error(`Ollama ${resp.status} — run 'ollama serve' and check the model`);
|
|
7788
8588
|
const j = await resp.json();
|
|
7789
8589
|
return (j.message && j.message.content) || "";
|
|
7790
8590
|
}
|
|
7791
8591
|
const supported = backend === "anthropic" || backend === "openai" || backend === "google" ||
|
|
7792
8592
|
backend === "upstage" || backend === "custom" || !!ANTHROPIC_COMPAT_API[backend];
|
|
7793
|
-
if (!supported) throw new Error("
|
|
8593
|
+
if (!supported) throw new Error("Unsupported backend: " + backend);
|
|
7794
8594
|
const key = Object.prototype.hasOwnProperty.call(options, "apiKey") ? options.apiKey : await apiKey(backend);
|
|
7795
|
-
if (!key) throw new Error(`${backend} API
|
|
8595
|
+
if (!key) throw new Error(`${backend} API key is missing. Register it in App settings → BYOK.`);
|
|
7796
8596
|
|
|
7797
8597
|
const anthropicCompat = ANTHROPIC_COMPAT_API[backend];
|
|
7798
8598
|
if (backend === "anthropic" || anthropicCompat) {
|
|
@@ -7840,18 +8640,129 @@ async function runApi(backend, model, system, prompt, options) {
|
|
|
7840
8640
|
const c = j.candidates && j.candidates[0];
|
|
7841
8641
|
return (c && c.content && c.content.parts && c.content.parts[0] && c.content.parts[0].text) || "";
|
|
7842
8642
|
}
|
|
7843
|
-
throw new Error("
|
|
8643
|
+
throw new Error("Unsupported backend: " + backend);
|
|
7844
8644
|
}
|
|
7845
8645
|
|
|
7846
8646
|
// 1회 실행 — CLI면 spawn(스트리밍 stdout), API면 호출 후 텍스트 출력. 종료코드 반환.
|
|
7847
8647
|
// ctx = { projectPath, agentId } — 메모리 주입/큐레이션에 사용.
|
|
8648
|
+
function finalizeExperienceExecutionCli(db, input) {
|
|
8649
|
+
if (input.permission === "read") return null;
|
|
8650
|
+
if (!input.agentId) return null;
|
|
8651
|
+
let agent;
|
|
8652
|
+
try { agent = db.prepare("SELECT * FROM installed_agents WHERE id=?").get(input.agentId); }
|
|
8653
|
+
catch { return null; }
|
|
8654
|
+
if (!agent) return null;
|
|
8655
|
+
const exactBase = exactAgentBaseForExecution(db, agent, input.runtimeExperience);
|
|
8656
|
+
if (!exactBase) return null;
|
|
8657
|
+
const runtime = input.runtime || {};
|
|
8658
|
+
const provider = runtime.mode === "cli" ? runtime.kind : runtime.backend;
|
|
8659
|
+
const modelId = input.model || runtime.model || provider;
|
|
8660
|
+
const usage = input.usage || {};
|
|
8661
|
+
try {
|
|
8662
|
+
return terminalExperienceIntake.finalizeAgentExecution({
|
|
8663
|
+
db,
|
|
8664
|
+
userDataDir: userDataDir(),
|
|
8665
|
+
cwd: input.cwd || input.projectPath || projectCwd(),
|
|
8666
|
+
agent,
|
|
8667
|
+
exactBase,
|
|
8668
|
+
environment: { runtime: provider || "terminal", os: process.platform, arch: process.arch },
|
|
8669
|
+
model: { provider: provider || "terminal-runtime", modelId: modelId || "terminal-runtime" },
|
|
8670
|
+
mcp: (input.mcpServers || []).flatMap((server) => {
|
|
8671
|
+
const catalogId = server.catalog_id || server.catalogId;
|
|
8672
|
+
// A reviewed runtime allowlist proves approval, not that this turn's
|
|
8673
|
+
// child completed an MCP initialize/tool call. Do not inflate it into
|
|
8674
|
+
// connected evidence without an exact runtime signal.
|
|
8675
|
+
return catalogId ? [{ catalogId, status: "approved" }] : [];
|
|
8676
|
+
}),
|
|
8677
|
+
outcome: input.outcome,
|
|
8678
|
+
metrics: {
|
|
8679
|
+
promptTokens: usage.input_tokens || usage.prompt_tokens || 0,
|
|
8680
|
+
completionTokens: usage.output_tokens || usage.completion_tokens || 0,
|
|
8681
|
+
totalTokens: usage.total_tokens || 0,
|
|
8682
|
+
durationMs: input.durationMs || usage.duration_ms || 0,
|
|
8683
|
+
retryCount: 0,
|
|
8684
|
+
},
|
|
8685
|
+
curatedMemories: input.curatedMemories || [],
|
|
8686
|
+
taskHint: input.taskHint,
|
|
8687
|
+
taskSignatures: input.runtimeExperience?.taskSignatures || [],
|
|
8688
|
+
experiencePackReleaseId: input.runtimeExperience?.experiencePackReleaseIds?.[0] || null,
|
|
8689
|
+
locale: input.lang || prefsLang(),
|
|
8690
|
+
runId: input.runId,
|
|
8691
|
+
createdAt: input.createdAt,
|
|
8692
|
+
});
|
|
8693
|
+
} catch (error) {
|
|
8694
|
+
process.stderr.write(`▸ local Experience intake skipped · ${String((error && error.message) || error).slice(0, 180)}\n`);
|
|
8695
|
+
return null;
|
|
8696
|
+
}
|
|
8697
|
+
}
|
|
8698
|
+
|
|
7848
8699
|
async function executeOnce(db, system, prompt, override, ctx) {
|
|
7849
8700
|
ctx = ctx || { projectPath: null, agentId: null };
|
|
8701
|
+
const runStartedAt = Date.now();
|
|
8702
|
+
const memoryTurn = beginMemoryTurnCli(db, prompt, {
|
|
8703
|
+
...ctx,
|
|
8704
|
+
surface: ctx.surface || "terminal-one-shot",
|
|
8705
|
+
stableTurnId: ctx.turnId,
|
|
8706
|
+
});
|
|
8707
|
+
ctx.memoryTurn = memoryTurn;
|
|
8708
|
+
ctx.turnId = memoryTurn.turnId;
|
|
8709
|
+
const experienceRunId = `terminal-run:${memoryTurn.turnId}`;
|
|
8710
|
+
const curatedMemories = [];
|
|
8711
|
+
ctx.curatedMemories = curatedMemories;
|
|
7850
8712
|
if (!ctx.cwdAtRequest) ctx.cwdAtRequest = projectCwd();
|
|
8713
|
+
let memoryRuntime = null;
|
|
8714
|
+
let memorySettled = false;
|
|
8715
|
+
try {
|
|
8716
|
+
let runtimeSystem = system;
|
|
8717
|
+
let localExperienceContext = null;
|
|
8718
|
+
if (ctx.runtimeExperience?.disabled === true && ctx.runtimeExperience.observableReason) {
|
|
8719
|
+
process.stderr.write(`▸ local Experience skipped · ${ctx.runtimeExperience.observableReason}\n`);
|
|
8720
|
+
} else if (ctx.runtimeExperience && ctx.runtimeExperience.disabled !== true) {
|
|
8721
|
+
const runtimeExperience = ctx.runtimeExperience;
|
|
8722
|
+
const augmented = terminalExperienceExchange.augmentRuntimeSystemWithLocalExperience(system, {
|
|
8723
|
+
userDataDir: userDataDir(),
|
|
8724
|
+
cwd: ctx.projectPath || ctx.cwdAtRequest,
|
|
8725
|
+
baseAgentReleaseId: runtimeExperience.baseAgentReleaseId,
|
|
8726
|
+
agentDefinitionId: runtimeExperience.agentDefinitionId,
|
|
8727
|
+
experiencePackReleaseIds: runtimeExperience.experiencePackReleaseIds || [],
|
|
8728
|
+
taskSignatures: runtimeExperience.taskSignatures || [],
|
|
8729
|
+
environmentTags: Array.isArray(runtimeExperience.environmentTags) && runtimeExperience.environmentTags.length
|
|
8730
|
+
? runtimeExperience.environmentTags
|
|
8731
|
+
: terminalExperienceExchange.defaultEnvironmentTags(),
|
|
8732
|
+
reservedTokens: ctx.runtimeExperience?.tasteRuntimeOverlay?.estimatedTokens ?? 0,
|
|
8733
|
+
});
|
|
8734
|
+
runtimeSystem = augmented.systemPrompt;
|
|
8735
|
+
localExperienceContext = augmented.experienceContext;
|
|
8736
|
+
if (localExperienceContext.itemIds.length) {
|
|
8737
|
+
const source = runtimeExperience.loadoutAuthority === "desktop-terminal-exact-loadout"
|
|
8738
|
+
? "Desktop-approved exact Experience"
|
|
8739
|
+
: "local Experience advisory";
|
|
8740
|
+
process.stderr.write(`▸ ${source} · ${localExperienceContext.itemIds.length} item(s) · ~${localExperienceContext.estimatedTokens} tokens · no server rental receipt\n`);
|
|
8741
|
+
}
|
|
8742
|
+
}
|
|
8743
|
+
const tasteTaskResolution = terminalExperienceExchange.deriveCanonicalTaskClasses(prompt);
|
|
8744
|
+
if (
|
|
8745
|
+
ctx.runtimeExperience?.tasteRuntimeOverlay &&
|
|
8746
|
+
desktopOntologyLoadout.tasteRuntimeOverlayMatchesTask(
|
|
8747
|
+
ctx.runtimeExperience.tasteRuntimeOverlay,
|
|
8748
|
+
tasteTaskResolution.taskIds,
|
|
8749
|
+
prompt,
|
|
8750
|
+
)
|
|
8751
|
+
) {
|
|
8752
|
+
const tasteDirective = desktopOntologyLoadout.renderTasteRuntimeDirective(
|
|
8753
|
+
ctx.runtimeExperience.tasteRuntimeOverlay,
|
|
8754
|
+
);
|
|
8755
|
+
runtimeSystem = `${runtimeSystem}\n\n${tasteDirective}`;
|
|
8756
|
+
process.stderr.write(
|
|
8757
|
+
`▸ Desktop-approved exact Taste · ${ctx.runtimeExperience.tasteRuntimeOverlay.releaseId} · ~${ctx.runtimeExperience.tasteRuntimeOverlay.estimatedTokens} tokens · session snapshot\n`,
|
|
8758
|
+
);
|
|
8759
|
+
}
|
|
7851
8760
|
const rt = resolveRuntime(db, override);
|
|
8761
|
+
memoryRuntime = rt;
|
|
7852
8762
|
if (rt.mode === "cli") {
|
|
7853
|
-
// 네이티브 CLI
|
|
7854
|
-
|
|
8763
|
+
// 네이티브 CLI에도 같은 Memory emitter를 주입하되 guard가 화면의 JSON 블록을 숨긴다.
|
|
8764
|
+
// 큐레이터가 만든 구조화 Memory만 성공 RunReceipt 이후 Experience intake로 전달된다.
|
|
8765
|
+
const sys = augmentSystem(db, runtimeSystem, ctx, true, prompt);
|
|
7855
8766
|
const cwd = ctx.projectPath || projectCwd();
|
|
7856
8767
|
const permission = ctx.permission || "write";
|
|
7857
8768
|
const env = await buildChildEnvCli(db, { ...ctx, cwd });
|
|
@@ -7859,40 +8770,300 @@ async function executeOnce(db, system, prompt, override, ctx) {
|
|
|
7859
8770
|
// one-shot(`agentlas "작업"`)도 REPL과 동일한 리치 렌더(⏺ 툴 / └ 결과 / 토큰)로 출력한다.
|
|
7860
8771
|
const { runNativeTurn } = require("./agentlas-native-host.cjs");
|
|
7861
8772
|
const { Ui } = require("./agentlas-ui.cjs");
|
|
8773
|
+
const { makeMemoryGuard } = require("./agentlas-repl.cjs");
|
|
7862
8774
|
const ui = new Ui({ lang: prefsLang() });
|
|
7863
8775
|
let mcpServers = [];
|
|
7864
8776
|
if (permission === "full") {
|
|
7865
|
-
|
|
7866
|
-
|
|
7867
|
-
|
|
8777
|
+
if (Array.isArray(ctx.mcpServers)) {
|
|
8778
|
+
// Build's reviewed host allowlist is authoritative, including the valid
|
|
8779
|
+
// empty list. Never fall back to every enabled registry row.
|
|
8780
|
+
mcpServers = ctx.mcpServers;
|
|
8781
|
+
} else {
|
|
8782
|
+
try {
|
|
8783
|
+
mcpServers = terminalAssets.readConsentedSystemMcpServers(db, { userDataDir: userDataDir() });
|
|
8784
|
+
} catch { /* ignore */ }
|
|
8785
|
+
}
|
|
7868
8786
|
}
|
|
7869
8787
|
ui.beginTurn();
|
|
7870
|
-
const
|
|
7871
|
-
|
|
7872
|
-
|
|
7873
|
-
|
|
7874
|
-
|
|
8788
|
+
const memoryGuard = makeMemoryGuard(ui, loadArch().eventsHeading);
|
|
8789
|
+
let res;
|
|
8790
|
+
try {
|
|
8791
|
+
res = await runNativeTurn({
|
|
8792
|
+
kind: rt.kind,
|
|
8793
|
+
bin: which(RUNTIME_BIN[rt.kind]) || RUNTIME_BIN[rt.kind],
|
|
8794
|
+
prompt,
|
|
8795
|
+
systemPrompt: sys,
|
|
8796
|
+
cwd,
|
|
8797
|
+
permission,
|
|
8798
|
+
session: {},
|
|
8799
|
+
model: ctx.model || null,
|
|
8800
|
+
effort: ctx.effort || null,
|
|
8801
|
+
mcpServers,
|
|
8802
|
+
mcpAllowlistMode: ctx.mcpAllowlistMode,
|
|
8803
|
+
env,
|
|
8804
|
+
ui: memoryGuard,
|
|
8805
|
+
});
|
|
8806
|
+
} finally {
|
|
8807
|
+
ui.endTurn();
|
|
8808
|
+
}
|
|
8809
|
+
const nativeText = String(res.text || "");
|
|
8810
|
+
const memoryResult = await completeMemoryTurnCli(db, nativeText, ctx, rt, {
|
|
8811
|
+
model: ctx.model || rt.model,
|
|
8812
|
+
outcome: res.error ? "failed" : "succeeded",
|
|
8813
|
+
requestText: prompt,
|
|
8814
|
+
// Every failed runtime needs a deterministic receipt, but must not
|
|
8815
|
+
// recursively call the runtime that just failed.
|
|
8816
|
+
invokeCurator: !res.error,
|
|
8817
|
+
});
|
|
8818
|
+
memorySettled = true;
|
|
8819
|
+
for (const memory of memoryResult.curatedMemories || []) {
|
|
8820
|
+
if (memory && !curatedMemories.some((item) => item.id === memory.id)) curatedMemories.push(memory);
|
|
8821
|
+
}
|
|
8822
|
+
finalizeExperienceExecutionCli(db, {
|
|
8823
|
+
agentId: ctx.agentId,
|
|
8824
|
+
projectPath: ctx.projectPath,
|
|
7875
8825
|
cwd,
|
|
7876
|
-
|
|
7877
|
-
|
|
7878
|
-
model:
|
|
7879
|
-
|
|
8826
|
+
runtime: rt,
|
|
8827
|
+
permission: ctx.permission,
|
|
8828
|
+
model: ctx.model || rt.model,
|
|
8829
|
+
runtimeExperience: ctx.runtimeExperience,
|
|
7880
8830
|
mcpServers,
|
|
7881
|
-
|
|
7882
|
-
|
|
8831
|
+
curatedMemories,
|
|
8832
|
+
taskHint: prompt,
|
|
8833
|
+
outcome: { status: res.error ? "failed" : "succeeded", failureCode: res.error ? "runtime-error" : null },
|
|
8834
|
+
usage: res.usage,
|
|
8835
|
+
durationMs: Date.now() - runStartedAt,
|
|
8836
|
+
runId: experienceRunId,
|
|
8837
|
+
lang: ctx.lang,
|
|
7883
8838
|
});
|
|
7884
|
-
ui.endTurn();
|
|
7885
8839
|
return res.error ? 1 : 0;
|
|
7886
8840
|
}
|
|
7887
8841
|
// API 경로 — emitter 동봉 → 답변에서 메모리 이벤트를 파싱·큐레이션하고 블록은 제거.
|
|
7888
|
-
const sys = augmentSystem(db,
|
|
8842
|
+
const sys = augmentSystem(db, runtimeSystem, ctx, true, prompt);
|
|
7889
8843
|
const env = await buildChildEnvCli(db, { ...ctx, cwd: ctx.cwd || projectCwd() });
|
|
7890
8844
|
Object.assign(process.env, env);
|
|
7891
|
-
|
|
7892
|
-
|
|
7893
|
-
|
|
8845
|
+
const selectedModel = ctx.model || rt.model;
|
|
8846
|
+
process.stderr.write(`▸ ${rt.backend}${selectedModel ? " · " + selectedModel : ""}\n`);
|
|
8847
|
+
let text;
|
|
8848
|
+
try {
|
|
8849
|
+
text = await runApi(rt.backend, selectedModel, sys, prompt);
|
|
8850
|
+
} catch (error) {
|
|
8851
|
+
finalizeExperienceExecutionCli(db, {
|
|
8852
|
+
agentId: ctx.agentId,
|
|
8853
|
+
projectPath: ctx.projectPath,
|
|
8854
|
+
cwd: ctx.cwd || projectCwd(),
|
|
8855
|
+
runtime: rt,
|
|
8856
|
+
permission: ctx.permission,
|
|
8857
|
+
model: selectedModel,
|
|
8858
|
+
runtimeExperience: ctx.runtimeExperience,
|
|
8859
|
+
curatedMemories,
|
|
8860
|
+
taskHint: prompt,
|
|
8861
|
+
outcome: { status: "failed", failureCode: "runtime-error" },
|
|
8862
|
+
durationMs: Date.now() - runStartedAt,
|
|
8863
|
+
runId: experienceRunId,
|
|
8864
|
+
lang: ctx.lang,
|
|
8865
|
+
});
|
|
8866
|
+
throw error;
|
|
8867
|
+
}
|
|
8868
|
+
const memoryResult = await completeMemoryTurnCli(db, text || "", ctx, rt, {
|
|
8869
|
+
model: selectedModel,
|
|
8870
|
+
outcome: "succeeded",
|
|
8871
|
+
requestText: prompt,
|
|
8872
|
+
});
|
|
8873
|
+
memorySettled = true;
|
|
8874
|
+
for (const memory of memoryResult.curatedMemories || []) {
|
|
8875
|
+
if (memory && !curatedMemories.some((item) => item.id === memory.id)) curatedMemories.push(memory);
|
|
8876
|
+
}
|
|
8877
|
+
const cleaned = require("./agentlas-style.cjs").sanitizeAssistantText(memoryResult.cleaned || "");
|
|
8878
|
+
finalizeExperienceExecutionCli(db, {
|
|
8879
|
+
agentId: ctx.agentId,
|
|
8880
|
+
projectPath: ctx.projectPath,
|
|
8881
|
+
cwd: ctx.cwd || projectCwd(),
|
|
8882
|
+
runtime: rt,
|
|
8883
|
+
permission: ctx.permission,
|
|
8884
|
+
model: selectedModel,
|
|
8885
|
+
runtimeExperience: ctx.runtimeExperience,
|
|
8886
|
+
curatedMemories,
|
|
8887
|
+
taskHint: prompt,
|
|
8888
|
+
outcome: { status: "succeeded", failureCode: null },
|
|
8889
|
+
durationMs: Date.now() - runStartedAt,
|
|
8890
|
+
runId: experienceRunId,
|
|
8891
|
+
lang: ctx.lang,
|
|
8892
|
+
});
|
|
7894
8893
|
process.stdout.write((cleaned || "").trim() + "\n");
|
|
7895
8894
|
return 0;
|
|
8895
|
+
} catch (error) {
|
|
8896
|
+
if (!memorySettled) {
|
|
8897
|
+
try {
|
|
8898
|
+
await completeMemoryTurnCli(db, "", ctx, memoryRuntime, {
|
|
8899
|
+
model: ctx.model || memoryRuntime?.model || null,
|
|
8900
|
+
outcome: "failed",
|
|
8901
|
+
requestText: prompt,
|
|
8902
|
+
invokeCurator: false,
|
|
8903
|
+
});
|
|
8904
|
+
memorySettled = true;
|
|
8905
|
+
} catch {
|
|
8906
|
+
// A missing/locked DB is the only remaining case where a receipt may
|
|
8907
|
+
// be impossible. Preserve the original runtime error for the caller.
|
|
8908
|
+
}
|
|
8909
|
+
}
|
|
8910
|
+
throw error;
|
|
8911
|
+
}
|
|
8912
|
+
}
|
|
8913
|
+
|
|
8914
|
+
async function runTerminalBuilder(db, request, metadata = {}, runtimeOverride = null, cwd = projectCwd()) {
|
|
8915
|
+
const builder = resolveMetaBuilder(db);
|
|
8916
|
+
if (!builder) throw new Error("Agentlas Core Engine Meta-Agent is unavailable; Build did not start.");
|
|
8917
|
+
const runtime = resolveRuntime(db, runtimeOverride);
|
|
8918
|
+
const routingOptions = metadata.workloadRouting && typeof metadata.workloadRouting === "object"
|
|
8919
|
+
? metadata.workloadRouting
|
|
8920
|
+
: {};
|
|
8921
|
+
let allocation = null;
|
|
8922
|
+
try {
|
|
8923
|
+
const currentInventory = currentRuntimeInventoryCli(db, runtime);
|
|
8924
|
+
const plannerSystem = workloadRouting.plannerSystemPrompt({
|
|
8925
|
+
language: prefsLang() === "ko" ? "Korean" : "English",
|
|
8926
|
+
maxTasks: 1,
|
|
8927
|
+
mode: "builder",
|
|
8928
|
+
liveRuntimeInventory: workloadRouting.runtimeInventory([currentInventory]),
|
|
8929
|
+
});
|
|
8930
|
+
let plannerText;
|
|
8931
|
+
if (runtime.mode === "cli") {
|
|
8932
|
+
const env = await buildChildEnvCli(db, { projectPath: cwd, agentId: builder.id, permission: "read", cwd });
|
|
8933
|
+
plannerText = await captureRuntime(runtime.kind, plannerSystem, request, {
|
|
8934
|
+
cwd,
|
|
8935
|
+
env,
|
|
8936
|
+
permission: "read",
|
|
8937
|
+
model: routingOptions.modelPin || runtime.model || null,
|
|
8938
|
+
effort: routingOptions.effortPin === undefined ? null : routingOptions.effortPin,
|
|
8939
|
+
});
|
|
8940
|
+
} else {
|
|
8941
|
+
plannerText = await runApi(runtime.backend, routingOptions.modelPin || runtime.model, plannerSystem, request);
|
|
8942
|
+
}
|
|
8943
|
+
const plan = workloadRouting.normalizePlan(plannerText, { maxTasks: 1 });
|
|
8944
|
+
allocation = plan && plan.tasks[0] && plan.tasks[0].allocation;
|
|
8945
|
+
} catch (error) {
|
|
8946
|
+
process.stderr.write(`▸ builder model planner fallback · ${String((error && error.message) || error).slice(0, 160)}\n`);
|
|
8947
|
+
}
|
|
8948
|
+
const currentInventory = currentRuntimeInventoryCli(db, runtime);
|
|
8949
|
+
const resolution = workloadRouting.resolveAllocation({
|
|
8950
|
+
runtime: currentInventory,
|
|
8951
|
+
decision: allocation,
|
|
8952
|
+
modelPin: routingOptions.modelPin,
|
|
8953
|
+
effortPin: routingOptions.effortPin,
|
|
8954
|
+
availableModels: currentInventory.availableModels,
|
|
8955
|
+
maxTier: routingOptions.maxTier || process.env.AGENTLAS_MODEL_MAX_TIER,
|
|
8956
|
+
});
|
|
8957
|
+
const receipt = workloadRouting.createDecisionReceipt({
|
|
8958
|
+
taskId: "builder-execution",
|
|
8959
|
+
stage: "builder",
|
|
8960
|
+
decision: allocation,
|
|
8961
|
+
resolution,
|
|
8962
|
+
});
|
|
8963
|
+
try {
|
|
8964
|
+
workloadRouting.appendDecisionReceipt(receipt, path.join(userDataDir(), "model-routing-receipts.jsonl"));
|
|
8965
|
+
} catch (error) {
|
|
8966
|
+
process.stderr.write(`▸ builder model routing receipt failed · ${String((error && error.message) || error).slice(0, 120)}\n`);
|
|
8967
|
+
}
|
|
8968
|
+
if (!resolution.ok) {
|
|
8969
|
+
throw new Error(`Agentlas builder model allocation failed closed: ${resolution.fallbackReason || "no compliant live model"}`);
|
|
8970
|
+
}
|
|
8971
|
+
process.stderr.write(
|
|
8972
|
+
`▸ builder model route · ${resolution.source} · ${resolution.model || runtime.kind || runtime.backend}` +
|
|
8973
|
+
`${resolution.effort ? ` · ${resolution.effort}` : ""}` +
|
|
8974
|
+
`${resolution.fallbackReason ? ` · ${resolution.fallbackReason}` : ""}\n`,
|
|
8975
|
+
);
|
|
8976
|
+
const code = await executeOnce(db, agentSystemPromptCli(builder), request, runtimeOverride, {
|
|
8977
|
+
projectPath: cwd,
|
|
8978
|
+
agentId: builder.id,
|
|
8979
|
+
permission: "full",
|
|
8980
|
+
// This is an exact private host object created after consent. An empty array
|
|
8981
|
+
// deliberately overrides all global/project/default MCP configuration.
|
|
8982
|
+
mcpServers: Array.isArray(metadata.mcpServers) ? metadata.mcpServers : [],
|
|
8983
|
+
mcpAllowlistMode: "exact",
|
|
8984
|
+
model: resolution.model,
|
|
8985
|
+
effort: resolution.effort,
|
|
8986
|
+
});
|
|
8987
|
+
if (code !== 0) throw new Error(`Agentlas builder runtime exited ${code}`);
|
|
8988
|
+
return code;
|
|
8989
|
+
}
|
|
8990
|
+
|
|
8991
|
+
async function allocateSingleWorkloadCli(db, request, options = {}) {
|
|
8992
|
+
const runtime = options.runtime || resolveRuntime(db, options.runtimeOverride);
|
|
8993
|
+
const cwd = options.cwd || projectCwd();
|
|
8994
|
+
let allocation = null;
|
|
8995
|
+
try {
|
|
8996
|
+
const currentInventory = currentRuntimeInventoryCli(db, runtime);
|
|
8997
|
+
const plannerSystem = workloadRouting.plannerSystemPrompt({
|
|
8998
|
+
language: options.lang === "ko" ? "Korean" : "English",
|
|
8999
|
+
maxTasks: 1,
|
|
9000
|
+
mode: options.mode || "team",
|
|
9001
|
+
liveRuntimeInventory: workloadRouting.runtimeInventory([currentInventory]),
|
|
9002
|
+
});
|
|
9003
|
+
let plannerText;
|
|
9004
|
+
if (runtime.mode === "cli") {
|
|
9005
|
+
const env = await buildChildEnvCli(db, {
|
|
9006
|
+
projectPath: options.projectPath || null,
|
|
9007
|
+
agentId: options.agentId || null,
|
|
9008
|
+
permission: "read",
|
|
9009
|
+
cwd,
|
|
9010
|
+
});
|
|
9011
|
+
plannerText = await captureRuntime(runtime.kind, plannerSystem, request, {
|
|
9012
|
+
cwd,
|
|
9013
|
+
env,
|
|
9014
|
+
permission: "read",
|
|
9015
|
+
model: options.modelPin || runtime.model || null,
|
|
9016
|
+
effort: options.effortPin === undefined ? null : options.effortPin,
|
|
9017
|
+
});
|
|
9018
|
+
} else {
|
|
9019
|
+
plannerText = await runApi(runtime.backend, options.modelPin || runtime.model, plannerSystem, request);
|
|
9020
|
+
}
|
|
9021
|
+
const plan = workloadRouting.normalizePlan(plannerText, { maxTasks: 1 });
|
|
9022
|
+
allocation = plan && plan.tasks[0] && plan.tasks[0].allocation;
|
|
9023
|
+
} catch (error) {
|
|
9024
|
+
if (options.onWarning) options.onWarning(`model planner fallback: ${String((error && error.message) || error).slice(0, 160)}`);
|
|
9025
|
+
}
|
|
9026
|
+
const currentInventory = currentRuntimeInventoryCli(db, runtime);
|
|
9027
|
+
const resolution = workloadRouting.resolveAllocation({
|
|
9028
|
+
runtime: currentInventory,
|
|
9029
|
+
decision: allocation,
|
|
9030
|
+
modelPin: options.modelPin,
|
|
9031
|
+
effortPin: options.effortPin,
|
|
9032
|
+
availableModels: options.availableModels || currentInventory.availableModels,
|
|
9033
|
+
maxTier: options.maxTier || process.env.AGENTLAS_MODEL_MAX_TIER,
|
|
9034
|
+
});
|
|
9035
|
+
const receipt = workloadRouting.createDecisionReceipt({
|
|
9036
|
+
taskId: options.taskId || `${options.mode || "team"}-execution`,
|
|
9037
|
+
stage: options.mode || "team",
|
|
9038
|
+
decision: allocation,
|
|
9039
|
+
resolution,
|
|
9040
|
+
});
|
|
9041
|
+
try {
|
|
9042
|
+
workloadRouting.appendDecisionReceipt(receipt, options.receiptFile || path.join(userDataDir(), "model-routing-receipts.jsonl"));
|
|
9043
|
+
} catch (error) {
|
|
9044
|
+
if (options.onWarning) options.onWarning(`model routing receipt failed: ${String((error && error.message) || error).slice(0, 120)}`);
|
|
9045
|
+
}
|
|
9046
|
+
if (!resolution.ok) {
|
|
9047
|
+
throw new Error(`Agentlas model allocation failed closed: ${resolution.fallbackReason || "no compliant live model"}`);
|
|
9048
|
+
}
|
|
9049
|
+
return { allocation, resolution, receipt };
|
|
9050
|
+
}
|
|
9051
|
+
|
|
9052
|
+
async function probeApprovedTerminalMcp(db, server, runtimeOverride, cwd, probeOptions = {}) {
|
|
9053
|
+
const runtime = resolveRuntime(db, runtimeOverride);
|
|
9054
|
+
if (runtime.mode !== "cli") return { connected: false, reason: "runtime_incompatible" };
|
|
9055
|
+
const env = await buildChildEnvCli(db, { cwd, permission: "full" });
|
|
9056
|
+
if (runtime.kind === "gemini") {
|
|
9057
|
+
const readiness = require("./agentlas-native-host.cjs").geminiMcpIsolationReadiness(env);
|
|
9058
|
+
if (!readiness.ready) return { connected: false, reason: "runtime_isolation_unavailable" };
|
|
9059
|
+
}
|
|
9060
|
+
return terminalAssets.probeSystemMcpServerConnection(server, {
|
|
9061
|
+
cwd,
|
|
9062
|
+
env,
|
|
9063
|
+
userDataDir: userDataDir(),
|
|
9064
|
+
timeoutMs: probeOptions.timeoutMs,
|
|
9065
|
+
signal: probeOptions.signal,
|
|
9066
|
+
});
|
|
7896
9067
|
}
|
|
7897
9068
|
|
|
7898
9069
|
// API 백엔드용 간이 대화형 REPL (네이티브 인터랙티브가 없는 BYOK/Ollama).
|
|
@@ -7901,7 +9072,7 @@ function apiRepl(db, backend, model, system, label, ctx) {
|
|
|
7901
9072
|
ctx = ctx || { projectPath: null, agentId: null };
|
|
7902
9073
|
if (!ctx.cwdAtRequest) ctx.cwdAtRequest = ctx.cwd || projectCwd();
|
|
7903
9074
|
const readline = require("node:readline");
|
|
7904
|
-
process.stderr.write(`▸ ${label} (${backend}${model ? " · " + model : ""}) —
|
|
9075
|
+
process.stderr.write(`▸ ${label} (${backend}${model ? " · " + model : ""}) — type /exit to stop\n`);
|
|
7905
9076
|
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
7906
9077
|
const ask = () =>
|
|
7907
9078
|
rl.question("\nyou › ", async (line) => {
|
|
@@ -7909,7 +9080,7 @@ function apiRepl(db, backend, model, system, label, ctx) {
|
|
|
7909
9080
|
if (tt === "/exit" || tt === "/quit") return rl.close();
|
|
7910
9081
|
if (!tt) return ask();
|
|
7911
9082
|
try {
|
|
7912
|
-
const sys = augmentSystem(db, system, ctx, true);
|
|
9083
|
+
const sys = augmentSystem(db, system, ctx, true, tt);
|
|
7913
9084
|
const text = await runApi(backend, model, sys, tt);
|
|
7914
9085
|
const cleaned = curateCliReply(db, text || "", ctx);
|
|
7915
9086
|
process.stdout.write("\n" + (cleaned || "").trim() + "\n");
|
|
@@ -7955,12 +9126,6 @@ function runCwd() {
|
|
|
7955
9126
|
}
|
|
7956
9127
|
}
|
|
7957
9128
|
|
|
7958
|
-
function cliMcpConfigPath() {
|
|
7959
|
-
return require("./agentlas-native-host.cjs").cliMcpConfigPath([]).file;
|
|
7960
|
-
}
|
|
7961
|
-
|
|
7962
|
-
const CODEX_PLAYWRIGHT_MCP_ARGS = require("./agentlas-native-host.cjs").codexMcpArgs([]);
|
|
7963
|
-
|
|
7964
9129
|
// 에이전트가 실제로 실행될 작업 폴더 = 사용자가 명령을 친 현재 디렉터리(= 대상 프로젝트).
|
|
7965
9130
|
// 단, home/userData/agent-cwd 같은 "프로젝트 아님" 위치면 안전한 전용 폴더로 폴백한다.
|
|
7966
9131
|
function projectCwd() {
|
|
@@ -8055,13 +9220,28 @@ const PROTECTED_CHILD_ENV_KEYS_CLI = new Set([
|
|
|
8055
9220
|
"AGENTLAS_NATIVE_IDLE_TIMEOUT_MS", "AGENTLAS_NATIVE_TOTAL_TIMEOUT_MS", "AGENTLAS_NATIVE_KILL_GRACE_MS",
|
|
8056
9221
|
"AGENTLAS_CAPTURE_MAX_OUTPUT_BYTES",
|
|
8057
9222
|
]);
|
|
8058
|
-
|
|
8059
|
-
|
|
9223
|
+
// 네트워크 무결성 키 — TLS 검증·프록시·CA·엔드포인트·세션. 프로젝트/에이전트 dotenv(신뢰 불가:
|
|
9224
|
+
// 클론한 레포에 딸려올 수 있음)로 주입되면 MITM/SSRF/세션 하이재킹이 된다. 단, 사용자 본인의
|
|
9225
|
+
// 전역 credentials.env와 호스트 셸 env는 신뢰하므로 그대로 허용한다. 사고 방지: 원샷 API 경로는
|
|
9226
|
+
// buildChildEnvCli 결과를 process.env에 병합(Object.assign)하므로, 프로젝트 .env가 부모 프로세스의
|
|
9227
|
+
// 클라우드 호출(세션 쿠키 동반)까지 오염시킬 수 있었다.
|
|
9228
|
+
const UNTRUSTED_PROTECTED_ENV_KEYS_CLI = new Set([
|
|
9229
|
+
"NODE_TLS_REJECT_UNAUTHORIZED", "NODE_EXTRA_CA_CERTS", "SSL_CERT_FILE", "SSL_CERT_DIR",
|
|
9230
|
+
"OPENSSL_CONF", "REQUESTS_CA_BUNDLE", "CURL_CA_BUNDLE",
|
|
9231
|
+
"HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "NO_PROXY", "GRPC_PROXY", "NPM_CONFIG_PROXY",
|
|
9232
|
+
"AGENTLAS_SESSION", "AGENTLAS_MCP_BASE_URL", "AGENTLAS_WEB_BASE_URL", "AGENTLAS_API_BASE_URL",
|
|
9233
|
+
"AGENTLAS_HUB_BASE_URL", "AGENTLAS_CLOUD_BASE_URL", "OLLAMA_HOST",
|
|
9234
|
+
]);
|
|
9235
|
+
function isProtectedChildEnvKeyCli(key, trusted) {
|
|
9236
|
+
const k = String(key || "").trim().toUpperCase();
|
|
9237
|
+
if (PROTECTED_CHILD_ENV_KEYS_CLI.has(k)) return true; // 호스트 신원/플러그인 루트 — 모든 출처 차단
|
|
9238
|
+
if (!trusted && UNTRUSTED_PROTECTED_ENV_KEYS_CLI.has(k)) return true; // 네트워크 무결성 — 비신뢰 출처만 차단
|
|
9239
|
+
return false;
|
|
8060
9240
|
}
|
|
8061
|
-
function mergeChildEnvValuesCli(target, values, overwrite) {
|
|
9241
|
+
function mergeChildEnvValuesCli(target, values, overwrite, trusted) {
|
|
8062
9242
|
const injected = [];
|
|
8063
9243
|
for (const [key, value] of Object.entries(values || {})) {
|
|
8064
|
-
if (!value || isProtectedChildEnvKeyCli(key)) continue;
|
|
9244
|
+
if (!value || isProtectedChildEnvKeyCli(key, trusted)) continue;
|
|
8065
9245
|
if (!overwrite && target[key]) continue;
|
|
8066
9246
|
target[key] = value;
|
|
8067
9247
|
injected.push(key);
|
|
@@ -8070,19 +9250,20 @@ function mergeChildEnvValuesCli(target, values, overwrite) {
|
|
|
8070
9250
|
}
|
|
8071
9251
|
async function buildChildEnvCli(db, ctx) {
|
|
8072
9252
|
const env = { ...process.env };
|
|
8073
|
-
|
|
8074
|
-
|
|
9253
|
+
// trusted=true: 사용자 본인의 전역 자격/볼트. trusted=false: 프로젝트·에이전트 폴더 dotenv.
|
|
9254
|
+
const apply = (values, overwrite, trusted) => {
|
|
9255
|
+
mergeChildEnvValuesCli(env, values, overwrite, trusted);
|
|
8075
9256
|
};
|
|
8076
9257
|
const globalCredentials = {
|
|
8077
9258
|
...readDotEnvFileCli(path.join(userDataDir(), "credentials.env")),
|
|
8078
9259
|
...readDotEnvFileCli(path.join(os.homedir(), ".agentlas", "credentials.env")),
|
|
8079
9260
|
};
|
|
8080
|
-
apply(globalCredentials, false);
|
|
8081
|
-
if (ctx && ctx.projectPath) apply(projectScopedEnvValuesCli(globalCredentials, ctx.projectPath), true);
|
|
8082
|
-
if (ctx && ctx.cwd) apply(readDotEnvDirCli(ctx.cwd), true);
|
|
8083
|
-
if (ctx && ctx.projectPath) apply(readDotEnvDirCli(ctx.projectPath), true);
|
|
9261
|
+
apply(globalCredentials, false, true);
|
|
9262
|
+
if (ctx && ctx.projectPath) apply(projectScopedEnvValuesCli(globalCredentials, ctx.projectPath), true, true);
|
|
9263
|
+
if (ctx && ctx.cwd) apply(readDotEnvDirCli(ctx.cwd), true, false);
|
|
9264
|
+
if (ctx && ctx.projectPath) apply(readDotEnvDirCli(ctx.projectPath), true, false);
|
|
8084
9265
|
const agentDir = agentEnvDirCli(ctx && ctx.agentId);
|
|
8085
|
-
if (agentDir) apply(readDotEnvDirCli(agentDir), true);
|
|
9266
|
+
if (agentDir) apply(readDotEnvDirCli(agentDir), true, false);
|
|
8086
9267
|
|
|
8087
9268
|
const mm = loadMultimodalCatalog();
|
|
8088
9269
|
const settings = getMultimodalSettingsCli(db);
|
|
@@ -8091,7 +9272,7 @@ async function buildChildEnvCli(db, ctx) {
|
|
|
8091
9272
|
if (req && req.key) keys.add(req.key);
|
|
8092
9273
|
}
|
|
8093
9274
|
const vaultValues = await readVaultEnvValuesCli([...keys].filter((key) => !env[key]), ctx && ctx.projectPath);
|
|
8094
|
-
apply(vaultValues, false);
|
|
9275
|
+
apply(vaultValues, false, true); // 볼트는 사용자 본인 저장소 — 신뢰
|
|
8095
9276
|
env.AGENTLAS_MULTIMODAL_IMAGE_PROVIDER = settings.imageProvider;
|
|
8096
9277
|
env.AGENTLAS_MULTIMODAL_VIDEO_PROVIDER = settings.videoProvider;
|
|
8097
9278
|
env.AGENTLAS_MULTIMODAL_AUDIO_PROVIDER = settings.audioProvider;
|
|
@@ -8100,29 +9281,89 @@ async function buildChildEnvCli(db, ctx) {
|
|
|
8100
9281
|
|
|
8101
9282
|
// One-shot/background capture uses the same permission truth as the interactive host.
|
|
8102
9283
|
// Keep its plain-output argument shape, but never duplicate the security mapping here.
|
|
8103
|
-
function buildArgs(kind, systemPrompt, prompt, permission) {
|
|
9284
|
+
function buildArgs(kind, systemPrompt, prompt, permission, runtimeOptions = {}) {
|
|
8104
9285
|
const native = require("./agentlas-native-host.cjs");
|
|
8105
9286
|
const level = require("./agentlas-permissions.cjs").normalize(permission);
|
|
9287
|
+
const model = runtimeOptions.model ? String(runtimeOptions.model) : null;
|
|
9288
|
+
const effort = runtimeOptions.effort ? String(runtimeOptions.effort) : null;
|
|
9289
|
+
const noAuthority = runtimeOptions.authorityMode === "no-authority";
|
|
8106
9290
|
if (kind === "claude-code") {
|
|
8107
|
-
const perm = native.claudePermissionArgs(level);
|
|
8108
|
-
|
|
8109
|
-
|
|
8110
|
-
|
|
8111
|
-
|
|
9291
|
+
const perm = native.claudePermissionArgs(noAuthority ? "read" : level);
|
|
9292
|
+
// Background/capture has no one-pass reviewed server list. Full permission
|
|
9293
|
+
// changes tool authority, not MCP consent, so this path remains exact-empty.
|
|
9294
|
+
const mcp = native.claudeMcpIsolationArgs();
|
|
9295
|
+
const thinking = effort === "max" || effort === "xhigh" ? "Ultrathink. " : effort === "high" ? "Think hard. " : effort === "medium" ? "Think. " : "";
|
|
9296
|
+
const claudeEffort = effort === "minimal" ? "low" : effort === "xhigh" ? "max" : effort;
|
|
9297
|
+
const effortArgs = claudeEffort && claudeEffort !== "none" ? ["--effort", claudeEffort] : [];
|
|
9298
|
+
return [
|
|
9299
|
+
"-p", thinking + prompt,
|
|
9300
|
+
"--append-system-prompt", systemPrompt,
|
|
9301
|
+
...(model ? ["--model", model] : []),
|
|
9302
|
+
...effortArgs,
|
|
9303
|
+
...perm,
|
|
9304
|
+
...(noAuthority ? ["--tools", ""] : []),
|
|
9305
|
+
...mcp,
|
|
9306
|
+
];
|
|
8112
9307
|
}
|
|
8113
9308
|
if (kind === "codex") {
|
|
8114
|
-
const perm = native.codexPermissionArgs(level);
|
|
8115
|
-
const mcp =
|
|
8116
|
-
|
|
9309
|
+
const perm = native.codexPermissionArgs(noAuthority ? "read" : level);
|
|
9310
|
+
const mcp = [];
|
|
9311
|
+
const modelArgs = model ? ["-m", model] : [];
|
|
9312
|
+
const effortArgs = effort ? ["-c", `model_reasoning_effort="${effort}"`] : [];
|
|
9313
|
+
const noAuthorityArgs = noAuthority ? [
|
|
9314
|
+
"--ephemeral",
|
|
9315
|
+
"--ignore-user-config",
|
|
9316
|
+
"--ignore-rules",
|
|
9317
|
+
"--disable", "shell_tool",
|
|
9318
|
+
"--disable", "unified_exec",
|
|
9319
|
+
"--disable", "apps",
|
|
9320
|
+
"--disable", "browser_use",
|
|
9321
|
+
"--disable", "computer_use",
|
|
9322
|
+
"--disable", "image_generation",
|
|
9323
|
+
"--disable", "workspace_dependencies",
|
|
9324
|
+
"--disable", "goals",
|
|
9325
|
+
"--disable", "memories",
|
|
9326
|
+
"--disable", "plugins",
|
|
9327
|
+
"--disable", "hooks",
|
|
9328
|
+
"--disable", "multi_agent",
|
|
9329
|
+
"--disable", "tool_suggest",
|
|
9330
|
+
"--json",
|
|
9331
|
+
] : [];
|
|
9332
|
+
return ["exec", "--skip-git-repo-check", ...noAuthorityArgs, ...modelArgs, ...effortArgs, ...perm, ...mcp, `[SYSTEM]\n${systemPrompt}\n\n${prompt}`];
|
|
8117
9333
|
}
|
|
8118
9334
|
if (kind === "gemini") {
|
|
8119
|
-
const perm = native.geminiPermissionArgs(level);
|
|
8120
|
-
|
|
8121
|
-
|
|
9335
|
+
const perm = native.geminiPermissionArgs(noAuthority ? "read" : level);
|
|
9336
|
+
if (noAuthority && !runtimeOptions.noToolsPolicyPath) {
|
|
9337
|
+
throw new Error("Gemini no-authority capture requires an explicit deny-all policy");
|
|
9338
|
+
}
|
|
9339
|
+
const noAuthorityArgs = noAuthority
|
|
9340
|
+
? ["--admin-policy", String(runtimeOptions.noToolsPolicyPath)]
|
|
9341
|
+
: [];
|
|
9342
|
+
// Legacy/background capture has no structured reviewed server list. Even
|
|
9343
|
+
// at full permission it must stay exact-empty instead of inheriting the
|
|
9344
|
+
// user's global Gemini MCP definitions with the provider credential env.
|
|
9345
|
+
const mcp = native.geminiMcpIsolationArgs();
|
|
9346
|
+
return ["--prompt", `[SYSTEM]\n${systemPrompt}\n\n${prompt}`, ...(model ? ["-m", model] : []), ...perm, ...noAuthorityArgs, ...mcp];
|
|
8122
9347
|
}
|
|
8123
9348
|
return [prompt];
|
|
8124
9349
|
}
|
|
8125
9350
|
|
|
9351
|
+
function codexCaptureAgentText(jsonl) {
|
|
9352
|
+
const completed = [];
|
|
9353
|
+
const latest = new Map();
|
|
9354
|
+
for (const line of String(jsonl || "").split(/\r?\n/)) {
|
|
9355
|
+
if (!line.trim()) continue;
|
|
9356
|
+
let event;
|
|
9357
|
+
try { event = JSON.parse(line); } catch { continue; }
|
|
9358
|
+
const item = event?.item;
|
|
9359
|
+
if (!item || item.type !== "agent_message" || typeof item.text !== "string") continue;
|
|
9360
|
+
if (event.type === "item.completed") completed.push(item.text);
|
|
9361
|
+
else if (event.type === "item.started" || event.type === "item.updated") latest.set(String(item.id || latest.size), item.text);
|
|
9362
|
+
}
|
|
9363
|
+
if (completed.length) return completed.join("");
|
|
9364
|
+
return [...latest.values()].join("");
|
|
9365
|
+
}
|
|
9366
|
+
|
|
8126
9367
|
// `claude` 치면 바로 대화형 세션 뜨듯이 — 에이전트 폴더(CLAUDE.md/AGENTS.md/GEMINI.md 보유)에서
|
|
8127
9368
|
// 네이티브 CLI를 인자 없이(대화형) 실행. 에이전트 페르소나는 그 폴더의 프로젝트 지시로 자동 로드. (A+B 결합)
|
|
8128
9369
|
// 보스턴테리어 터미널(대화형 TUI)로 진입. agentlas 가 항상 "호스트"다 —
|
|
@@ -8145,14 +9386,23 @@ function buildHelpers(db) {
|
|
|
8145
9386
|
return {
|
|
8146
9387
|
which,
|
|
8147
9388
|
RUNTIME_BIN,
|
|
8148
|
-
augmentSystem: (db_, base, ctx, emit) => augmentSystem(db_, base, ctx, emit),
|
|
8149
|
-
|
|
9389
|
+
augmentSystem: (db_, base, ctx, emit, request) => augmentSystem(db_, base, ctx, emit, request),
|
|
9390
|
+
memoryEmitterPrompt: (request, ctx) => memoryEmitterPromptFor(
|
|
9391
|
+
request,
|
|
9392
|
+
loadArch(),
|
|
9393
|
+
ctx && ctx.turnId,
|
|
9394
|
+
ctx && ctx.permission,
|
|
9395
|
+
),
|
|
9396
|
+
beginMemoryTurn: (db_, prompt, ctx) => beginMemoryTurnCli(db_, prompt, ctx),
|
|
9397
|
+
completeMemoryTurn: (db_, text, ctx, runtime, options) => completeMemoryTurnCli(db_, text, ctx, runtime, options),
|
|
8150
9398
|
detectResponseLanguage: (prompt, fallback) => require("./agentlas-style.cjs").detectResponseLanguage(prompt, fallback),
|
|
8151
9399
|
sanitizeAssistantText: (text) => require("./agentlas-style.cjs").sanitizeAssistantText(text),
|
|
8152
9400
|
apiKey: (backend) => apiKey(backend),
|
|
8153
9401
|
eventsHeading: () => loadArch().eventsHeading,
|
|
8154
9402
|
defaultApiModel: (backend) => DEFAULT_API_MODEL[backend],
|
|
8155
9403
|
buildChildEnv: (db_, ctx) => buildChildEnvCli(db_, ctx),
|
|
9404
|
+
allocateWorkload: (db_, request, ctx) => allocateSingleWorkloadCli(db_, request, ctx),
|
|
9405
|
+
finalizeExperienceRun: (db_, input) => finalizeExperienceExecutionCli(db_, input),
|
|
8156
9406
|
multimodalStatus: (db_) => multimodalStatusCli(db_),
|
|
8157
9407
|
setMultimodal: (db_, modality, providerId) => setMultimodalCli(db_, modality, providerId),
|
|
8158
9408
|
resolveAgent,
|
|
@@ -8164,11 +9414,11 @@ function buildHelpers(db) {
|
|
|
8164
9414
|
autoRouteNote: (choice, lang) => autoRouteNote(choice, lang),
|
|
8165
9415
|
autoRoutePreamble: (choice, lang) => autoRoutePreamble(choice, lang),
|
|
8166
9416
|
directSystemPrompt: (lang) => directSystemPrompt(lang),
|
|
8167
|
-
cliMemoryContext: (db_, pp) => cliMemoryContext(db_, pp),
|
|
9417
|
+
cliMemoryContext: (db_, pp, agentId) => cliMemoryContext(db_, pp, agentId),
|
|
8168
9418
|
importLocal: (db_, p) => importLocalFolderCli(db_, p),
|
|
8169
9419
|
// REPL-safe public Hub install: fail()(process.exit) 대신 Error를 throw 해 REPL이 직접 렌더하게 한다.
|
|
8170
9420
|
cloudInstall: async (db_, slug) => {
|
|
8171
|
-
if (typeof fetch !== "function") throw new Error("
|
|
9421
|
+
if (typeof fetch !== "function") throw new Error("fetch is unavailable in this runtime (app runtime required).");
|
|
8172
9422
|
const base = process.env.AGENTLAS_MCP_BASE_URL || "https://agentlas.cloud/api/mcp/v1";
|
|
8173
9423
|
const headers = { "content-type": "application/json" };
|
|
8174
9424
|
const cookie = await cloudSessionCookieCli();
|
|
@@ -8181,18 +9431,18 @@ function buildHelpers(db) {
|
|
|
8181
9431
|
body: JSON.stringify({ method: "marketplace.get_manifest", params: { name: "marketplace.get_manifest", arguments: { kind: "agent", slug } } }),
|
|
8182
9432
|
});
|
|
8183
9433
|
} catch (e) {
|
|
8184
|
-
throw new Error(`Hub
|
|
9434
|
+
throw new Error(`Hub connection failed: ${(e && e.message) || e}`);
|
|
8185
9435
|
}
|
|
8186
9436
|
if (!resp.ok) {
|
|
8187
9437
|
const authHint = resp.status === 401 || resp.status === 403 ? " — 로그인이 필요합니다 (앱에서 로그인 또는 AGENTLAS_SESSION 설정)" : "";
|
|
8188
|
-
throw new Error(`Hub
|
|
9438
|
+
throw new Error(`Hub returned HTTP ${resp.status}${authHint}`);
|
|
8189
9439
|
}
|
|
8190
9440
|
const json = parseHubJsonCli(resp, "marketplace.get_manifest");
|
|
8191
9441
|
if (json.error) throw new Error(json.error.message || "Hub error");
|
|
8192
9442
|
const listing = json.result;
|
|
8193
|
-
if (!listing) throw new Error(`
|
|
9443
|
+
if (!listing) throw new Error(`Not found in Hub: ${slug}`);
|
|
8194
9444
|
if (listing.delivery && listing.delivery.mode === "call_only") {
|
|
8195
|
-
throw new Error(
|
|
9445
|
+
throw new Error(`This Hub agent is call-only. Run: agentlas call ${slug}`);
|
|
8196
9446
|
}
|
|
8197
9447
|
return persistCloudListingCli(db_, listing);
|
|
8198
9448
|
},
|
|
@@ -8201,7 +9451,22 @@ function buildHelpers(db) {
|
|
|
8201
9451
|
},
|
|
8202
9452
|
mcpServers: (db_) => {
|
|
8203
9453
|
try {
|
|
8204
|
-
|
|
9454
|
+
const consentedIds = new Set(
|
|
9455
|
+
terminalAssets.readConsentedSystemMcpServers(db_, { userDataDir: userDataDir(), createRuntimeHome: false })
|
|
9456
|
+
.map((server) => server.id),
|
|
9457
|
+
);
|
|
9458
|
+
return db_.prepare("SELECT id, catalog_id, name, name_en, transport, command, args_json, url, env_keys_json, enabled FROM mcp_servers ORDER BY installed_at ASC")
|
|
9459
|
+
.all()
|
|
9460
|
+
.map((row) => {
|
|
9461
|
+
const runtime = terminalAssets.materializeTrustedSystemMcpServer(row, { userDataDir: userDataDir(), createRuntimeHome: false });
|
|
9462
|
+
Object.defineProperty(row, "runtimeEligible", { value: Boolean(runtime), enumerable: false });
|
|
9463
|
+
Object.defineProperty(row, "runtimeConsented", { value: Boolean(runtime && consentedIds.has(String(row.id))), enumerable: false });
|
|
9464
|
+
if (runtime) {
|
|
9465
|
+
Object.defineProperty(row, "credentialKeyNames", { value: runtime.credentialKeyNames, enumerable: false });
|
|
9466
|
+
if (runtime.mcpRuntimeHome) Object.defineProperty(row, "mcpRuntimeHome", { value: runtime.mcpRuntimeHome, enumerable: false });
|
|
9467
|
+
}
|
|
9468
|
+
return row;
|
|
9469
|
+
});
|
|
8205
9470
|
} catch {
|
|
8206
9471
|
return [];
|
|
8207
9472
|
}
|
|
@@ -8211,11 +9476,30 @@ function buildHelpers(db) {
|
|
|
8211
9476
|
try { return JSON.parse(fs.readFileSync(path.join(userDataDir(), "cli-sessions.json"), "utf8")) || []; } catch { return []; }
|
|
8212
9477
|
},
|
|
8213
9478
|
sessionsSave: (list) => {
|
|
8214
|
-
try {
|
|
9479
|
+
try { writeJsonPrivateAtomicCli(path.join(userDataDir(), "cli-sessions.json"), (list || []).slice(0, 30)); } catch { /* ignore */ }
|
|
8215
9480
|
},
|
|
8216
9481
|
// 패리티: REPL의 /storm·/swarm·/build·/route·/research 가 그대로 호출한다.
|
|
8217
9482
|
stormRun: (db_, goal, ctx) => parity().stormRun(db_, goal, ctx),
|
|
8218
9483
|
swarmRun: (db_, goal, ctx) => parity().swarmRun(db_, goal, ctx),
|
|
9484
|
+
workforceRun: (db_, goal, ctx) => workforce().workforceRun(db_, goal, ctx),
|
|
9485
|
+
terminalBuild: (db_, args, ctx = {}) => terminalAssets.cmdBuild({
|
|
9486
|
+
db: db_,
|
|
9487
|
+
args: Array.isArray(args) ? args : terminalAssets.tokenizeBuildCommandLine(String(args || "")),
|
|
9488
|
+
userDataDir: userDataDir(),
|
|
9489
|
+
cwd: ctx.cwd || projectCwd(),
|
|
9490
|
+
input: ctx.input || process.stdin,
|
|
9491
|
+
promptOutput: ctx.promptOutput || process.stderr,
|
|
9492
|
+
out: ctx.out || out,
|
|
9493
|
+
probeMcpServer: (server, probeOptions) => probeApprovedTerminalMcp(db_, server, null, ctx.cwd || projectCwd(), probeOptions),
|
|
9494
|
+
invokeBuild: (request, metadata) => runTerminalBuilder(db_, request, {
|
|
9495
|
+
...metadata,
|
|
9496
|
+
workloadRouting: {
|
|
9497
|
+
modelPin: ctx.modelPin || null,
|
|
9498
|
+
effortPin: ctx.effortPin,
|
|
9499
|
+
maxTier: ctx.maxTier,
|
|
9500
|
+
},
|
|
9501
|
+
}, null, ctx.cwd || projectCwd()),
|
|
9502
|
+
}),
|
|
8219
9503
|
hepRun: (args, opts) => parity().runHephaestusInteractive(args, opts),
|
|
8220
9504
|
cloudSearch: (db_, args) => parity().cloudSearch(db_, args),
|
|
8221
9505
|
careerGraphCommand: (text, ctx) => runCareerGraphNaturalCli(text, {
|
|
@@ -8236,12 +9520,14 @@ function buildHelpers(db) {
|
|
|
8236
9520
|
return null;
|
|
8237
9521
|
}
|
|
8238
9522
|
},
|
|
9523
|
+
ensureProjectForExecution: (db_, dir, permission, reason) =>
|
|
9524
|
+
ensureTerminalProjectForExecutionCli(db_, dir, permission, reason || "terminal-interactive-turn"),
|
|
8239
9525
|
doctor: async (db_, ui) => {
|
|
8240
9526
|
ui.line("");
|
|
8241
9527
|
ui.info("userData: " + userDataDir());
|
|
8242
|
-
ui.info("db: " + (fs.existsSync(dbPath()) ? "OK" : "
|
|
9528
|
+
ui.info("db: " + (fs.existsSync(dbPath()) ? "OK" : "missing"));
|
|
8243
9529
|
const ar = activeRuntime(db_);
|
|
8244
|
-
ui.info("
|
|
9530
|
+
ui.info("Active runtime: " + (ar ? ar.kind : "(none)"));
|
|
8245
9531
|
// CLI 런타임: 설치 + 로그인(인증 파일) 휴리스틱
|
|
8246
9532
|
const home = os.homedir();
|
|
8247
9533
|
const authFiles = {
|
|
@@ -8253,17 +9539,17 @@ function buildHelpers(db) {
|
|
|
8253
9539
|
for (const [kind, bin] of Object.entries(RUNTIME_BIN)) {
|
|
8254
9540
|
const installed = !!which(bin);
|
|
8255
9541
|
const authed = (authFiles[kind] || []).some(has);
|
|
8256
|
-
ui.info(` ${kind.padEnd(12)} ${!installed ? "
|
|
9542
|
+
ui.info(` ${kind.padEnd(12)} ${!installed ? "not installed" : authed ? "installed · signed in" : "installed · sign-in unverified"}`);
|
|
8257
9543
|
}
|
|
8258
9544
|
// BYOK 키 (keytar) + 클라우드 세션
|
|
8259
9545
|
const byok = [];
|
|
8260
9546
|
for (const b of ["anthropic", "openai", "google", "upstage"]) {
|
|
8261
9547
|
try { if (await apiKey(b)) byok.push(b); } catch { /* keytar 미사용 */ }
|
|
8262
9548
|
}
|
|
8263
|
-
ui.info("BYOK
|
|
9549
|
+
ui.info("BYOK keys: " + (byok.length ? byok.join(", ") : "(none — App settings → BYOK)"));
|
|
8264
9550
|
let cloud = false;
|
|
8265
9551
|
try { cloud = !!(await cloudSessionCookieCli()); } catch { /* ignore */ }
|
|
8266
|
-
ui.info("
|
|
9552
|
+
ui.info("Cloud session: " + (cloud ? "signed in" : "signed out"));
|
|
8267
9553
|
},
|
|
8268
9554
|
};
|
|
8269
9555
|
}
|
|
@@ -8304,14 +9590,18 @@ function spawnRuntime(kind, systemPrompt, prompt, opts) {
|
|
|
8304
9590
|
const cwd = opts.cwd || runCwd();
|
|
8305
9591
|
return new Promise((resolve) => {
|
|
8306
9592
|
const bin = which(RUNTIME_BIN[kind]) || RUNTIME_BIN[kind];
|
|
8307
|
-
const env = require("./agentlas-native-host.cjs").runtimeEnvForKind(kind, opts.env || process.env
|
|
9593
|
+
const env = require("./agentlas-native-host.cjs").runtimeEnvForKind(kind, opts.env || process.env, {
|
|
9594
|
+
permission: opts.permission,
|
|
9595
|
+
mcpServers: [],
|
|
9596
|
+
mcpAllowlistMode: kind === "gemini" ? "exact" : undefined,
|
|
9597
|
+
});
|
|
8308
9598
|
const child = spawn(bin, buildArgs(kind, systemPrompt, prompt, opts.permission), {
|
|
8309
9599
|
cwd,
|
|
8310
9600
|
stdio: ["ignore", "inherit", "inherit"],
|
|
8311
9601
|
env,
|
|
8312
9602
|
});
|
|
8313
9603
|
child.on("error", (err) => {
|
|
8314
|
-
process.stderr.write(`\
|
|
9604
|
+
process.stderr.write(`\nExecution failed (${kind}): ${err.message}\n`);
|
|
8315
9605
|
resolve(1);
|
|
8316
9606
|
});
|
|
8317
9607
|
child.on("close", (code) => resolve(code ?? 0));
|
|
@@ -8341,8 +9631,17 @@ function captureRuntime(kind, systemPrompt, prompt, opts) {
|
|
|
8341
9631
|
let child;
|
|
8342
9632
|
try {
|
|
8343
9633
|
const spawnImpl = opts.spawn || spawn;
|
|
8344
|
-
const env = require("./agentlas-native-host.cjs").runtimeEnvForKind(kind, opts.env || process.env
|
|
8345
|
-
|
|
9634
|
+
const env = require("./agentlas-native-host.cjs").runtimeEnvForKind(kind, opts.env || process.env, {
|
|
9635
|
+
permission: opts.permission,
|
|
9636
|
+
mcpServers: [],
|
|
9637
|
+
mcpAllowlistMode: kind === "gemini" ? "exact" : undefined,
|
|
9638
|
+
});
|
|
9639
|
+
child = spawnImpl(bin, buildArgs(kind, systemPrompt, prompt, opts.permission, {
|
|
9640
|
+
model: opts.model,
|
|
9641
|
+
effort: opts.effort,
|
|
9642
|
+
authorityMode: opts.authorityMode,
|
|
9643
|
+
noToolsPolicyPath: opts.noToolsPolicyPath,
|
|
9644
|
+
}), {
|
|
8346
9645
|
cwd,
|
|
8347
9646
|
stdio: ["ignore", "pipe", "pipe"],
|
|
8348
9647
|
env,
|
|
@@ -8453,7 +9752,11 @@ function captureRuntime(kind, systemPrompt, prompt, opts) {
|
|
|
8453
9752
|
finishReject(new Error(`${kind} exited ${code}: ${stderr.slice(-500)}`));
|
|
8454
9753
|
return;
|
|
8455
9754
|
}
|
|
8456
|
-
|
|
9755
|
+
const raw = stdout.trim() || stderr.trim();
|
|
9756
|
+
const captured = kind === "codex" && opts.authorityMode === "no-authority"
|
|
9757
|
+
? codexCaptureAgentText(raw)
|
|
9758
|
+
: "";
|
|
9759
|
+
finishResolve(captured || raw);
|
|
8457
9760
|
};
|
|
8458
9761
|
onAbort = () => {
|
|
8459
9762
|
const reason = opts.signal && opts.signal.reason;
|
|
@@ -8483,10 +9786,12 @@ function parity() {
|
|
|
8483
9786
|
captureRuntime,
|
|
8484
9787
|
runApi,
|
|
8485
9788
|
resolveRuntime,
|
|
9789
|
+
listAvailableRuntimes,
|
|
8486
9790
|
buildChildEnvCli,
|
|
8487
9791
|
projectCwd,
|
|
8488
9792
|
runCwd,
|
|
8489
9793
|
userDataDir,
|
|
9794
|
+
modelRoutingReceiptPath: () => path.join(userDataDir(), "model-routing-receipts.jsonl"),
|
|
8490
9795
|
resolveAgent,
|
|
8491
9796
|
resolveFirm,
|
|
8492
9797
|
listAgents,
|
|
@@ -8505,6 +9810,99 @@ function parity() {
|
|
|
8505
9810
|
return parity._i;
|
|
8506
9811
|
}
|
|
8507
9812
|
|
|
9813
|
+
// Agent Workforce Ontology is a separate, fail-closed route. Unlike the
|
|
9814
|
+
// compatibility router it gives final staffing authority to the active host
|
|
9815
|
+
// LLM and uses Hub MCP only for retrieval, validation, and pinned preparation.
|
|
9816
|
+
async function listWorkforceToolsCli({ db, roster, runtimeId, cwd, env, timeoutMs, signal }) {
|
|
9817
|
+
const mcp = require("./agentlas-experience-mcp.cjs");
|
|
9818
|
+
const servers = mcp.readConsentedSystemMcpServers(db, {
|
|
9819
|
+
userDataDir: userDataDir(),
|
|
9820
|
+
createRuntimeHome: false,
|
|
9821
|
+
}).slice(0, 8);
|
|
9822
|
+
if (!servers.length) return [];
|
|
9823
|
+
const deadline = Date.now() + Math.max(50, Math.min(12_000, Number(timeoutMs) || 12_000));
|
|
9824
|
+
const outcomes = new Array(servers.length);
|
|
9825
|
+
let cursor = 0;
|
|
9826
|
+
const worker = async () => {
|
|
9827
|
+
while (true) {
|
|
9828
|
+
const index = cursor++;
|
|
9829
|
+
if (index >= servers.length) return;
|
|
9830
|
+
const remaining = deadline - Date.now();
|
|
9831
|
+
if (remaining <= 0 || signal?.aborted) return;
|
|
9832
|
+
outcomes[index] = await mcp.probeSystemMcpServerConnection(servers[index], {
|
|
9833
|
+
cwd,
|
|
9834
|
+
env,
|
|
9835
|
+
userDataDir: userDataDir(),
|
|
9836
|
+
timeoutMs: Math.min(4_000, remaining),
|
|
9837
|
+
signal,
|
|
9838
|
+
});
|
|
9839
|
+
}
|
|
9840
|
+
};
|
|
9841
|
+
await Promise.all(Array.from({ length: Math.min(3, servers.length) }, () => worker()));
|
|
9842
|
+
|
|
9843
|
+
const safeId = /^[A-Za-z0-9][A-Za-z0-9_.$:/@+~-]{0,127}$/;
|
|
9844
|
+
const rows = [];
|
|
9845
|
+
for (let index = 0; index < servers.length; index += 1) {
|
|
9846
|
+
const server = servers[index];
|
|
9847
|
+
const listed = outcomes[index];
|
|
9848
|
+
if (!listed?.connected || !Array.isArray(listed.tools)) continue;
|
|
9849
|
+
for (const tool of listed.tools.slice(0, 256)) {
|
|
9850
|
+
if (!tool || typeof tool !== "object" || Array.isArray(tool)) continue;
|
|
9851
|
+
const declared = tool._meta?.agentlas || {};
|
|
9852
|
+
const toolId = typeof declared.toolId === "string" ? declared.toolId : String(tool.name || "");
|
|
9853
|
+
const capabilityIds = Array.isArray(declared.capabilityIds)
|
|
9854
|
+
? [...new Set(declared.capabilityIds.filter((id) => /^[A-Za-z0-9][A-Za-z0-9._:/@-]{1,255}$/.test(String(id))))]
|
|
9855
|
+
: [];
|
|
9856
|
+
if (!safeId.test(toolId) || !capabilityIds.length) continue;
|
|
9857
|
+
const schemaJson = JSON.stringify(tool.inputSchema || {}, Object.keys(tool.inputSchema || {}).sort());
|
|
9858
|
+
for (const pinned of roster || []) {
|
|
9859
|
+
if (pinned?.permissionPolicy?.mcp?.mode !== "allowlist" || !pinned.permissionPolicy.mcp.allowedTools.includes(toolId)) continue;
|
|
9860
|
+
rows.push({
|
|
9861
|
+
slotId: pinned.slotId,
|
|
9862
|
+
agentReleaseId: pinned.agentReleaseId,
|
|
9863
|
+
permissionPolicyDigest: pinned.permissionPolicyDigest,
|
|
9864
|
+
provider: "mcp",
|
|
9865
|
+
toolId,
|
|
9866
|
+
serverId: server.id,
|
|
9867
|
+
description: "Ready consented host MCP tool",
|
|
9868
|
+
inputSchemaDigest: `sha256:${crypto.createHash("sha256").update(schemaJson).digest("hex")}`,
|
|
9869
|
+
// Terminal's one-shot native/API runners do not yet expose a proven
|
|
9870
|
+
// exact per-tool attachment boundary. Preserve the real tools/list
|
|
9871
|
+
// observation, but advertise no executable runtime instead of
|
|
9872
|
+
// manufacturing authority. collectToolInventory filters this row and
|
|
9873
|
+
// fails closed before the planner for a required capability.
|
|
9874
|
+
runtimeIds: [],
|
|
9875
|
+
selectiveEnforcement: "unavailable",
|
|
9876
|
+
capabilityIds,
|
|
9877
|
+
status: "observed-not-executable",
|
|
9878
|
+
});
|
|
9879
|
+
}
|
|
9880
|
+
}
|
|
9881
|
+
}
|
|
9882
|
+
return rows;
|
|
9883
|
+
}
|
|
9884
|
+
|
|
9885
|
+
function workforce() {
|
|
9886
|
+
if (!workforce._i) {
|
|
9887
|
+
workforce._i = require("./agentlas-workforce.cjs").create({
|
|
9888
|
+
captureRuntime,
|
|
9889
|
+
runApi,
|
|
9890
|
+
resolveRuntime,
|
|
9891
|
+
buildChildEnv: buildChildEnvCli,
|
|
9892
|
+
projectCwd,
|
|
9893
|
+
userDataDir,
|
|
9894
|
+
receiptFile: () => path.join(userDataDir(), "workforce-execution-receipts.jsonl"),
|
|
9895
|
+
cloudSessionCookie: cloudSessionCookieCli,
|
|
9896
|
+
fetchHub: (url, init) => fetchHubCli(url, init),
|
|
9897
|
+
listWorkforceTools: listWorkforceToolsCli,
|
|
9898
|
+
supportsWorkforceToolAuthority: async () => false,
|
|
9899
|
+
prefsLang,
|
|
9900
|
+
out,
|
|
9901
|
+
});
|
|
9902
|
+
}
|
|
9903
|
+
return workforce._i;
|
|
9904
|
+
}
|
|
9905
|
+
|
|
8508
9906
|
// ── 명령 구현 ──────────────────────────────────────────────
|
|
8509
9907
|
function cmdList(db) {
|
|
8510
9908
|
const agents = listAgents(db);
|
|
@@ -8556,52 +9954,162 @@ function writeIfMissing(file, content) {
|
|
|
8556
9954
|
|
|
8557
9955
|
function cmdCd(db, query) {
|
|
8558
9956
|
const agent = resolveAgent(db, query);
|
|
8559
|
-
if (!agent) fail(
|
|
9957
|
+
if (!agent) fail(`Agent not found: ${query}`);
|
|
8560
9958
|
const folder = agentFolder(agent);
|
|
8561
9959
|
ensureNativeFiles(agent, folder);
|
|
8562
9960
|
// 경로만 stdout으로 (cd "$(agentlas cd seo)") — 안내는 stderr로.
|
|
8563
|
-
process.stderr.write(`# ${agent.name} —
|
|
9961
|
+
process.stderr.write(`# ${agent.name} — native CLI context ready (CLAUDE.md/AGENTS.md/GEMINI.md)\n`);
|
|
8564
9962
|
process.stdout.write(folder + "\n");
|
|
8565
9963
|
}
|
|
8566
9964
|
|
|
8567
|
-
|
|
9965
|
+
function parseRunExperienceArgs(args) {
|
|
9966
|
+
const prompt = [];
|
|
9967
|
+
const experience = { taskSignatures: [], declaredTaskClasses: [], environmentTags: [], experiencePackReleaseIds: [] };
|
|
9968
|
+
let passthrough = false;
|
|
9969
|
+
const addList = (target, value) => {
|
|
9970
|
+
for (const item of String(value || "").split(",").map((entry) => entry.trim()).filter(Boolean)) {
|
|
9971
|
+
if (!target.includes(item)) target.push(item);
|
|
9972
|
+
}
|
|
9973
|
+
};
|
|
9974
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
9975
|
+
const token = String(args[index]);
|
|
9976
|
+
if (passthrough) { prompt.push(token); continue; }
|
|
9977
|
+
if (token === "--") { passthrough = true; continue; }
|
|
9978
|
+
const take = () => index + 1 < args.length ? String(args[++index]) : "";
|
|
9979
|
+
if (token === "--experience-base-release") experience.baseAgentReleaseId = take();
|
|
9980
|
+
else if (token.startsWith("--experience-base-release=")) experience.baseAgentReleaseId = token.slice(26);
|
|
9981
|
+
else if (token === "--experience-pack-release") addList(experience.experiencePackReleaseIds, take());
|
|
9982
|
+
else if (token.startsWith("--experience-pack-release=")) addList(experience.experiencePackReleaseIds, token.slice(26));
|
|
9983
|
+
else if (token === "--experience-agent-definition") experience.agentDefinitionId = take();
|
|
9984
|
+
else if (token.startsWith("--experience-agent-definition=")) experience.agentDefinitionId = token.slice(30);
|
|
9985
|
+
else if (token === "--experience-task-signature") addList(experience.taskSignatures, take());
|
|
9986
|
+
else if (token.startsWith("--experience-task-signature=")) addList(experience.taskSignatures, token.slice(28));
|
|
9987
|
+
else if (token === "--experience-task-class") addList(experience.declaredTaskClasses, take());
|
|
9988
|
+
else if (token.startsWith("--experience-task-class=")) addList(experience.declaredTaskClasses, token.slice(24));
|
|
9989
|
+
else if (token === "--experience-environment") addList(experience.environmentTags, take());
|
|
9990
|
+
else if (token.startsWith("--experience-environment=")) addList(experience.environmentTags, token.slice(25));
|
|
9991
|
+
else if (token === "--experience-desktop-loadout") experience.desktopLoadout = true;
|
|
9992
|
+
else if (
|
|
9993
|
+
token === "--experience-loadout" || token === "--experience-loadout-file" ||
|
|
9994
|
+
token.startsWith("--experience-loadout=") || token.startsWith("--experience-loadout-file=")
|
|
9995
|
+
) {
|
|
9996
|
+
throw new Error("Custom Experience loadout paths are no longer supported; use --experience-desktop-loadout.");
|
|
9997
|
+
}
|
|
9998
|
+
else if (token === "--no-experience") experience.disabled = true;
|
|
9999
|
+
else prompt.push(token);
|
|
10000
|
+
}
|
|
10001
|
+
return { prompt: prompt.join(" "), experience };
|
|
10002
|
+
}
|
|
10003
|
+
|
|
10004
|
+
function resolveRuntimeExperienceCli(agent, prompt, requested, cwd, overrides = {}) {
|
|
10005
|
+
const prepared = desktopOntologyLoadout.prepareDesktopLoadoutRequest({
|
|
10006
|
+
db: overrides.db,
|
|
10007
|
+
agent,
|
|
10008
|
+
userDataDir: overrides.userDataDir || userDataDir(),
|
|
10009
|
+
requested: requested || {},
|
|
10010
|
+
now: overrides.now,
|
|
10011
|
+
});
|
|
10012
|
+
if (prepared.mode === "skip") {
|
|
10013
|
+
return { disabled: true, observableReason: prepared.reason, resolution: "skipped" };
|
|
10014
|
+
}
|
|
10015
|
+
const resolved = terminalExperienceExchange.resolveRuntimeExperienceForAgent({
|
|
10016
|
+
userDataDir: overrides.userDataDir || userDataDir(),
|
|
10017
|
+
cwd,
|
|
10018
|
+
prompt,
|
|
10019
|
+
requested: prepared.requested || requested || {},
|
|
10020
|
+
agent,
|
|
10021
|
+
agentRoot: agent ? (overrides.agentRoot || agentFolder(agent)) : null,
|
|
10022
|
+
...(overrides.platform ? { platform: overrides.platform } : {}),
|
|
10023
|
+
...(overrides.arch ? { arch: overrides.arch } : {}),
|
|
10024
|
+
...(overrides.runtime ? { runtime: overrides.runtime } : {}),
|
|
10025
|
+
});
|
|
10026
|
+
if (prepared.mode !== "resolved") return resolved;
|
|
10027
|
+
const authority = prepared.authority;
|
|
10028
|
+
const tasteRuntime = {
|
|
10029
|
+
tasteRuntimeOverlay: authority.tasteRuntimeOverlay || null,
|
|
10030
|
+
loadoutAuthority: "desktop-terminal-exact-loadout",
|
|
10031
|
+
projectionRevision: authority.projectionRevision,
|
|
10032
|
+
loadoutRevision: authority.loadoutRevision,
|
|
10033
|
+
};
|
|
10034
|
+
if (!authority.experiencePackReleaseId) {
|
|
10035
|
+
return {
|
|
10036
|
+
disabled: true,
|
|
10037
|
+
resolution: "desktop-loadout-taste-only",
|
|
10038
|
+
...tasteRuntime,
|
|
10039
|
+
};
|
|
10040
|
+
}
|
|
10041
|
+
if (resolved.disabled === true) return { ...resolved, ...tasteRuntime };
|
|
10042
|
+
if (
|
|
10043
|
+
resolved.agentDefinitionId !== authority.agentDefinitionId ||
|
|
10044
|
+
resolved.baseAgentReleaseId !== authority.baseAgentReleaseId ||
|
|
10045
|
+
!Array.isArray(resolved.experiencePackReleaseIds) ||
|
|
10046
|
+
resolved.experiencePackReleaseIds.length !== 1 ||
|
|
10047
|
+
resolved.experiencePackReleaseIds[0] !== authority.experiencePackReleaseId
|
|
10048
|
+
) {
|
|
10049
|
+
return {
|
|
10050
|
+
disabled: true,
|
|
10051
|
+
observableReason: "desktop-loadout-runtime-resolution-mismatch",
|
|
10052
|
+
resolution: "skipped",
|
|
10053
|
+
...tasteRuntime,
|
|
10054
|
+
};
|
|
10055
|
+
}
|
|
10056
|
+
return {
|
|
10057
|
+
...resolved,
|
|
10058
|
+
...tasteRuntime,
|
|
10059
|
+
};
|
|
10060
|
+
}
|
|
10061
|
+
|
|
10062
|
+
async function cmdRun(db, query, prompt, runtimeOverride, runtimeExperience = null) {
|
|
8568
10063
|
const agent = resolveAgent(db, query);
|
|
8569
10064
|
if (!agent) {
|
|
8570
10065
|
const routedPrompt = [query, prompt].filter(Boolean).join(" ").trim() || (await readStdin());
|
|
8571
|
-
if (!routedPrompt || !routedPrompt.trim()) fail("
|
|
8572
|
-
return cmdAutoRun(db, routedPrompt.trim(), runtimeOverride);
|
|
10066
|
+
if (!routedPrompt || !routedPrompt.trim()) fail("Prompt is empty. Use agentlas run <agent> \"...\" or agentlas run \"...\".");
|
|
10067
|
+
return cmdAutoRun(db, routedPrompt.trim(), runtimeOverride, runtimeExperience);
|
|
8573
10068
|
}
|
|
8574
10069
|
let userPrompt = prompt;
|
|
8575
10070
|
if (!userPrompt) userPrompt = await readStdin();
|
|
8576
|
-
if (!userPrompt || !userPrompt.trim()) fail("
|
|
10071
|
+
if (!userPrompt || !userPrompt.trim()) fail("Prompt is empty. Pass agentlas run <agent> \"...\" or provide it through stdin.");
|
|
8577
10072
|
process.stderr.write(`▸ ${agent.name}\n`);
|
|
8578
|
-
const
|
|
10073
|
+
const projectPath = ensureTerminalProjectForExecutionCli(db, projectCwd(), PERMISSION, "terminal-run");
|
|
10074
|
+
const cwd = projectPath || projectCwd();
|
|
10075
|
+
const resolvedExperience = resolveRuntimeExperienceCli(agent, userPrompt.trim(), runtimeExperience, cwd, { db });
|
|
10076
|
+
const code = await executeOnce(db, agentSystemPromptCli(agent), userPrompt.trim(), runtimeOverride, {
|
|
10077
|
+
projectPath, agentId: agent.id, permission: PERMISSION, runtimeExperience: resolvedExperience,
|
|
10078
|
+
});
|
|
8579
10079
|
process.exit(code);
|
|
8580
10080
|
}
|
|
8581
10081
|
|
|
8582
|
-
async function cmdAutoRun(db, prompt, runtimeOverride) {
|
|
10082
|
+
async function cmdAutoRun(db, prompt, runtimeOverride, runtimeExperience = null) {
|
|
8583
10083
|
const lang = prefsLang();
|
|
8584
10084
|
const choice = autoRouteAgent(db, prompt, lang);
|
|
8585
|
-
if (!choice) fail("
|
|
10085
|
+
if (!choice) fail("No agent is available for automatic routing. Check installation with agentlas list.");
|
|
8586
10086
|
if (choice.direct) {
|
|
8587
10087
|
// 전문 에이전트 확신 없음 → 페르소나/능력 라우팅 없이 현재 런타임으로 직답.
|
|
8588
10088
|
process.stderr.write(`▸ direct (no agent)\n`);
|
|
8589
10089
|
process.stderr.write(` ${autoRouteNote(choice, lang)}\n`);
|
|
8590
10090
|
const sys = `${autoRoutePreamble(choice, lang)}\n\n${directSystemPrompt(lang)}`;
|
|
10091
|
+
const projectPath = ensureTerminalProjectForExecutionCli(db, projectCwd(), PERMISSION, "terminal-auto-run");
|
|
10092
|
+
const cwd = projectPath || projectCwd();
|
|
10093
|
+
const resolvedExperience = resolveRuntimeExperienceCli(null, prompt.trim(), runtimeExperience, cwd, { db });
|
|
8591
10094
|
const code = await executeOnce(db, sys, prompt.trim(), runtimeOverride, {
|
|
8592
|
-
projectPath
|
|
10095
|
+
projectPath,
|
|
8593
10096
|
agentId: null,
|
|
8594
10097
|
permission: PERMISSION,
|
|
10098
|
+
runtimeExperience: resolvedExperience,
|
|
8595
10099
|
});
|
|
8596
10100
|
process.exit(code);
|
|
8597
10101
|
}
|
|
8598
10102
|
process.stderr.write(`▸ ${choice.agent.name} (auto)\n`);
|
|
8599
10103
|
process.stderr.write(` ${autoRouteNote(choice, lang)}\n`);
|
|
8600
10104
|
const sys = `${autoRoutePreamble(choice, lang)}\n\n${agentSystemPromptCli(choice.agent)}`;
|
|
10105
|
+
const projectPath = ensureTerminalProjectForExecutionCli(db, projectCwd(), PERMISSION, "terminal-auto-run");
|
|
10106
|
+
const cwd = projectPath || projectCwd();
|
|
10107
|
+
const resolvedExperience = resolveRuntimeExperienceCli(choice.agent, prompt.trim(), runtimeExperience, cwd, { db });
|
|
8601
10108
|
const code = await executeOnce(db, sys, prompt.trim(), runtimeOverride, {
|
|
8602
|
-
projectPath
|
|
10109
|
+
projectPath,
|
|
8603
10110
|
agentId: choice.agent.id,
|
|
8604
10111
|
permission: PERMISSION,
|
|
10112
|
+
runtimeExperience: resolvedExperience,
|
|
8605
10113
|
});
|
|
8606
10114
|
process.exit(code);
|
|
8607
10115
|
}
|
|
@@ -8609,7 +10117,7 @@ async function cmdAutoRun(db, prompt, runtimeOverride) {
|
|
|
8609
10117
|
// chat / open / 에이전트명 단독 → 네이티브 CLI 대화형 세션 (claude처럼 바로 접속)
|
|
8610
10118
|
function cmdOpen(db, query, runtimeOverride) {
|
|
8611
10119
|
const agent = resolveAgent(db, query);
|
|
8612
|
-
if (!agent) fail(
|
|
10120
|
+
if (!agent) fail(`Agent not found: ${query}`);
|
|
8613
10121
|
launchInteractive(db, agent, runtimeOverride);
|
|
8614
10122
|
}
|
|
8615
10123
|
|
|
@@ -8644,15 +10152,37 @@ function firmSystemPrompt(db, firm) {
|
|
|
8644
10152
|
/* ignore */
|
|
8645
10153
|
}
|
|
8646
10154
|
const base = (ceo && ceo.system_prompt) || `You are the CEO of ${firm.name}.`;
|
|
8647
|
-
return `${base}\n\n[FIRM]
|
|
10155
|
+
return `${base}\n\n[FIRM] You are the CEO of '${firm.name}'. Delegate user requests to the appropriate departments.\nOrganization:\n${roster}`;
|
|
8648
10156
|
}
|
|
8649
10157
|
async function cmdFirm(db, query, prompt, runtimeOverride) {
|
|
8650
10158
|
const firm = resolveFirm(db, query);
|
|
8651
|
-
if (!firm) fail(
|
|
10159
|
+
if (!firm) fail(`Company not found: ${query}`);
|
|
8652
10160
|
const sys = firmSystemPrompt(db, firm);
|
|
8653
10161
|
if (prompt && prompt.trim()) {
|
|
8654
10162
|
process.stderr.write(`▸ ${firm.name} CEO\n`);
|
|
8655
|
-
const
|
|
10163
|
+
const runtime = resolveRuntime(db, runtimeOverride);
|
|
10164
|
+
const projectPath = ensureTerminalProjectForExecutionCli(db, projectCwd(), PERMISSION, "terminal-firm-run");
|
|
10165
|
+
const allocated = await allocateSingleWorkloadCli(db, prompt.trim(), {
|
|
10166
|
+
runtime,
|
|
10167
|
+
cwd: projectCwd(),
|
|
10168
|
+
projectPath,
|
|
10169
|
+
agentId: firm.ceo_agent_id,
|
|
10170
|
+
lang: prefsLang(),
|
|
10171
|
+
mode: "team",
|
|
10172
|
+
onWarning: (message) => process.stderr.write(`▸ ${message}\n`),
|
|
10173
|
+
});
|
|
10174
|
+
process.stderr.write(
|
|
10175
|
+
`▸ team model route · ${allocated.resolution.source} · ${allocated.resolution.model || runtime.kind || runtime.backend}` +
|
|
10176
|
+
`${allocated.resolution.effort ? ` · ${allocated.resolution.effort}` : ""}` +
|
|
10177
|
+
`${allocated.resolution.fallbackReason ? ` · ${allocated.resolution.fallbackReason}` : ""}\n`,
|
|
10178
|
+
);
|
|
10179
|
+
const code = await executeOnce(db, sys, prompt.trim(), runtimeOverride, {
|
|
10180
|
+
projectPath,
|
|
10181
|
+
agentId: firm.ceo_agent_id,
|
|
10182
|
+
permission: PERMISSION,
|
|
10183
|
+
model: allocated.resolution.model,
|
|
10184
|
+
effort: allocated.resolution.effort,
|
|
10185
|
+
});
|
|
8656
10186
|
process.exit(code);
|
|
8657
10187
|
}
|
|
8658
10188
|
// 대화형 — agentlas TUI. CEO 페르소나를 system으로, 작업은 현재 폴더에서.
|
|
@@ -8662,7 +10192,7 @@ async function cmdFirm(db, query, prompt, runtimeOverride) {
|
|
|
8662
10192
|
slug: firm.slug,
|
|
8663
10193
|
label: firm.name + " CEO",
|
|
8664
10194
|
system: sys,
|
|
8665
|
-
capAgent: { name: firm.name, name_en: firm.name_en || firm.name, tagline: firm.tagline, system_prompt: sys },
|
|
10195
|
+
capAgent: { name: firm.name, name_en: firm.name_en || firm.name, tagline: firm.tagline, tagline_en: firm.tagline_en, entity_kind: "team", system_prompt: sys },
|
|
8666
10196
|
};
|
|
8667
10197
|
return launchTui(db, subject, runtimeOverride);
|
|
8668
10198
|
}
|
|
@@ -8844,23 +10374,23 @@ async function cmdEnv(db) {
|
|
|
8844
10374
|
...readDotEnvFileCli(path.join(os.homedir(), ".agentlas", "credentials.env")),
|
|
8845
10375
|
};
|
|
8846
10376
|
const keys = Object.keys(fromFiles).sort();
|
|
8847
|
-
out(
|
|
10377
|
+
out(`Shared env keys: ${keys.length} (values hidden; from credentials.env):`);
|
|
8848
10378
|
for (const k of keys) out(` ${k}`);
|
|
8849
10379
|
out("");
|
|
8850
|
-
out("
|
|
10380
|
+
out("Keychain entries are available in Desktop settings → Credentials.");
|
|
8851
10381
|
return;
|
|
8852
10382
|
}
|
|
8853
10383
|
const keytar = readKeytar();
|
|
8854
|
-
if (!keytar) fail("keytar
|
|
10384
|
+
if (!keytar) fail("The keytar module is unavailable (run through the app runtime).");
|
|
8855
10385
|
let creds;
|
|
8856
10386
|
try {
|
|
8857
10387
|
creds = await keytar.findCredentials(SERVICE);
|
|
8858
10388
|
} catch (e) {
|
|
8859
|
-
fail("
|
|
10389
|
+
fail("Failed to read environment settings: " + ((e && e.message) || e));
|
|
8860
10390
|
return;
|
|
8861
10391
|
}
|
|
8862
10392
|
const keys = creds.map((c) => c.account).filter((a) => a.startsWith(ENV_PREFIX)).map((a) => a.slice(ENV_PREFIX.length));
|
|
8863
|
-
out(
|
|
10393
|
+
out(`Shared env keys: ${keys.length} (values hidden):`);
|
|
8864
10394
|
for (const k of keys.sort()) out(` ${k}`);
|
|
8865
10395
|
}
|
|
8866
10396
|
|
|
@@ -8889,7 +10419,7 @@ function setMultimodalCli(db, modality, providerId) {
|
|
|
8889
10419
|
const mm = loadMultimodalCatalog();
|
|
8890
10420
|
if (!["image", "video", "audio"].includes(modality)) fail("usage: agentlas multimodal set <image|video|audio> <provider-id>");
|
|
8891
10421
|
const provider = mm.MULTIMODAL_PROVIDERS.find((p) => p.id === providerId && p.modality === modality);
|
|
8892
|
-
if (!provider) fail(`
|
|
10422
|
+
if (!provider) fail(`Provider not found: ${providerId} (${modality})`);
|
|
8893
10423
|
const key = modality === "image" ? "imageProvider" : modality === "video" ? "videoProvider" : "audioProvider";
|
|
8894
10424
|
return saveMultimodalSettingsCli(db, { [key]: providerId });
|
|
8895
10425
|
}
|
|
@@ -8952,7 +10482,7 @@ function parseUpdateFlags(args) {
|
|
|
8952
10482
|
else if (arg === "--no-launch") flags.launch = false;
|
|
8953
10483
|
else if (arg === "--url") flags.url = args[++i] || flags.url;
|
|
8954
10484
|
else if (arg === "--help" || arg === "-h" || arg === "help") flags.help = true;
|
|
8955
|
-
else fail(
|
|
10485
|
+
else fail(`Unknown update option: ${arg}`);
|
|
8956
10486
|
}
|
|
8957
10487
|
return flags;
|
|
8958
10488
|
}
|
|
@@ -9027,19 +10557,19 @@ function updateTimeoutError(kind, ms) {
|
|
|
9027
10557
|
return updateTransferError(`AGENTLAS_UPDATE_${kind.toUpperCase()}_TIMEOUT`, message);
|
|
9028
10558
|
}
|
|
9029
10559
|
|
|
9030
|
-
function parseSafeUpdateUrl(value, label = "
|
|
10560
|
+
function parseSafeUpdateUrl(value, label = "update URL") {
|
|
9031
10561
|
let parsed;
|
|
9032
10562
|
try {
|
|
9033
10563
|
parsed = new URL(String(value || ""));
|
|
9034
10564
|
} catch (error) {
|
|
9035
|
-
throw updateTransferError("AGENTLAS_UPDATE_INVALID_URL", `${label}
|
|
10565
|
+
throw updateTransferError("AGENTLAS_UPDATE_INVALID_URL", `${label} has an invalid format.`, error);
|
|
9036
10566
|
}
|
|
9037
10567
|
const loopback = parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1" || parsed.hostname === "[::1]" || parsed.hostname === "::1";
|
|
9038
10568
|
if (parsed.protocol !== "https:" && !(parsed.protocol === "http:" && loopback)) {
|
|
9039
|
-
throw updateTransferError("AGENTLAS_UPDATE_INSECURE_URL", `${label}
|
|
10569
|
+
throw updateTransferError("AGENTLAS_UPDATE_INSECURE_URL", `${label} must use HTTPS (except local loopback).`);
|
|
9040
10570
|
}
|
|
9041
10571
|
if (parsed.username || parsed.password) {
|
|
9042
|
-
throw updateTransferError("AGENTLAS_UPDATE_INVALID_URL", `${label}
|
|
10572
|
+
throw updateTransferError("AGENTLAS_UPDATE_INVALID_URL", `${label} cannot contain user information.`);
|
|
9043
10573
|
}
|
|
9044
10574
|
return parsed.toString();
|
|
9045
10575
|
}
|
|
@@ -9186,7 +10716,7 @@ async function fetchDesktopRelease(url) {
|
|
|
9186
10716
|
return await fetchUpdateMetadata(url, { signal: controller.signal });
|
|
9187
10717
|
} catch (error) {
|
|
9188
10718
|
const message = String((error && error.message) || error);
|
|
9189
|
-
fail(
|
|
10719
|
+
fail(`Update check failed: ${message}`);
|
|
9190
10720
|
}
|
|
9191
10721
|
}
|
|
9192
10722
|
|
|
@@ -9225,19 +10755,19 @@ async function cmdUpdateStandalone(flags) {
|
|
|
9225
10755
|
if (flags.json) {
|
|
9226
10756
|
return out(JSON.stringify({ currentVersion, latestVersion, updateAvailable: comparison == null ? null : comparison < 0, channel: "npm" }, null, 2));
|
|
9227
10757
|
}
|
|
9228
|
-
out(
|
|
10758
|
+
out(`Current version: ${currentVersion}`);
|
|
9229
10759
|
if (!latestVersion) {
|
|
9230
|
-
out("
|
|
9231
|
-
out("
|
|
10760
|
+
out("Could not check the latest version on the npm registry (offline or not published yet).");
|
|
10761
|
+
out("Manual update: npm i -g agentlas@latest");
|
|
9232
10762
|
return;
|
|
9233
10763
|
}
|
|
9234
|
-
out(
|
|
10764
|
+
out(`Latest version: ${latestVersion}`);
|
|
9235
10765
|
if (comparison == null) {
|
|
9236
|
-
out("
|
|
10766
|
+
out("Could not compare version formats. Manual update: npm i -g agentlas@latest");
|
|
9237
10767
|
} else if (comparison < 0) {
|
|
9238
|
-
out("
|
|
10768
|
+
out("Update: npm i -g agentlas@latest");
|
|
9239
10769
|
} else {
|
|
9240
|
-
out("
|
|
10770
|
+
out("Already on the latest version.");
|
|
9241
10771
|
}
|
|
9242
10772
|
}
|
|
9243
10773
|
|
|
@@ -9252,7 +10782,7 @@ async function cmdUpdate(args) {
|
|
|
9252
10782
|
const latestVersion = String(release.version || "");
|
|
9253
10783
|
const artifact = findCurrentArtifact(release);
|
|
9254
10784
|
const comparison = compareSemVer(currentVersion, latestVersion);
|
|
9255
|
-
if (comparison == null) fail(
|
|
10785
|
+
if (comparison == null) fail(`Current/latest version is not valid SemVer: current=${currentVersion} latest=${latestVersion}`);
|
|
9256
10786
|
const updateAvailable = comparison < 0;
|
|
9257
10787
|
const status = {
|
|
9258
10788
|
currentVersion,
|
|
@@ -9270,15 +10800,15 @@ async function cmdUpdate(args) {
|
|
|
9270
10800
|
if (flags.json) return out(JSON.stringify(status, null, 2));
|
|
9271
10801
|
out(formatUpdateSummary(status));
|
|
9272
10802
|
if (flags.check) return;
|
|
9273
|
-
if (release.ready !== true) fail("
|
|
9274
|
-
if (!updateAvailable && !flags.force) return out("
|
|
9275
|
-
if (process.platform !== "darwin") return out("
|
|
9276
|
-
if (!artifact || !artifact.url) fail("
|
|
10803
|
+
if (release.ready !== true) fail("The latest release is not ready for public installation.");
|
|
10804
|
+
if (!updateAvailable && !flags.force) return out("Already on the latest version.");
|
|
10805
|
+
if (process.platform !== "darwin") return out("Automatic installation is not supported on this OS yet. Use the release/download link above.");
|
|
10806
|
+
if (!artifact || !artifact.url) fail("Could not find a DMG for this Mac.");
|
|
9277
10807
|
await installMacDesktopUpdate(release, artifact, flags);
|
|
9278
10808
|
}
|
|
9279
10809
|
|
|
9280
10810
|
function requirePath(commandPath, label) {
|
|
9281
|
-
if (!fs.existsSync(commandPath)) fail(
|
|
10811
|
+
if (!fs.existsSync(commandPath)) fail(`Required update tool not found: ${label}`);
|
|
9282
10812
|
return commandPath;
|
|
9283
10813
|
}
|
|
9284
10814
|
|
|
@@ -9614,9 +11144,9 @@ async function installMacDesktopUpdate(release, artifact, flags) {
|
|
|
9614
11144
|
const stagingPath = path.join(targetDir, `.${targetName}.installing.${transactionId}.app`);
|
|
9615
11145
|
|
|
9616
11146
|
try {
|
|
9617
|
-
out(
|
|
11147
|
+
out(`Download: ${fileName}`);
|
|
9618
11148
|
await downloadUpdateFile(validatedArtifact.url, dmgPath, validatedArtifact);
|
|
9619
|
-
out("
|
|
11149
|
+
out("Verify: DMG, notarization, Gatekeeper");
|
|
9620
11150
|
await runCommand(hdiutil, ["verify", dmgPath]);
|
|
9621
11151
|
await runCommand(xcrun, ["stapler", "validate", dmgPath]);
|
|
9622
11152
|
await runCommand(spctl, ["-a", "-t", "open", "--context", "context:primary-signature", "-vv", dmgPath]);
|
|
@@ -9625,16 +11155,16 @@ async function installMacDesktopUpdate(release, artifact, flags) {
|
|
|
9625
11155
|
mountPoint = parseHdiutilMountPoint(mount.stdout);
|
|
9626
11156
|
const sourceApp = mountPoint ? path.join(mountPoint, "Agentlas.app") : "";
|
|
9627
11157
|
if (!sourceApp || !fs.existsSync(sourceApp)) {
|
|
9628
|
-
throw updateTransferError("AGENTLAS_UPDATE_APP_MISSING", "
|
|
11158
|
+
throw updateTransferError("AGENTLAS_UPDATE_APP_MISSING", "Agentlas.app was not found in the DMG.");
|
|
9629
11159
|
}
|
|
9630
11160
|
|
|
9631
11161
|
const installedVersion = await runCommand(plistBuddy, ["-c", "Print :CFBundleShortVersionString", path.join(sourceApp, "Contents", "Info.plist")], { capture: true });
|
|
9632
11162
|
const appVersion = installedVersion.stdout.trim();
|
|
9633
11163
|
if (appVersion !== String(release.version)) {
|
|
9634
|
-
throw updateTransferError("AGENTLAS_UPDATE_VERSION_MISMATCH",
|
|
11164
|
+
throw updateTransferError("AGENTLAS_UPDATE_VERSION_MISMATCH", `App version does not match the release: release=${release.version} app=${appVersion}`);
|
|
9635
11165
|
}
|
|
9636
11166
|
|
|
9637
|
-
out("
|
|
11167
|
+
out("Install: quit the existing Agentlas app and replace it");
|
|
9638
11168
|
await runCommand(osascript, ["-e", 'tell application "Agentlas" to quit'], { capture: true, allowFailure: true });
|
|
9639
11169
|
await sleep(2_000);
|
|
9640
11170
|
const replacement = await replaceMacAppBundle({
|
|
@@ -9645,10 +11175,10 @@ async function installMacDesktopUpdate(release, artifact, flags) {
|
|
|
9645
11175
|
runCommand,
|
|
9646
11176
|
commands: { codesign, spctl, ditto, mv, rm },
|
|
9647
11177
|
});
|
|
9648
|
-
if (replacement.backupRetained) out(
|
|
11178
|
+
if (replacement.backupRetained) out(`Warning: the verified app was installed, but the previous app backup could not be removed: ${replacement.backupPath}`);
|
|
9649
11179
|
if (fs.existsSync(lsregister)) await runCommand(lsregister, ["-f", targetApp], { allowFailure: true });
|
|
9650
11180
|
if (flags.launch) await runCommand(open, ["-a", "Agentlas"], { allowFailure: true });
|
|
9651
|
-
out(`Agentlas ${release.version}
|
|
11181
|
+
out(`Agentlas ${release.version} installed.`);
|
|
9652
11182
|
} finally {
|
|
9653
11183
|
if (mountPoint) await runCommand(hdiutil, ["detach", mountPoint], { allowFailure: true });
|
|
9654
11184
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
@@ -9740,32 +11270,32 @@ function oberonScaffold(args) {
|
|
|
9740
11270
|
};
|
|
9741
11271
|
if (flags.titles) manifest.titles = oberonSampleTitles(title);
|
|
9742
11272
|
fs.writeFileSync(outPath, JSON.stringify(manifest, null, 2), "utf8");
|
|
9743
|
-
out(`✓
|
|
9744
|
-
out(` ·
|
|
9745
|
-
out(` ·
|
|
9746
|
-
out(` ·
|
|
9747
|
-
if (!flags.titles) out(` ·
|
|
11273
|
+
out(`✓ Manifest created: ${outPath}`);
|
|
11274
|
+
out(` · ${shotCount} shots · ${aspect} · ${manifest.provider}`);
|
|
11275
|
+
out(` · fill prompts, then run: agentlas oberon render ${path.basename(outPath)}`);
|
|
11276
|
+
out(` · or use an agent: agentlas run oberon-film-studio "30-second fragrance ad trailer"`);
|
|
11277
|
+
if (!flags.titles) out(` · use --titles to include title/subtitle burn-in samples`);
|
|
9748
11278
|
}
|
|
9749
11279
|
|
|
9750
11280
|
function oberonRender(args) {
|
|
9751
11281
|
const { flags, rest } = oberonParseFlags(args);
|
|
9752
|
-
if (!rest[0]) fail("
|
|
11282
|
+
if (!rest[0]) fail("A manifest path is required: agentlas oberon render <manifest.json>");
|
|
9753
11283
|
const manifestPath = path.resolve(rest[0]);
|
|
9754
|
-
if (!fs.existsSync(manifestPath)) fail(
|
|
11284
|
+
if (!fs.existsSync(manifestPath)) fail(`Manifest not found: ${manifestPath}`);
|
|
9755
11285
|
|
|
9756
11286
|
let manifest;
|
|
9757
11287
|
try {
|
|
9758
11288
|
manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
9759
11289
|
} catch (e) {
|
|
9760
|
-
return fail(
|
|
11290
|
+
return fail(`Failed to parse manifest JSON: ${e.message}`);
|
|
9761
11291
|
}
|
|
9762
|
-
if (!Array.isArray(manifest.shots) || !manifest.shots.length) fail("
|
|
11292
|
+
if (!Array.isArray(manifest.shots) || !manifest.shots.length) fail("The manifest has no shots[].");
|
|
9763
11293
|
|
|
9764
11294
|
const root = oberonRepoRoot();
|
|
9765
11295
|
const script = path.join(root, "scripts", "render-oberon-live-request.cjs");
|
|
9766
11296
|
const builtRender = path.join(root, "dist", "electron", "oberon", "render.js");
|
|
9767
|
-
if (!fs.existsSync(script)) fail(
|
|
9768
|
-
if (!fs.existsSync(builtRender)) fail(`Electron
|
|
11297
|
+
if (!fs.existsSync(script)) fail(`Headless render script not found (not included in the packaged app): ${script}`);
|
|
11298
|
+
if (!fs.existsSync(builtRender)) fail(`An Electron build is required. Run npm run build:electron first (missing: ${builtRender})`);
|
|
9769
11299
|
|
|
9770
11300
|
// --max-shots 등 오버라이드가 있으면 사용자 매니페스트는 그대로 두고 임시 패치본을 만든다.
|
|
9771
11301
|
let reqPath = manifestPath;
|
|
@@ -9790,18 +11320,18 @@ function oberonRender(args) {
|
|
|
9790
11320
|
if (flags["poll-ms"]) childEnv.OBERON_LIVE_POLL_MS = String(flags["poll-ms"]);
|
|
9791
11321
|
if (flags.open) childEnv.OBERON_LIVE_OPEN_DELIVERY = "1";
|
|
9792
11322
|
|
|
9793
|
-
out(`▶ Oberon
|
|
9794
|
-
out(`
|
|
9795
|
-
out(`
|
|
9796
|
-
if (manifest.titles) out(`
|
|
11323
|
+
out(`▶ Oberon render: "${manifest.title}" (${manifest.shots.length} shots, max ${overrides.maxShots ?? manifest.maxShots ?? 3})`);
|
|
11324
|
+
out(` Manifest: ${manifestPath}`);
|
|
11325
|
+
out(` Delivery folder: ${deliveryDir}`);
|
|
11326
|
+
if (manifest.titles) out(` title/subtitle burn-in: enabled → generating additional *_titled.mp4`);
|
|
9797
11327
|
|
|
9798
11328
|
if (flags["dry-run"]) {
|
|
9799
|
-
out("\n[dry-run]
|
|
11329
|
+
out("\n[dry-run] Command to run:");
|
|
9800
11330
|
out(` ${process.execPath} ${script}`);
|
|
9801
11331
|
out(" env: OBERON_LIVE_VEO=1");
|
|
9802
11332
|
out(` OBERON_LIVE_REQUEST_FILE=${reqPath}`);
|
|
9803
11333
|
out(` OBERON_LIVE_DELIVERY_DIR=${deliveryDir}`);
|
|
9804
|
-
|
|
11334
|
+
out(" (full Electron · GEMINI_API_KEY/GOOGLE_CLOUD_PROJECT vault required)");
|
|
9805
11335
|
return;
|
|
9806
11336
|
}
|
|
9807
11337
|
|
|
@@ -9822,11 +11352,11 @@ function oberonRender(args) {
|
|
|
9822
11352
|
child.stderr.on("data", (c) => process.stderr.write(c));
|
|
9823
11353
|
child.on("close", (code) => {
|
|
9824
11354
|
if (code === 0) {
|
|
9825
|
-
out(`\n✓
|
|
11355
|
+
out(`\n✓ Render complete — delivery folder: ${deliveryDir}`);
|
|
9826
11356
|
const titled = files.filter((f) => f.kind && f.kind.startsWith("titled"));
|
|
9827
|
-
if (titled.length) out(`
|
|
11357
|
+
if (titled.length) out(` title/subtitle burn-in files: ${titled.map((f) => f.name).join(", ")}`);
|
|
9828
11358
|
} else {
|
|
9829
|
-
process.stderr.write(`\n✖
|
|
11359
|
+
process.stderr.write(`\n✖ Render failed (exit ${code})\n`);
|
|
9830
11360
|
process.exitCode = code || 1;
|
|
9831
11361
|
}
|
|
9832
11362
|
resolve();
|
|
@@ -9884,7 +11414,7 @@ function slugifyOberon(value) {
|
|
|
9884
11414
|
function oberonList() {
|
|
9885
11415
|
const dir = path.join(userDataDir(), "oberon");
|
|
9886
11416
|
if (!fs.existsSync(dir)) {
|
|
9887
|
-
out("
|
|
11417
|
+
out("No render outputs yet. Start with agentlas oberon scaffold my.json.");
|
|
9888
11418
|
return;
|
|
9889
11419
|
}
|
|
9890
11420
|
const entries = fs
|
|
@@ -9908,40 +11438,40 @@ function oberonList() {
|
|
|
9908
11438
|
.sort((a, b) => b.mtime - a.mtime)
|
|
9909
11439
|
.slice(0, 15);
|
|
9910
11440
|
if (!entries.length) {
|
|
9911
|
-
out("
|
|
11441
|
+
out("No render outputs yet.");
|
|
9912
11442
|
return;
|
|
9913
11443
|
}
|
|
9914
|
-
out(
|
|
11444
|
+
out(`Recent Oberon renders (${dir}):\n`);
|
|
9915
11445
|
for (const e of entries) {
|
|
9916
11446
|
const masters = e.files.filter((f) => /master|titled/.test(f) && /\.(mp4|mov)$/.test(f));
|
|
9917
11447
|
const when = e.mtime ? new Date(e.mtime).toISOString().slice(0, 16).replace("T", " ") : "";
|
|
9918
11448
|
out(` ${when} ${e.name}`);
|
|
9919
11449
|
if (masters.length) out(` ${masters.join(", ")}`);
|
|
9920
11450
|
}
|
|
9921
|
-
out(`\
|
|
11451
|
+
out(`\nOpen the folder with: agentlas oberon open`);
|
|
9922
11452
|
}
|
|
9923
11453
|
|
|
9924
11454
|
function oberonOpen(args) {
|
|
9925
11455
|
const target = args[0] ? path.resolve(args[0]) : path.join(userDataDir(), "oberon");
|
|
9926
|
-
if (!fs.existsSync(target)) fail(
|
|
11456
|
+
if (!fs.existsSync(target)) fail(`Path not found: ${target}`);
|
|
9927
11457
|
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "explorer" : "xdg-open";
|
|
9928
11458
|
spawn(opener, [target], { detached: true, stdio: "ignore" }).unref();
|
|
9929
|
-
out(
|
|
11459
|
+
out(`Opening folder: ${target}`);
|
|
9930
11460
|
}
|
|
9931
11461
|
|
|
9932
11462
|
function oberonHelp() {
|
|
9933
11463
|
out(
|
|
9934
11464
|
[
|
|
9935
|
-
"agentlas oberon —
|
|
11465
|
+
"agentlas oberon — AI film rendering from the terminal",
|
|
9936
11466
|
"",
|
|
9937
11467
|
" oberon scaffold [out.json] [--title T] [--aspect 16:9] [--shots N] [--titles]",
|
|
9938
|
-
"
|
|
11468
|
+
" create an editable render manifest (--titles: include title/subtitle burn-in samples)",
|
|
9939
11469
|
" oberon render <manifest.json> [--delivery DIR] [--max-shots N] [--open] [--dry-run]",
|
|
9940
|
-
" full Electron
|
|
11470
|
+
" spawn full Electron render + stream progress (GEMINI_API_KEY vault required)",
|
|
9941
11471
|
" oberon list 최근 렌더 산출물",
|
|
9942
11472
|
" oberon open [path] 산출물 폴더 열기",
|
|
9943
11473
|
"",
|
|
9944
|
-
"
|
|
11474
|
+
"Fill prompts directly, or ask an agent: agentlas run oberon-film-studio \"30-second fragrance ad\"",
|
|
9945
11475
|
].join("\n"),
|
|
9946
11476
|
);
|
|
9947
11477
|
}
|
|
@@ -9965,7 +11495,7 @@ async function cmdOberon(args) {
|
|
|
9965
11495
|
case "-h":
|
|
9966
11496
|
return oberonHelp();
|
|
9967
11497
|
default:
|
|
9968
|
-
fail(
|
|
11498
|
+
fail(`Unknown oberon subcommand: ${sub} (scaffold|render|list|open|help)`);
|
|
9969
11499
|
}
|
|
9970
11500
|
}
|
|
9971
11501
|
|
|
@@ -9983,23 +11513,31 @@ function cmdHelp() {
|
|
|
9983
11513
|
hdr("TALK & RUN"),
|
|
9984
11514
|
" <agent> jump into a chat with one agent (e.g. agentlas seo)",
|
|
9985
11515
|
" run [agent] [prompt] one-shot — omit agent to auto-route (reads stdin if no prompt)",
|
|
11516
|
+
" --experience-desktop-loadout use Desktop's fresh exact Operational/Taste loadout receipt",
|
|
11517
|
+
" --no-experience highest precedence; do not read or inject a loadout",
|
|
9986
11518
|
" firm <firm> [cmd] delegate to a company's CEO (interactive if no cmd)",
|
|
9987
11519
|
" chats [n] recent conversations · chat resume in REPL: /resume",
|
|
9988
11520
|
"",
|
|
9989
11521
|
hdr("AGENTS & HUB (Agentlas OS surface)"),
|
|
9990
11522
|
" search \"<what you need>\" discover agents in the Hub + local (hep-search)",
|
|
9991
11523
|
" install <slug> install an agent from the Hub (hep-cloud)",
|
|
11524
|
+
" plugin add <slug> install a Hub plugin (MCP servers) into this machine",
|
|
9992
11525
|
" build \"<request>\" build/repair/package an agent or team (hep-build)",
|
|
9993
11526
|
" upload <path> save owner-private in Agent Cloud (default) (hep-upload)",
|
|
9994
11527
|
" --visibility marketplace explicit compatibility flag: publish to Hub",
|
|
9995
11528
|
" connect [<sub>] wire Telegram / platforms to an agent team (hep-connect)",
|
|
9996
11529
|
" import <path> import a local agent/team folder",
|
|
9997
11530
|
" list installed agents/companies + active runtime",
|
|
11531
|
+
" experience <sub> portable Experience: list|inspect|validate|save|publish|status|export|unpublish",
|
|
11532
|
+
" legacy local intents require explicit legacy-* commands",
|
|
11533
|
+
" variant resolve local variant selection: selected|fallback|base-only|error",
|
|
9998
11534
|
"",
|
|
9999
11535
|
hdr("EXECUTE"),
|
|
10000
|
-
" storm <goal>
|
|
11536
|
+
" storm <goal> Agentlas Goal+UltraCode harness: plan → allocate → execute → verify [--research]",
|
|
10001
11537
|
" swarm <goal> emergent agent swarm — parallel workers + synthesizer [--parallel N]",
|
|
10002
|
-
" network <request>
|
|
11538
|
+
" network <request> host-LLM workforce ontology → exact TF → execute [--benchmark]",
|
|
11539
|
+
" workforce <request> same Agent Workforce Ontology route (explicit name)",
|
|
11540
|
+
" legacy-network <request> compatibility-only Hephaestus network route",
|
|
10003
11541
|
" call \"a,b\" \"<ctx>\" invoke named Hub/Cloud agents (hep-call)",
|
|
10004
11542
|
" browser [<sub>] real browser execution hardpoint (hep-browser)",
|
|
10005
11543
|
" route \"<request>\" routing preview — which agent/pipeline would take this",
|
|
@@ -10062,7 +11600,7 @@ async function main() {
|
|
|
10062
11600
|
runtimeOverride = argv[++i];
|
|
10063
11601
|
} else if (argv[i] === "--permission" || argv[i] === "-P") {
|
|
10064
11602
|
const p = (argv[++i] || "").toLowerCase();
|
|
10065
|
-
if (!["read", "write", "full"].includes(p)) fail(
|
|
11603
|
+
if (!["read", "write", "full"].includes(p)) fail(`Unknown permission: ${p} (read|write|full)`);
|
|
10066
11604
|
PERMISSION = p;
|
|
10067
11605
|
PERMISSION_EXPLICIT = true;
|
|
10068
11606
|
} else {
|
|
@@ -10097,8 +11635,10 @@ async function main() {
|
|
|
10097
11635
|
return cmdImport(db, rest[1]);
|
|
10098
11636
|
case "cd":
|
|
10099
11637
|
return cmdCd(db, rest[1]);
|
|
10100
|
-
case "run":
|
|
10101
|
-
|
|
11638
|
+
case "run": {
|
|
11639
|
+
const runInput = parseRunExperienceArgs(rest.slice(2));
|
|
11640
|
+
return cmdRun(db, rest[1], runInput.prompt, runtimeOverride, runInput.experience);
|
|
11641
|
+
}
|
|
10102
11642
|
case "chat":
|
|
10103
11643
|
case "open":
|
|
10104
11644
|
return cmdOpen(db, rest[1], runtimeOverride);
|
|
@@ -10121,10 +11661,16 @@ async function main() {
|
|
|
10121
11661
|
return cmdCloud(db, rest.slice(1), runtimeOverride);
|
|
10122
11662
|
case "creds":
|
|
10123
11663
|
return cmdCreds(db, rest.slice(1));
|
|
10124
|
-
case "storm":
|
|
10125
|
-
|
|
10126
|
-
|
|
10127
|
-
return parity().
|
|
11664
|
+
case "storm": {
|
|
11665
|
+
const cwd = projectCwd();
|
|
11666
|
+
const projectPath = ensureTerminalProjectForExecutionCli(db, cwd, PERMISSION, "terminal-storm");
|
|
11667
|
+
return parity().cmdStorm(db, rest.slice(1), runtimeOverride, { cwd, projectPath, permission: PERMISSION });
|
|
11668
|
+
}
|
|
11669
|
+
case "swarm": {
|
|
11670
|
+
const cwd = projectCwd();
|
|
11671
|
+
const projectPath = ensureTerminalProjectForExecutionCli(db, cwd, PERMISSION, "terminal-swarm");
|
|
11672
|
+
return parity().cmdSwarm(db, rest.slice(1), runtimeOverride, { cwd, projectPath, permission: PERMISSION });
|
|
11673
|
+
}
|
|
10128
11674
|
case "automation":
|
|
10129
11675
|
case "automations":
|
|
10130
11676
|
return parity().cmdAutomation(db, rest.slice(1), runtimeOverride);
|
|
@@ -10133,16 +11679,54 @@ async function main() {
|
|
|
10133
11679
|
return parity().cmdHep(db, rest.slice(1));
|
|
10134
11680
|
// ── Agentlas OS 정식 표면 (hep-*) 1급 노출 ──
|
|
10135
11681
|
case "build":
|
|
10136
|
-
//
|
|
10137
|
-
|
|
11682
|
+
// Terminal-owned preflight: trusted system-global MCP metadata first, one consent,
|
|
11683
|
+
// then pass only approved catalog IDs/value-free shortages to the existing builder.
|
|
11684
|
+
ensureTerminalProjectForExecutionCli(db, projectCwd(), PERMISSION, "terminal-build");
|
|
11685
|
+
return terminalAssets.cmdBuild({
|
|
11686
|
+
db,
|
|
11687
|
+
args: rest.slice(1),
|
|
11688
|
+
userDataDir: userDataDir(),
|
|
11689
|
+
cwd: projectCwd(),
|
|
11690
|
+
input: process.stdin,
|
|
11691
|
+
promptOutput: process.stderr,
|
|
11692
|
+
out,
|
|
11693
|
+
probeMcpServer: (server, probeOptions) => probeApprovedTerminalMcp(db, server, runtimeOverride, projectCwd(), probeOptions),
|
|
11694
|
+
invokeBuild: (request, metadata) => runTerminalBuilder(db, request, metadata, runtimeOverride, projectCwd()),
|
|
11695
|
+
});
|
|
11696
|
+
case "experience":
|
|
11697
|
+
return terminalExperienceExchange.cmdExperienceExchange({
|
|
11698
|
+
args: rest.slice(1),
|
|
11699
|
+
userDataDir: userDataDir(),
|
|
11700
|
+
cwd: projectCwd(),
|
|
11701
|
+
out,
|
|
11702
|
+
env: process.env,
|
|
11703
|
+
getSessionCookie: cloudSessionCookieCli,
|
|
11704
|
+
fetchHub: (url, init) => fetchHubCli(url, init),
|
|
11705
|
+
legacyCommand: (legacyOptions) => terminalAssets.cmdExperience(legacyOptions),
|
|
11706
|
+
});
|
|
11707
|
+
case "variant":
|
|
11708
|
+
return terminalAssets.cmdVariant({
|
|
11709
|
+
db,
|
|
11710
|
+
args: rest.slice(1),
|
|
11711
|
+
userDataDir: userDataDir(),
|
|
11712
|
+
cwd: projectCwd(),
|
|
11713
|
+
out,
|
|
11714
|
+
});
|
|
10138
11715
|
case "search": // hep-search — 에이전트 디렉터리 발견 (Hub + 로컬)
|
|
10139
|
-
if (!rest[1]) return fail('usage: agentlas search "
|
|
11716
|
+
if (!rest[1]) return fail('usage: agentlas search "<what you need>" [--limit 10]');
|
|
10140
11717
|
return parity().cloudSearch(db, rest.slice(1));
|
|
10141
11718
|
case "install": // public Hub package install — slug로 에이전트 설치
|
|
10142
|
-
if (!rest[1]) return fail('usage: agentlas install <slug> (
|
|
11719
|
+
if (!rest[1]) return fail('usage: agentlas install <slug> (run agentlas search "what you need" first)');
|
|
10143
11720
|
return cmdCloudInstall(db, rest[1]);
|
|
11721
|
+
case "plugin": // Hub 플러그인(MCP 서버 번들) — 에이전트 설치(install)와 다른 카탈로그다
|
|
11722
|
+
case "plugins": {
|
|
11723
|
+
const action = rest[1];
|
|
11724
|
+
if (action === "add") return cmdPluginAdd(db, rest[2]);
|
|
11725
|
+
if (action === "list" || !action) return cmdPluginList();
|
|
11726
|
+
return fail("usage: agentlas plugin add <slug> | agentlas plugin list");
|
|
11727
|
+
}
|
|
10144
11728
|
case "upload": { // 기본은 owner-private Agent Cloud, public Hub는 명시 flag로만.
|
|
10145
|
-
if (!rest[1]) return fail("usage: agentlas upload
|
|
11729
|
+
if (!rest[1]) return fail("usage: agentlas upload <agent-folder-path> [--visibility marketplace]");
|
|
10146
11730
|
const uploadArgs = rest.slice(1);
|
|
10147
11731
|
return cmdCloud(db, [cloudActionForTopLevelUpload(uploadArgs), ...uploadArgs], runtimeOverride);
|
|
10148
11732
|
}
|
|
@@ -10152,8 +11736,14 @@ async function main() {
|
|
|
10152
11736
|
return parity().cmdHep(db, ["hep-browser", ...rest.slice(1)]);
|
|
10153
11737
|
case "call": // hep-call — 지정 에이전트 호출/준비
|
|
10154
11738
|
return parity().cmdHep(db, ["hep-call", ...rest.slice(1)]);
|
|
10155
|
-
case "
|
|
10156
|
-
case "
|
|
11739
|
+
case "workforce":
|
|
11740
|
+
case "network":
|
|
11741
|
+
case "taskforce": {
|
|
11742
|
+
const cwd = projectCwd();
|
|
11743
|
+
const projectPath = ensureTerminalProjectForExecutionCli(db, cwd, PERMISSION, "terminal-workforce");
|
|
11744
|
+
return workforce().cmdWorkforce(db, rest.slice(1), runtimeOverride, { cwd, projectPath, permission: PERMISSION });
|
|
11745
|
+
}
|
|
11746
|
+
case "legacy-network": // explicit compatibility escape hatch only
|
|
10157
11747
|
return parity().cmdHep(db, ["hep-network", ...rest.slice(1)]);
|
|
10158
11748
|
case "route": // 라우팅 미리보기 (실행 없음)
|
|
10159
11749
|
return parity().cmdHep(
|
|
@@ -10202,7 +11792,7 @@ async function main() {
|
|
|
10202
11792
|
if (firm) return cmdFirm(db, cmd, "", runtimeOverride);
|
|
10203
11793
|
const prompt = rest.join(" ").trim();
|
|
10204
11794
|
if (prompt) return cmdAutoRun(db, prompt, runtimeOverride);
|
|
10205
|
-
fail(
|
|
11795
|
+
fail(`Agent/company not found: ${cmd} (check with agentlas list)`);
|
|
10206
11796
|
}
|
|
10207
11797
|
}
|
|
10208
11798
|
}
|
|
@@ -10219,6 +11809,9 @@ module.exports = {
|
|
|
10219
11809
|
parseDotEnvCli,
|
|
10220
11810
|
isProtectedChildEnvKeyCli,
|
|
10221
11811
|
mergeChildEnvValuesCli,
|
|
11812
|
+
openNodeSqliteDb,
|
|
11813
|
+
ensureMemoryContextColumn,
|
|
11814
|
+
writeJsonPrivateAtomicCli,
|
|
10222
11815
|
resolveCredentialSourcePath,
|
|
10223
11816
|
upsertEnvLine,
|
|
10224
11817
|
fetchHubCli,
|
|
@@ -10233,6 +11826,7 @@ module.exports = {
|
|
|
10233
11826
|
replaceMacAppBundle,
|
|
10234
11827
|
captureRuntime,
|
|
10235
11828
|
buildArgs,
|
|
11829
|
+
codexCaptureAgentText,
|
|
10236
11830
|
captureOutputLimit,
|
|
10237
11831
|
materializeCloudListingCli,
|
|
10238
11832
|
recoverCloudInstallJournalCli,
|
|
@@ -10252,6 +11846,28 @@ module.exports = {
|
|
|
10252
11846
|
cloudPackageHashVersion,
|
|
10253
11847
|
cloudPortablePathConflict,
|
|
10254
11848
|
cloudPortableExecutableForFile,
|
|
11849
|
+
parseRunExperienceArgs,
|
|
11850
|
+
resolveRuntimeExperienceCli,
|
|
11851
|
+
runTerminalBuilder,
|
|
11852
|
+
resolveRuntime,
|
|
11853
|
+
listAvailableRuntimes,
|
|
11854
|
+
probeApprovedTerminalMcp,
|
|
11855
|
+
finalizeExperienceExecutionCli,
|
|
11856
|
+
buildChildEnvCli,
|
|
11857
|
+
augmentSystem,
|
|
11858
|
+
beginMemoryTurnCli,
|
|
11859
|
+
completeMemoryTurnCli,
|
|
11860
|
+
curatorRuntimeEnvCli,
|
|
11861
|
+
ensureGeminiNoToolsPolicyCli,
|
|
11862
|
+
curateCliReply,
|
|
11863
|
+
TERMINAL_MEMORY_CORE,
|
|
11864
|
+
TERMINAL_MEMORY_CORE_MAX_TOKENS,
|
|
11865
|
+
approximatePromptTokens,
|
|
11866
|
+
memoryEmitterPromptFor,
|
|
11867
|
+
credentialIndexReminderFor,
|
|
11868
|
+
ensureCoreProjectCli,
|
|
11869
|
+
ensureTerminalProjectForExecutionCli,
|
|
11870
|
+
ensureAgentlasProjectStateIgnoreCli,
|
|
10255
11871
|
DEFAULT_API_MODEL,
|
|
10256
11872
|
ANTHROPIC_COMPAT_API,
|
|
10257
11873
|
// 자동 라우팅 회귀 테스트 표면 — 약한 매치 직답/오라우팅 방지 규칙 검증용.
|