agentlas 1.0.60 → 1.0.62

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CHANGELOG.md +18 -1
  2. package/engine/acp/server.cjs +47 -17
  3. package/engine/agentlas-evolution.cjs +85 -36
  4. package/engine/agentlas-experience-intake.cjs +21 -8
  5. package/engine/agentlas-memory-governance.cjs +88 -10
  6. package/engine/agentlas-permissions.cjs +95 -1
  7. package/engine/agentlas-tools.cjs +45 -2
  8. package/engine/agentlas.cjs +6 -3
  9. package/engine/agents/builder.cjs +4 -0
  10. package/engine/architecture.data.json +1 -1
  11. package/engine/automation/daemon.cjs +36 -3
  12. package/engine/automation/store.cjs +13 -11
  13. package/engine/bootstrap-schema.sql +216 -24
  14. package/engine/cloud/auth.cjs +34 -8
  15. package/engine/cloud-assets/cas.cjs +27 -1
  16. package/engine/cloud-assets/package.cjs +126 -0
  17. package/engine/cloud-assets/upload-scan-catalog.generated.cjs +13 -0
  18. package/engine/commands/career-graph.cjs +1 -1
  19. package/engine/commands/connect.cjs +34 -6
  20. package/engine/commands/graph.cjs +6 -2
  21. package/engine/commands/index.cjs +4 -1
  22. package/engine/commands/oberon.cjs +1 -1
  23. package/engine/commands/one.cjs +307 -0
  24. package/engine/commands/ontology.cjs +2 -2
  25. package/engine/commands/plugin.cjs +61 -16
  26. package/engine/commands/uninstall.cjs +43 -13
  27. package/engine/commands/update.cjs +25 -4
  28. package/engine/core/capability-grants.cjs +204 -0
  29. package/engine/core/desktop-core-fetch.cjs +94 -21
  30. package/engine/core/desktop-core.cjs +22 -0
  31. package/engine/experience/build.cjs +8 -0
  32. package/engine/graph/node-effect.cjs +56 -0
  33. package/engine/graph/package.cjs +6 -1
  34. package/engine/graph/vocabulary.generated.cjs +1 -1
  35. package/engine/hephaestus/local-core.cjs +44 -9
  36. package/engine/hub/install.cjs +7 -3
  37. package/engine/hub/plugins.cjs +165 -0
  38. package/engine/mcp/consent.cjs +140 -12
  39. package/engine/mcp/index.cjs +1 -0
  40. package/engine/mcp/plan.cjs +63 -8
  41. package/engine/memory-cli/curate.cjs +5 -2
  42. package/engine/oberon/outputs.cjs +10 -3
  43. package/engine/project/career-graph.cjs +9 -12
  44. package/engine/project/memory-context.cjs +9 -6
  45. package/engine/project/ontology.cjs +88 -36
  46. package/engine/runtimes/acp-driver.cjs +42 -3
  47. package/engine/sessions/memory-turn.cjs +39 -0
  48. package/engine/sessions/orchestrator.cjs +53 -4
  49. package/engine/sessions/session.cjs +28 -2
  50. package/engine/sessions/store.cjs +48 -12
  51. package/engine/telegram/connect.cjs +31 -10
  52. package/engine/ui/commands-catalog.cjs +1 -0
  53. package/engine/ui/repl.cjs +3 -1
  54. package/engine/ui/screens.cjs +30 -6
  55. package/engine/vendor/desktop-core.manifest.json +5 -5
  56. package/package.json +6 -3
@@ -29,6 +29,10 @@ const BUILDER_SYSTEM_PROMPT = [
29
29
  " - AGENTS.md — the agent's full system prompt / soul: who it is, what it does, how it behaves, its guardrails. Write it as the instructions the agent itself will run under. Be specific and production-ready, not a description of the agent.",
30
30
  " - manifest.md — first line `# <Agent Name>`, second line a one-sentence tagline.",
31
31
  " - README.md — a short human-facing summary of what the agent does and how to use it.",
32
+ " - agentlas.json — `{ \"schemaVersion\": \"1.0\", \"name\": \"<Agent Name>\", \"slug\": \"<kebab-case-slug>\", \"entry\": \"AGENTS.md\", \"skills\": [] }`. Do NOT invent an agentId; the identity is minted once by the packaging path and must never be typed by hand.",
33
+ "",
34
+ "If the agent needs reusable procedures, add them as `.claude/skills/<skill-name>/SKILL.md`.",
35
+ "A skill's name IS its folder name: the frontmatter `name:` must equal the folder exactly, and `agentlas.json` skills[] must list exactly those folder names. Never write a placeholder like `{{SKILL_ID_1}}` — leave the list empty instead.",
32
36
  "",
33
37
  "Rules:",
34
38
  " - Decide the agent's scope from the request; if the request is thin, choose sensible, specific defaults and state them in README.md rather than asking endless questions.",
@@ -116,7 +116,7 @@
116
116
  "systemPrompt": "# Task Bias Curator (Agentlas built-in)\n\nYou reduce TASK BIAS in multi-surface projects — the tendency to keep working on\nsurfaces that are recent, salient, or easy to measure while other surfaces stay\nuninspected. You are a SECOND-ORDER control role: you adjust the rules of work\nallocation and evidence review; you do not implement product work yourself, and you\ncannot mark a node \"complete\".\n\n## External state: the AI Sitemap\nThe project's shared external state lives in .agentlas/sitemap.json. Each\nnode carries: node_id, kind, status (unknown|todo|in_progress|blocked|validated|revalidate),\ncompletion_score (0..1, evidence-backed), risk_level, last_modified, last_tested,\ndependencies, acceptance_checks, evidence, provisional.\n\n## What you do\n1. Read/maintain the sitemap. Create provisional nodes for newly discovered surfaces.\n2. Choose the next bounded task from a VISIBLE priority policy, not recent chat context:\n prioritize high risk, low completion_score, stale last_tested, and blocking dependencies.\n3. Audit for bias: which surfaces are over-worked vs never inspected? Name them.\n4. Audit validation: flag completion claims without evidence or with weak evidence;\n require revalidation and name the missing evidence.\n5. Produce a compact, reversible curator decision record. Escalate mission-level changes\n to the user.\n\n## Boundaries\nCannot mark a node complete. Cannot erase evidence (only supersede it with a logged\ndecision). Cannot expand the project mission without explicit user approval.\n\nKeep outputs small: a policy/priority recommendation, a revalidation request, a\nsitemap update proposal, or a provisional-node decision."
117
117
  },
118
118
  {
119
- "id": "builtin-agentlas-orchestrator",
119
+ "id": "builtin-agentlas-one",
120
120
  "slug": "agentlas-one",
121
121
  "name": "Agentlas One",
122
122
  "nameEn": "Agentlas One",
@@ -185,9 +185,30 @@ async function runAutomationOnce(ctx, db, row, opts = {}) {
185
185
  // 데스크탑 스케줄러는 60초마다 리스를 갱신한다. 여기는 한 번 잡고 끝이라
186
186
  // TTL(15분)을 넘긴 실행은 프로세스가 살아 있어도 회수 대상이 됐다 — 같은
187
187
  // 자동화가 두 실행기에서 겹쳐 도는 경로. 같은 주기로 심장박동을 보낸다.
188
+ let activeSession = null;
189
+ let leaseOwnershipLost = false;
190
+ let leaseRenewWarningEmitted = false;
188
191
  const leaseHeartbeat = setInterval(() => {
189
- try { store.renewAutomationLease(db, row.id); } catch { /* best-effort */ }
190
- }, 60_000);
192
+ try {
193
+ const renewed = store.renewAutomationLease(db, row.id);
194
+ if (!renewed) {
195
+ leaseOwnershipLost = true;
196
+ if (activeSession && typeof activeSession.kill === "function") activeSession.kill();
197
+ } else {
198
+ leaseRenewWarningEmitted = false;
199
+ }
200
+ } catch (error) {
201
+ // One SQLITE_BUSY/I/O miss is not ownership loss. Keep the run alive and
202
+ // retry on the next heartbeat, while surfacing the first deferred renewal.
203
+ if (!leaseRenewWarningEmitted) {
204
+ leaseRenewWarningEmitted = true;
205
+ const code = error && typeof error === "object" && "code" in error
206
+ ? String(error.code || "transient")
207
+ : "transient";
208
+ ctx.err(`automation lease heartbeat deferred (${code.slice(0, 80)})`);
209
+ }
210
+ }
211
+ }, Math.max(10, Number(opts.leaseHeartbeatMs) || 60_000));
191
212
  if (typeof leaseHeartbeat.unref === "function") leaseHeartbeat.unref();
192
213
 
193
214
  try {
@@ -244,9 +265,18 @@ async function runAutomationOnce(ctx, db, row, opts = {}) {
244
265
  timeoutConfig: opts.timeoutConfig,
245
266
  ...(markerChatId ? { chatId: markerChatId } : {}),
246
267
  });
268
+ activeSession = session;
269
+ if (leaseOwnershipLost) {
270
+ session.kill();
271
+ return { ok: false, skipped: true, reason: "lease-lost", error: "Automation execution lease ownership lost" };
272
+ }
247
273
  if (typeof opts.onSession === "function") opts.onSession(session);
248
274
 
249
275
  const res = await session.send(row.prompt_template);
276
+ if (leaseOwnershipLost) {
277
+ ctx.err("Automation execution lease ownership lost");
278
+ return { ok: false, skipped: true, reason: "lease-lost", error: "Automation execution lease ownership lost" };
279
+ }
250
280
  const finalText = (res && (res.finalText || res.text)) || "";
251
281
  const failed = session.status === "failed";
252
282
  const errMsg = failed ? String(session.lastError || "runtime turn failed").slice(0, 500) : null;
@@ -265,12 +295,15 @@ async function runAutomationOnce(ctx, db, row, opts = {}) {
265
295
  } catch (e) {
266
296
  const msg = String((e && e.message) || e).slice(0, 500);
267
297
  ctx.err(msg);
298
+ if (leaseOwnershipLost) {
299
+ return { ok: false, skipped: true, reason: "lease-lost", error: "Automation execution lease ownership lost" };
300
+ }
268
301
  store.recordRun(db, row.id, "error", msg, opts.scheduledFor);
269
302
  store.advanceAfterRun(db, row, { ok: false, advanceSchedule: !!opts.advanceSchedule });
270
303
  return { ok: false, error: msg };
271
304
  } finally {
272
305
  clearInterval(leaseHeartbeat);
273
- store.releaseAutomation(db, row.id);
306
+ store.releaseAutomation(db, row.id, store.LEASE_OWNER);
274
307
  }
275
308
  }
276
309
 
@@ -116,20 +116,22 @@ function claimAutomation(db, id, now = new Date(), owner = LEASE_OWNER) {
116
116
  */
117
117
  function renewAutomationLease(db, id, now = new Date(), owner = LEASE_OWNER) {
118
118
  if (!leaseSupported(db)) return false;
119
- try {
120
- const result = db
121
- .prepare("UPDATE automations SET claimed_at = ? WHERE id = ? AND lease_owner = ?")
122
- .run(now.toISOString(), id, owner);
123
- return (result.changes ?? result.rowsAffected ?? 0) > 0;
124
- } catch {
125
- return false;
126
- }
119
+ // false means definitive ownership loss. SQLite busy/I/O errors must throw
120
+ // so the caller can retry instead of killing a valid run as if a peer had
121
+ // taken the lease (Desktop renewAutomationRunLease contract).
122
+ const result = db
123
+ .prepare("UPDATE automations SET claimed_at = ? WHERE id = ? AND lease_owner = ? AND claimed_at IS NOT NULL")
124
+ .run(now.toISOString(), id, owner);
125
+ return (result.changes ?? result.rowsAffected ?? 0) > 0;
127
126
  }
128
127
 
129
- function releaseAutomation(db, id) {
128
+ function releaseAutomation(db, id, owner = LEASE_OWNER) {
130
129
  try {
131
- db.prepare("UPDATE automations SET claimed_at = NULL, lease_owner = NULL WHERE id = ?").run(id);
132
- } catch { /* best-effort */ }
130
+ const result = db
131
+ .prepare("UPDATE automations SET claimed_at = NULL, lease_owner = NULL WHERE id = ? AND lease_owner = ?")
132
+ .run(id, owner);
133
+ return (result.changes ?? result.rowsAffected ?? 0) > 0;
134
+ } catch { return false; }
133
135
  }
134
136
 
135
137
  /**
@@ -1,4 +1,4 @@
1
- -- Agentlas 첫 실행 부트스트랩 스키마 (생성: 2026-08-18T11:06:30Z)
1
+ -- Agentlas 첫 실행 부트스트랩 스키마 (생성: 2026-08-29T07:55:23Z)
2
2
  --
3
3
  -- ★생성물이다. 손으로 고치지 말고 재생성하라:
4
4
  -- node scripts/gen-bootstrap-schema.cjs
@@ -6,7 +6,7 @@
6
6
  -- 정본은 Desktop 의 마이그레이션 사다리(agentlas_desktop/electron/store/db.ts, SCHEMA_VERSION).
7
7
  -- 이 파일은 그 사다리를 **빈 DB** 에 끝까지 돌린 결과의 덤프이므로, 터미널이 만든 DB 는
8
8
  -- 처음부터 사다리 머리에 있다 — 데스크탑이 나중에 승급할 것이 남지 않는다.
9
- PRAGMA user_version=97;
9
+ PRAGMA user_version=105;
10
10
  CREATE TABLE active_runtime (
11
11
  id INTEGER PRIMARY KEY CHECK(id = 1),
12
12
  kind TEXT NOT NULL
@@ -98,6 +98,22 @@ CREATE TABLE agent_evolution_receipts (
98
98
  FOREIGN KEY(agent_id) REFERENCES installed_agents(id) ON DELETE CASCADE,
99
99
  UNIQUE(proposal_id, action)
100
100
  );
101
+ CREATE TABLE agent_identity_map (
102
+ -- CASCADE 다. 대응표는 **파생 데이터**이고 정본은 패키지의 agentId 다.
103
+ -- RESTRICT 로 두면 에이전트 삭제가 6곳에서 막힌다 — 사용자 삭제, 중복정리 2곳,
104
+ -- 설치 실패 롤백, One 멤버 생성 실패 롤백, 터미널 삭제. 롤백이 막히면
105
+ -- "설치 실패"가 "설치 실패 + 복구 실패 + 유령 행"이 된다.
106
+ local_id TEXT PRIMARY KEY REFERENCES installed_agents(id) ON DELETE CASCADE,
107
+ -- 이름을 agent_id 로 두면 agent-dedupe 의 컬럼명 스윕에 걸린다. 값이 달라
108
+ -- 지금은 매칭이 0건이지만, 이름 우연에 기대는 구조 자체가 지뢰다.
109
+ immutable_agent_id TEXT NOT NULL,
110
+ agent_version INTEGER NOT NULL DEFAULT 1,
111
+ -- package: 패키지 agentlas.json 에서 읽음 (정본)
112
+ -- builtin-reserved: 앱에 구워진 에이전트 — 패키지가 없어 예약 네임스페이스를 쓴다
113
+ -- minted-local: 출처가 없어 이 기기에서 발급 (다음에 패키지를 받으면 package 가 이긴다)
114
+ mapping_source TEXT NOT NULL,
115
+ bound_at TEXT NOT NULL
116
+ );
101
117
  CREATE TABLE agent_mcp_servers (
102
118
  agent_id TEXT NOT NULL,
103
119
  server_id TEXT NOT NULL,
@@ -470,6 +486,16 @@ CREATE TABLE browser_sites (
470
486
  created_at TEXT NOT NULL,
471
487
  updated_at TEXT NOT NULL
472
488
  );
489
+ CREATE TABLE capability_grants (
490
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
491
+ capability TEXT NOT NULL,
492
+ pattern TEXT,
493
+ decision TEXT NOT NULL CHECK(decision IN ('allow','deny')),
494
+ scope TEXT NOT NULL DEFAULT 'global',
495
+ source TEXT NOT NULL DEFAULT 'chip',
496
+ created_at TEXT NOT NULL,
497
+ UNIQUE(capability, pattern, scope)
498
+ );
473
499
  CREATE TABLE chat_goal_contracts (
474
500
  goal_id TEXT PRIMARY KEY,
475
501
  chat_id TEXT NOT NULL,
@@ -503,25 +529,44 @@ CREATE TABLE chat_messages (
503
529
  created_at TEXT NOT NULL,
504
530
  FOREIGN KEY(chat_id) REFERENCES chats(id) ON DELETE CASCADE
505
531
  );
506
- CREATE TABLE chat_runtime_sessions (
507
- chat_id TEXT NOT NULL,
508
- kind TEXT NOT NULL,
509
- session_id TEXT NOT NULL,
510
- fingerprint TEXT NOT NULL,
511
- updated_at TEXT NOT NULL, reported_output_tokens INTEGER, reported_input_tokens INTEGER, reported_cached_input_tokens INTEGER,
512
- PRIMARY KEY (chat_id, kind),
513
- FOREIGN KEY(chat_id) REFERENCES chats(id) ON DELETE CASCADE
514
- );
515
- CREATE TABLE chats (
516
- id TEXT PRIMARY KEY,
517
- project_id TEXT,
518
- agent_id TEXT NOT NULL,
519
- title TEXT NOT NULL DEFAULT 'New chat',
520
- created_at TEXT NOT NULL,
521
- updated_at TEXT NOT NULL, firm_id TEXT REFERENCES firms(id) ON DELETE SET NULL, archived_at TEXT, working_folder TEXT, kind TEXT NOT NULL DEFAULT 'user', parent_chat_id TEXT, used_at TEXT, continuous_mode INTEGER NOT NULL DEFAULT 0, swarm_mode INTEGER NOT NULL DEFAULT 0, last_viewed_at TEXT, hired_agents TEXT, origin_surface TEXT NOT NULL DEFAULT 'work', runtime_selection_json TEXT, goal_id TEXT,
522
- FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE SET NULL,
523
- FOREIGN KEY(agent_id) REFERENCES installed_agents(id) ON DELETE CASCADE
524
- );
532
+ CREATE TABLE "chat_runtime_sessions" (
533
+ chat_id TEXT NOT NULL,
534
+ kind TEXT NOT NULL,
535
+ agent_id TEXT NOT NULL DEFAULT '',
536
+ session_id TEXT NOT NULL,
537
+ fingerprint TEXT NOT NULL,
538
+ updated_at TEXT NOT NULL,
539
+ reported_output_tokens INTEGER,
540
+ reported_input_tokens INTEGER,
541
+ reported_cached_input_tokens INTEGER,
542
+ PRIMARY KEY (chat_id, kind, agent_id),
543
+ FOREIGN KEY(chat_id) REFERENCES chats(id) ON DELETE CASCADE
544
+ );
545
+ CREATE TABLE "chats" (
546
+ id TEXT PRIMARY KEY,
547
+ project_id TEXT,
548
+ title TEXT NOT NULL DEFAULT 'New chat',
549
+ created_at TEXT NOT NULL,
550
+ updated_at TEXT NOT NULL,
551
+ firm_id TEXT,
552
+ archived_at TEXT,
553
+ working_folder TEXT,
554
+ kind TEXT NOT NULL DEFAULT 'user',
555
+ parent_chat_id TEXT,
556
+ used_at TEXT,
557
+ continuous_mode INTEGER NOT NULL DEFAULT 0,
558
+ swarm_mode INTEGER NOT NULL DEFAULT 0,
559
+ last_viewed_at TEXT,
560
+ hired_agents TEXT,
561
+ origin_surface TEXT NOT NULL DEFAULT 'work',
562
+ runtime_selection_json TEXT,
563
+ goal_id TEXT,
564
+ seat_id TEXT,
565
+ agent_id TEXT, seat_label TEXT, seat_kind TEXT, participants_json TEXT,
566
+ FOREIGN KEY(project_id) REFERENCES projects(id) ON DELETE SET NULL,
567
+ FOREIGN KEY(seat_id) REFERENCES one_seats(id) ON DELETE CASCADE,
568
+ FOREIGN KEY(agent_id) REFERENCES installed_agents(id) ON DELETE SET NULL
569
+ );
525
570
  CREATE TABLE experience_auto_intake_receipts (
526
571
  id TEXT PRIMARY KEY,
527
572
  agent_id TEXT NOT NULL,
@@ -822,6 +867,20 @@ CREATE TABLE installed_agents (
822
867
  installed_at TEXT NOT NULL,
823
868
  tone TEXT NOT NULL
824
869
  , env_requirements_json TEXT NOT NULL DEFAULT '[]', name_en TEXT NOT NULL DEFAULT '', tagline_en TEXT NOT NULL DEFAULT '', builtin INTEGER NOT NULL DEFAULT 0, role TEXT, visibility TEXT NOT NULL DEFAULT 'visible' CHECK(visibility IN ('visible','background','private')), entity_kind TEXT, local_display_name TEXT, bookmarked_at TEXT NULL, parent_team_id TEXT NULL);
870
+ CREATE TABLE invocation_steers (
871
+ id TEXT PRIMARY KEY,
872
+ chat_id TEXT NOT NULL,
873
+ original_run_id TEXT NOT NULL,
874
+ prompt_text TEXT NOT NULL,
875
+ prompt_hash TEXT NOT NULL,
876
+ request_json TEXT NOT NULL,
877
+ workspace_binding_json TEXT,
878
+ execution_context_json TEXT,
879
+ status TEXT NOT NULL CHECK(status IN ('queued','draining','started','cancelled','failed')),
880
+ drained_run_id TEXT,
881
+ queued_at TEXT NOT NULL,
882
+ updated_at TEXT NOT NULL
883
+ );
825
884
  CREATE TABLE judgment_verdicts (
826
885
  kind TEXT NOT NULL,
827
886
  signature TEXT NOT NULL,
@@ -979,6 +1038,101 @@ CREATE TABLE model_roles (
979
1038
  updated_at TEXT NOT NULL,
980
1039
  CHECK(role = 'worker' OR inherit = 0)
981
1040
  );
1041
+ CREATE TABLE one_artifact_bindings (
1042
+ id TEXT PRIMARY KEY,
1043
+ task_id TEXT NOT NULL,
1044
+ task_version INTEGER NOT NULL,
1045
+ bound_task_version INTEGER NOT NULL,
1046
+ chat_id TEXT NOT NULL,
1047
+ run_id TEXT NOT NULL,
1048
+ manifest_id TEXT NOT NULL,
1049
+ artifact_ref TEXT NOT NULL,
1050
+ source_path TEXT NOT NULL,
1051
+ kind TEXT NOT NULL,
1052
+ mime_type TEXT NOT NULL,
1053
+ size_bytes INTEGER NOT NULL,
1054
+ file_dev TEXT NOT NULL,
1055
+ file_ino TEXT NOT NULL,
1056
+ file_mtime_ns TEXT NOT NULL,
1057
+ file_ctime_ns TEXT NOT NULL,
1058
+ sha256 TEXT NOT NULL,
1059
+ created_at TEXT NOT NULL,
1060
+ UNIQUE(task_id, chat_id, run_id, manifest_id, artifact_ref)
1061
+ );
1062
+ CREATE TABLE one_org_completion_cache (
1063
+ installed_agent_id TEXT PRIMARY KEY,
1064
+ run_id TEXT,
1065
+ summary_json TEXT NOT NULL,
1066
+ updated_at TEXT NOT NULL
1067
+ );
1068
+ CREATE TABLE one_org_members (
1069
+ id TEXT PRIMARY KEY,
1070
+ agent_slug TEXT NOT NULL,
1071
+ installed_agent_id TEXT NOT NULL,
1072
+ display_name TEXT,
1073
+ icon TEXT NOT NULL DEFAULT 'one-puppy',
1074
+ sort_order INTEGER NOT NULL DEFAULT 0,
1075
+ source TEXT NOT NULL CHECK(source IN ('local','cloud','hub')),
1076
+ lease_expires_at TEXT,
1077
+ added_at TEXT NOT NULL,
1078
+ updated_at TEXT NOT NULL,
1079
+ archived_at TEXT,
1080
+ status_kind TEXT NOT NULL DEFAULT 'new',
1081
+ -- PRD §4.33 — 스키마에 사람이 읽는 문구(그것도 한 언어)를 박지 않는다.
1082
+ -- 빈 값이면 투영이 로케일 표에서 "아직 맡은 일 없음 / No work assigned yet"을 만든다.
1083
+ status_line TEXT NOT NULL DEFAULT '',
1084
+ last_activity_at TEXT,
1085
+ pending_count INTEGER NOT NULL DEFAULT 0,
1086
+ pending_kind TEXT NOT NULL DEFAULT 'approval' CHECK(pending_kind IN ('approval','review','input')),
1087
+ unread_count INTEGER NOT NULL DEFAULT 0,
1088
+ unread_generation INTEGER NOT NULL DEFAULT 0,
1089
+ credit_state TEXT NOT NULL DEFAULT 'unknown' CHECK(credit_state IN ('ok','insufficient','unknown')),
1090
+ auto_select_tools INTEGER NOT NULL DEFAULT 1 CHECK(auto_select_tools IN (0,1)),
1091
+ collaboration_style TEXT NOT NULL DEFAULT 'default' CHECK(collaboration_style IN ('default','concise','warm','direct')),
1092
+ handover_note TEXT,
1093
+ revision INTEGER NOT NULL DEFAULT 1
1094
+ );
1095
+ CREATE TABLE one_seat_occupants (
1096
+ seat_id TEXT NOT NULL REFERENCES one_seats(id) ON DELETE CASCADE,
1097
+ slot INTEGER NOT NULL DEFAULT 0,
1098
+ agent_id TEXT,
1099
+ display_name TEXT NOT NULL DEFAULT '',
1100
+ since TEXT NOT NULL,
1101
+ until TEXT,
1102
+ PRIMARY KEY (seat_id, slot, since)
1103
+ );
1104
+ CREATE TABLE one_seats (
1105
+ id TEXT PRIMARY KEY,
1106
+ kind TEXT NOT NULL CHECK(kind IN ('solo','group')),
1107
+ title TEXT NOT NULL DEFAULT '',
1108
+ project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
1109
+ created_at TEXT NOT NULL,
1110
+ updated_at TEXT NOT NULL,
1111
+ archived_at TEXT
1112
+ , dissolved_at TEXT);
1113
+ CREATE TABLE one_taskforces (
1114
+ id TEXT PRIMARY KEY,
1115
+ chat_id TEXT NOT NULL UNIQUE,
1116
+ title TEXT NOT NULL,
1117
+ description TEXT NOT NULL DEFAULT '',
1118
+ member_agent_ids_json TEXT NOT NULL DEFAULT '[]',
1119
+ created_at TEXT NOT NULL,
1120
+ updated_at TEXT NOT NULL,
1121
+ revision INTEGER NOT NULL DEFAULT 1,
1122
+ FOREIGN KEY(chat_id) REFERENCES chats(id) ON DELETE CASCADE
1123
+ );
1124
+ CREATE TABLE plugin_builder_sessions (
1125
+ id TEXT PRIMARY KEY,
1126
+ chat_id TEXT NOT NULL,
1127
+ slug TEXT,
1128
+ phase TEXT NOT NULL CHECK(phase IN ('interview','draft','verify','install','prove','discarded')),
1129
+ staging_dir TEXT,
1130
+ answers_json TEXT,
1131
+ gate_report_json TEXT,
1132
+ seed_json TEXT NOT NULL,
1133
+ created_at TEXT NOT NULL,
1134
+ updated_at TEXT NOT NULL
1135
+ );
982
1136
  CREATE TABLE project_agent_rent_allow (
983
1137
  project_id TEXT NOT NULL,
984
1138
  slug TEXT NOT NULL,
@@ -996,6 +1150,13 @@ CREATE TABLE projects (
996
1150
  updated_at TEXT NOT NULL, folder_path TEXT, system_prompt TEXT, agent_pool_json TEXT NOT NULL DEFAULT '[]', source_type TEXT NOT NULL DEFAULT 'local', source_ref TEXT,
997
1151
  FOREIGN KEY(default_agent_id) REFERENCES installed_agents(id) ON DELETE SET NULL
998
1152
  );
1153
+ CREATE TABLE prompt_chat_start_intents (
1154
+ intent_id TEXT PRIMARY KEY,
1155
+ chat_id TEXT NOT NULL UNIQUE,
1156
+ prompt_digest TEXT NOT NULL,
1157
+ seed_only INTEGER NOT NULL CHECK(seed_only IN (0,1)),
1158
+ created_at TEXT NOT NULL
1159
+ );
999
1160
  CREATE TABLE run_events (
1000
1161
  id TEXT PRIMARY KEY,
1001
1162
  run_id TEXT NOT NULL,
@@ -1123,7 +1284,7 @@ CREATE TABLE "telegram_bindings" (
1123
1284
  designated_project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
1124
1285
  designated_graph_id TEXT,
1125
1286
  legacy_notice_at TEXT
1126
- );
1287
+ , seat_id TEXT);
1127
1288
  CREATE INDEX idx_agent_app_ops_app_created
1128
1289
  ON agent_app_operations(app_id, created_at DESC);
1129
1290
  CREATE INDEX idx_agent_apps_chat_updated
@@ -1142,6 +1303,8 @@ CREATE INDEX idx_agent_evolution_receipts_agent
1142
1303
  ON agent_evolution_receipts(agent_id, created_at DESC);
1143
1304
  CREATE INDEX idx_agent_evolution_receipts_proposal
1144
1305
  ON agent_evolution_receipts(proposal_id, created_at ASC);
1306
+ CREATE INDEX idx_agent_identity_map_agent ON agent_identity_map(immutable_agent_id);
1307
+ CREATE INDEX idx_agent_identity_map_source ON agent_identity_map(mapping_source);
1145
1308
  CREATE INDEX idx_agent_mcp_agent ON agent_mcp_servers(agent_id);
1146
1309
  CREATE INDEX idx_agent_runtime_overrides_updated
1147
1310
  ON agent_runtime_overrides(updated_at DESC);
@@ -1205,6 +1368,7 @@ CREATE INDEX idx_browser_logs_ts ON browser_action_logs(ts DESC);
1205
1368
  CREATE UNIQUE INDEX idx_browser_perm_site_action
1206
1369
  ON browser_permissions(site, action_type);
1207
1370
  CREATE UNIQUE INDEX idx_browser_sessions_site ON browser_sessions(site);
1371
+ CREATE INDEX idx_capability_grants_scope ON capability_grants(scope);
1208
1372
  CREATE UNIQUE INDEX idx_chat_goal_contracts_active_chat
1209
1373
  ON chat_goal_contracts(chat_id)
1210
1374
  WHERE status = 'active';
@@ -1217,6 +1381,7 @@ CREATE INDEX idx_chats_firm_updated ON chats(firm_id, updated_at DESC);
1217
1381
  CREATE INDEX idx_chats_parent ON chats(parent_chat_id);
1218
1382
  CREATE INDEX idx_chats_project_updated
1219
1383
  ON chats(project_id, updated_at DESC);
1384
+ CREATE INDEX idx_chats_seat_updated ON chats(seat_id, updated_at DESC);
1220
1385
  CREATE INDEX idx_chats_updated ON chats(updated_at DESC);
1221
1386
  CREATE INDEX idx_chats_used_updated ON chats(used_at, updated_at DESC);
1222
1387
  CREATE INDEX idx_experience_auto_intake_agent_status
@@ -1283,6 +1448,10 @@ CREATE INDEX idx_installed_agent_hub_binding_exact
1283
1448
  ON installed_agent_hub_bindings(agent_definition_id, agent_release_id);
1284
1449
  CREATE INDEX idx_installed_agents_parent_team ON installed_agents(parent_team_id) WHERE parent_team_id IS NOT NULL;
1285
1450
  CREATE INDEX idx_installed_agents_visibility ON installed_agents(visibility, installed_at DESC);
1451
+ CREATE INDEX idx_invocation_steers_chat
1452
+ ON invocation_steers(chat_id, queued_at, id);
1453
+ CREATE INDEX idx_invocation_steers_queue
1454
+ ON invocation_steers(status, queued_at, id);
1286
1455
  CREATE INDEX idx_judgment_verdicts_recency ON judgment_verdicts(last_hit_at);
1287
1456
  CREATE INDEX idx_memory_agent ON memory_entries(agent_id, superseded_at);
1288
1457
  CREATE INDEX idx_memory_chat ON memory_entries(chat_id);
@@ -1308,6 +1477,24 @@ CREATE INDEX idx_memory_tickets_project_created
1308
1477
  ON memory_tickets(project_id, created_at DESC);
1309
1478
  CREATE INDEX idx_memory_tickets_status_created
1310
1479
  ON memory_tickets(emitter_status, state, created_at DESC);
1480
+ CREATE INDEX idx_occupants_seat_time ON one_seat_occupants(seat_id, since);
1481
+ CREATE INDEX idx_one_artifact_binding_chat
1482
+ ON one_artifact_bindings(chat_id, created_at);
1483
+ CREATE INDEX idx_one_artifact_binding_exact
1484
+ ON one_artifact_bindings(task_id, chat_id, run_id, manifest_id, artifact_ref);
1485
+ CREATE INDEX idx_one_org_members_agent
1486
+ ON one_org_members(installed_agent_id);
1487
+ CREATE INDEX idx_one_org_members_order
1488
+ ON one_org_members(archived_at, sort_order, added_at);
1489
+ CREATE INDEX idx_one_seats_updated ON one_seats(updated_at DESC);
1490
+ CREATE INDEX idx_one_taskforces_updated
1491
+ ON one_taskforces(updated_at DESC);
1492
+ CREATE INDEX idx_plugin_builder_sessions_chat_updated
1493
+ ON plugin_builder_sessions(chat_id, updated_at DESC);
1494
+ CREATE INDEX idx_plugin_builder_sessions_slug_phase
1495
+ ON plugin_builder_sessions(slug, phase);
1496
+ CREATE INDEX idx_prompt_chat_start_chat
1497
+ ON prompt_chat_start_intents(chat_id);
1311
1498
  CREATE INDEX idx_run_events_agent_kind_ts
1312
1499
  ON run_events(agent_id, kind, ts DESC);
1313
1500
  CREATE INDEX idx_run_events_agent_ts ON run_events(agent_id, ts DESC);
@@ -1320,6 +1507,10 @@ CREATE INDEX idx_run_events_run_seq
1320
1507
  CREATE INDEX idx_run_events_ts
1321
1508
  ON run_events(ts DESC);
1322
1509
  CREATE INDEX idx_run_history_automation ON run_history(automation_id);
1510
+ CREATE INDEX idx_seat_occupants_agent
1511
+ ON one_seat_occupants(agent_id) WHERE agent_id IS NOT NULL;
1512
+ CREATE UNIQUE INDEX idx_seat_occupants_current
1513
+ ON one_seat_occupants(seat_id, slot) WHERE until IS NULL;
1323
1514
  CREATE INDEX idx_task_participants_agent ON task_agent_participants(agent_id);
1324
1515
  CREATE INDEX idx_tasks_firm_updated ON tasks(firm_id, updated_at DESC);
1325
1516
  CREATE INDEX idx_tasks_origin_chat ON tasks(origin_chat_id);
@@ -1337,7 +1528,8 @@ CREATE INDEX idx_telegram_bindings_chat
1337
1528
  ON telegram_bindings(telegram_chat_id);
1338
1529
  CREATE INDEX idx_telegram_bindings_enabled
1339
1530
  ON telegram_bindings(enabled, status);
1340
- CREATE UNIQUE INDEX idx_telegram_bindings_one_singleton
1341
- ON telegram_bindings(target_kind) WHERE target_kind = 'one';
1531
+ CREATE UNIQUE INDEX idx_telegram_bindings_one_room
1532
+ ON telegram_bindings(telegram_chat_id)
1533
+ WHERE target_kind = 'one' AND telegram_chat_id IS NOT NULL;
1342
1534
  CREATE INDEX idx_telegram_bindings_target
1343
1535
  ON telegram_bindings(target_kind, target_id);
@@ -24,6 +24,12 @@ const LOGIN_CALLBACK_PATH = "/callback";
24
24
  const LOGIN_TIMEOUT_MS = 180_000;
25
25
  const MAX_LOGIN_SESSION_BYTES = 16 * 1024;
26
26
 
27
+ function validLoginSessionValue(value) {
28
+ return typeof value === "string" && value.length > 0 &&
29
+ Buffer.byteLength(value, "utf8") <= MAX_LOGIN_SESSION_BYTES &&
30
+ !/[\u0000-\u0020\u007f;,]/.test(value);
31
+ }
32
+
27
33
  function webBaseUrl() {
28
34
  return (process.env.AGENTLAS_WEB_BASE_URL || "https://agentlas.cloud").replace(/\/$/, "");
29
35
  }
@@ -97,13 +103,13 @@ function createLoginCallbackGuard(expectedState) {
97
103
  message: "The callback did not include a session value.",
98
104
  };
99
105
  }
100
- if (Buffer.byteLength(value, "utf8") > MAX_LOGIN_SESSION_BYTES) {
106
+ if (!validLoginSessionValue(value)) {
101
107
  return {
102
108
  handled: true,
103
109
  final: true,
104
110
  ok: false,
105
111
  statusCode: 400,
106
- message: "The login session value is too large.",
112
+ message: "The login session value is invalid.",
107
113
  };
108
114
  }
109
115
  return { handled: true, final: true, ok: true, statusCode: 200, value, message: "Agentlas login complete" };
@@ -120,6 +126,9 @@ function openInBrowser(url) {
120
126
  : ["xdg-open", url];
121
127
  try {
122
128
  const child = spawn(argv[0], argv.slice(1), { stdio: "ignore", detached: true });
129
+ // spawn failures such as a missing xdg-open arrive asynchronously. Without
130
+ // a listener Node treats them as an uncaught process error.
131
+ child.once("error", () => {});
123
132
  child.unref();
124
133
  } catch { /* ignore */ }
125
134
  }
@@ -220,18 +229,35 @@ function cliSessionPath() {
220
229
 
221
230
  function readCliSessionValue() {
222
231
  try {
223
- const j = JSON.parse(fs.readFileSync(cliSessionPath(), "utf8"));
224
- return (j && typeof j.value === "string" && j.value) || null;
232
+ const p = cliSessionPath();
233
+ const stat = fs.lstatSync(p);
234
+ if (stat.isSymbolicLink() || !stat.isFile() || stat.size > MAX_LOGIN_SESSION_BYTES * 2) return null;
235
+ const j = JSON.parse(fs.readFileSync(p, "utf8"));
236
+ return j && validLoginSessionValue(j.value) ? j.value : null;
225
237
  } catch {
226
238
  return null;
227
239
  }
228
240
  }
229
241
 
230
242
  function saveCliSession(value) {
243
+ if (!validLoginSessionValue(value)) throw new Error("Refusing to save an invalid Agentlas login session value.");
231
244
  const p = cliSessionPath();
232
245
  // 디렉터리 0700 + 파일 0600 — 기본 umask(0644)로 세션이 world-readable 이 되는 것을 막는다.
233
- fs.mkdirSync(path.dirname(p), { recursive: true, mode: 0o700 });
234
- fs.writeFileSync(p, JSON.stringify({ version: 1, value, updatedAt: new Date().toISOString() }, null, 2) + "\n", { mode: 0o600 });
246
+ const dir = path.dirname(p);
247
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
248
+ try { fs.chmodSync(dir, 0o700); } catch { /* win32 */ }
249
+ const temp = path.join(dir, `.cli-session.${process.pid}.${crypto.randomUUID()}.tmp`);
250
+ try {
251
+ fs.writeFileSync(temp, JSON.stringify({ version: 1, value, updatedAt: new Date().toISOString() }, null, 2) + "\n", {
252
+ encoding: "utf8",
253
+ mode: 0o600,
254
+ flag: "wx",
255
+ });
256
+ fs.renameSync(temp, p);
257
+ } catch (error) {
258
+ try { fs.rmSync(temp, { force: true }); } catch { /* preserve original */ }
259
+ throw error;
260
+ }
235
261
  try { fs.chmodSync(p, 0o600); } catch { /* win32 */ }
236
262
  return p;
237
263
  }
@@ -246,7 +272,7 @@ function deleteCliSession() {
246
272
  // 쿠키 해석 순서 계약: AGENTLAS_SESSION env → 세션 파일. (v1의 keytar 폴백은
247
273
  // 데스크탑이 세션을 keytar에 두지 않아 항상 비어 있었다 — v2에서는 싣지 않는다.)
248
274
  function cloudSessionCookie() {
249
- if (process.env.AGENTLAS_SESSION) return `agentlas_session=${process.env.AGENTLAS_SESSION}`;
275
+ if (validLoginSessionValue(process.env.AGENTLAS_SESSION)) return `agentlas_session=${process.env.AGENTLAS_SESSION}`;
250
276
  const fileValue = readCliSessionValue();
251
277
  if (fileValue) return `agentlas_session=${fileValue}`;
252
278
  return null;
@@ -275,5 +301,5 @@ module.exports = {
275
301
  deleteCliSession,
276
302
  cloudSessionCookie,
277
303
  fetchSessionMeta,
278
- _test: { createLoginState, createLoginCallbackGuard },
304
+ _test: { createLoginState, createLoginCallbackGuard, validLoginSessionValue },
279
305
  };
@@ -68,6 +68,22 @@ function cloudCasResponseError(response, label) {
68
68
  }
69
69
 
70
70
  /** 등록(생성/갱신). 성공 시 검증된 리비전 디스크립터(+operation/url)를 반환한다. */
71
+ // 서버는 받은 것보다 적게 저장할 수 있고, 그건 증명 실패가 아니다.
72
+ //
73
+ // 등록은 제출 해시를 검증한 뒤, 자체 스캔이 자격증명으로 판정한 파일을 빼고
74
+ // 나머지를 새 해시로 저장한다(uploadReceipt.omissions가 뺀 경로를 전부 적는다).
75
+ // 저장 해시만 대조하면, 이미 허브에 게시가 끝난 뒤에 "잘못된 등록 영수증"으로
76
+ // 실패하게 된다 — 에이전트는 검색·호출되는데 올린 사람은 실패로 듣는다.
77
+ // 증명의 목적은 "서버가 바로 이 패키지를 봤다"이고 submittedPackageHash가 그
78
+ // 증거이므로, 둘 중 하나가 우리 해시와 같으면 통과시킨다. 둘 다 아니면 그대로 실패.
79
+ function registrationSawOurPackage(json, manifest) {
80
+ const ours = String(manifest.packageHash || "").toLowerCase();
81
+ if (String(json.packageHash || "").toLowerCase() === ours) return true;
82
+ const receipt = json && typeof json === "object" ? json.uploadReceipt : null;
83
+ if (!receipt || typeof receipt !== "object") return false;
84
+ return String(receipt.submittedPackageHash || "").toLowerCase() === ours;
85
+ }
86
+
71
87
  async function registerCloudAgent(manifest, bundlePath, review, visibility, options = {}) {
72
88
  const cookie = await cloudSessionCookie();
73
89
  if (!cookie) throw new Error("Agent Cloud sign-in is required. Sign in through Desktop or set AGENTLAS_SESSION.");
@@ -119,7 +135,7 @@ async function registerCloudAgent(manifest, bundlePath, review, visibility, opti
119
135
  json.dryRun !== false ||
120
136
  typeof json.cloudId !== "string" || !json.cloudId.trim() ||
121
137
  json.slug !== manifest.slug ||
122
- json.packageHash !== manifest.packageHash ||
138
+ !registrationSawOurPackage(json, manifest) ||
123
139
  json.packageHashVersion !== manifest.packageHashVersion ||
124
140
  typeof json.revision !== "string" || etag !== cloudRevisionEtag(json.revision) ||
125
141
  typeof json.registeredAt !== "string" || !Number.isFinite(Date.parse(json.registeredAt)) ||
@@ -138,11 +154,21 @@ async function registerCloudAgent(manifest, bundlePath, review, visibility, opti
138
154
  etag,
139
155
  updatedAt: json.savedAt || json.registeredAt,
140
156
  }, "registration receipt");
157
+ // 서버가 자기 스캔으로 뺀 파일은 사용자에게 보여야 한다. 영수증 안에만 있으면
158
+ // "내 파일이 안 올라갔다"는 사실을 아무도 읽지 않는다.
159
+ const serverWithheld = (() => {
160
+ const receipt = json && typeof json === "object" ? json.uploadReceipt : null;
161
+ const omissions = receipt && Array.isArray(receipt.omissions) ? receipt.omissions : [];
162
+ return omissions
163
+ .map((entry) => (entry && typeof entry.path === "string" ? entry.path : ""))
164
+ .filter(Boolean);
165
+ })();
141
166
  return {
142
167
  ...descriptor,
143
168
  operation: json.operation,
144
169
  ...(typeof json.url === "string" ? { url: json.url } : {}),
145
170
  ...(typeof json.marketplaceUrl === "string" ? { marketplaceUrl: json.marketplaceUrl } : {}),
171
+ ...(serverWithheld.length ? { serverWithheld } : {}),
146
172
  registeredAt: json.registeredAt,
147
173
  dryRun: false,
148
174
  };