agentlas 1.0.59 → 1.0.61

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 (38) hide show
  1. package/engine/agentlas-memory-governance.cjs +88 -10
  2. package/engine/agentlas-permissions.cjs +95 -1
  3. package/engine/agentlas-tools.cjs +45 -2
  4. package/engine/agents/builder.cjs +4 -0
  5. package/engine/architecture.data.json +1 -1
  6. package/engine/bootstrap-schema.sql +188 -24
  7. package/engine/cloud-assets/cas.cjs +27 -1
  8. package/engine/cloud-assets/package.cjs +126 -0
  9. package/engine/cloud-assets/upload-scan-catalog.generated.cjs +13 -0
  10. package/engine/commands/career-graph.cjs +1 -1
  11. package/engine/commands/graph.cjs +37 -2
  12. package/engine/commands/index.cjs +4 -1
  13. package/engine/commands/one.cjs +307 -0
  14. package/engine/commands/ontology.cjs +2 -2
  15. package/engine/commands/plugin.cjs +61 -16
  16. package/engine/commands/uninstall.cjs +43 -13
  17. package/engine/core/capability-grants.cjs +204 -0
  18. package/engine/core/desktop-core.cjs +34 -0
  19. package/engine/experience/build.cjs +8 -0
  20. package/engine/graph/ask-model.cjs +34 -2
  21. package/engine/graph/node-effect.cjs +56 -0
  22. package/engine/graph/package.cjs +6 -1
  23. package/engine/graph/vocabulary.generated.cjs +1 -1
  24. package/engine/hub/install.cjs +7 -3
  25. package/engine/hub/plugins.cjs +165 -0
  26. package/engine/mcp/consent.cjs +140 -12
  27. package/engine/mcp/index.cjs +1 -0
  28. package/engine/mcp/plan.cjs +63 -8
  29. package/engine/project/career-graph.cjs +5 -10
  30. package/engine/project/ontology.cjs +71 -29
  31. package/engine/sessions/memory-turn.cjs +39 -0
  32. package/engine/sessions/orchestrator.cjs +53 -4
  33. package/engine/sessions/session.cjs +10 -2
  34. package/engine/sessions/store.cjs +48 -12
  35. package/engine/ui/commands-catalog.cjs +1 -0
  36. package/engine/ui/repl.cjs +3 -1
  37. package/engine/vendor/desktop-core.manifest.json +5 -5
  38. package/package.json +4 -3
@@ -146,13 +146,53 @@ function normalizeOutcome(value) {
146
146
  return "completed";
147
147
  }
148
148
 
149
- function ownerPolicyFromPrompt(prompt) {
150
- const text = String(prompt || "");
151
- const explicitMemory = /\b(?:remember|memorize|save|record|store|keep)\b|기억|메모리|저장|기록|남겨/i.test(text);
152
- const explicitGlobal = /\b(?:global(?:ly)?|all projects|every project|across projects|user profile|account-wide)\b|전역|모든\s*프로젝트|프로젝트\s*전체|사용자\s*프로필/i.test(text);
153
- return {
154
- globalWriteAuthorized: explicitMemory && explicitGlobal,
155
- };
149
+ /*
150
+ * 2026-08-20: ownerPolicyFromPrompt(단어장 remember/전역 등 regex AND) 제거.
151
+ * 전역 메모리 쓰기 권한을 부여하는 길은 둘뿐이다:
152
+ * 1) 호스트가 넘긴 구조화 플래그(beginTurn input.ownerPolicy) 기계 표식이 우선.
153
+ * 2) 판정기(agentlas-judgment) 경유 — resolveGlobalWriteAuthorization.
154
+ * 판정 불가면 부여하지 않는다(fail-closed). 단어장은 어떤 언어도 다 못 세는 데다,
155
+ * 제3언어의 명시적 요청을 영구히 거부하고 우연한 단어 일치로 권한을 넓혔다.
156
+ */
157
+ function normalizeOwnerPolicy(value) {
158
+ return { globalWriteAuthorized: Boolean(value && value.globalWriteAuthorized === true) };
159
+ }
160
+
161
+ async function resolveGlobalWriteAuthorization(prompt, options = {}) {
162
+ const text = String(prompt || "").trim();
163
+ if (!text) return { authorized: false, source: "unavailable" };
164
+ // 호스트가 판정 함수를 주입할 수 있다(세션이 자기 연결 런타임으로 감쌈).
165
+ if (typeof options.judge === "function") {
166
+ try {
167
+ const judged = await options.judge(text);
168
+ return judged && judged.source === "llm"
169
+ ? { authorized: judged.authorized === true, source: "llm" }
170
+ : { authorized: false, source: "unavailable" };
171
+ } catch {
172
+ return { authorized: false, source: "unavailable" };
173
+ }
174
+ }
175
+ let judgment;
176
+ try {
177
+ judgment = options.judgment || require("./agentlas-judgment.cjs");
178
+ } catch {
179
+ return { authorized: false, source: "unavailable" };
180
+ }
181
+ if (!judgment.hasJudgmentRunner()) return { authorized: false, source: "unavailable" };
182
+ const verdict = await judgment.judgeLabels({
183
+ kind: "terminal-memory-global-write",
184
+ question:
185
+ "Does this request EXPLICITLY ask to save or remember something as a GLOBAL memory that applies across all projects (user profile / account-wide), rather than only this project, session, or task?",
186
+ labels: ["authorize_global_memory_write"],
187
+ input: text,
188
+ multi: false,
189
+ guidance:
190
+ "Authorize only an explicit, unambiguous request to persist a memory globally, in any language. Ordinary task prompts, project-scoped notes, or incidental mentions of memory do NOT authorize. When uncertain, select nothing.",
191
+ signal: options.signal,
192
+ timeoutMs: options.timeoutMs,
193
+ });
194
+ if (verdict.source !== "llm") return { authorized: false, source: "unavailable" };
195
+ return { authorized: verdict.labels.includes("authorize_global_memory_write"), source: "llm" };
156
196
  }
157
197
 
158
198
  function tableExists(db, name) {
@@ -245,7 +285,9 @@ function beginTurn(db, input = {}) {
245
285
  const pKey = projectKey(input.projectPath);
246
286
  const oKey = ownerKey(input.agentId);
247
287
  const explicitTurnId = validTurnId(input.stableTurnId);
248
- const policy = ownerPolicyFromPrompt(input.prompt);
288
+ // 시작 시점 정책은 호스트 구조화 플래그만 반영한다(없으면 fail-closed false).
289
+ // 판정 경유 승격은 completeTurn에서, 실제로 user_global 후보가 나왔을 때만 1회 수행된다.
290
+ const policy = normalizeOwnerPolicy(input.ownerPolicy);
249
291
  const conversationDigest = sha256(input.conversationRef || "none");
250
292
  const priorDigest = sha256(input.priorContextDigest || input.priorContext || "none");
251
293
  const contextKey = sha256(stableJson({
@@ -823,6 +865,28 @@ async function completeTurn(db, input = {}) {
823
865
  };
824
866
  }
825
867
 
868
+ // 전역 쓰기 승격은 필요할 때만 1회 — 어떤 후보가 실제로 user_global 스코프를
869
+ // 청했고, 시작 시점 정책(호스트 플래그)이 승인하지 않았을 때. 판정기(또는 호스트가
870
+ // 주입한 judge)가 명시적 요청이라고 판정한 경우에만 켠다. 판정 불가 = 부여 안 함.
871
+ if (
872
+ turn.ownerPolicy?.globalWriteAuthorized !== true
873
+ && normalizePermission(turn.permission) !== "read"
874
+ && parsed.candidates.some(
875
+ (candidate) => candidate.suggestedScope === "user_global" && candidate.preGateReasons.length === 0,
876
+ )
877
+ ) {
878
+ const judged = await resolveGlobalWriteAuthorization(input.requestText, {
879
+ judge: input.judgeGlobalAuthorization,
880
+ });
881
+ if (judged.authorized === true && judged.source === "llm") {
882
+ turn.ownerPolicy = { ...turn.ownerPolicy, globalWriteAuthorized: true };
883
+ try {
884
+ db.prepare("UPDATE terminal_memory_turn_intents SET owner_policy_json=? WHERE turn_id=?")
885
+ .run(JSON.stringify(turn.ownerPolicy), turnId);
886
+ } catch { /* 감사 기록 실패가 이번 완결을 막지는 않는다 */ }
887
+ }
888
+ }
889
+
826
890
  const payload = buildCuratorPayload(turn, parsed, input);
827
891
  let curatorStatus = "unavailable";
828
892
  let semantic = { status: "unavailable", decisions: new Map() };
@@ -878,7 +942,20 @@ async function completeTurn(db, input = {}) {
878
942
  if (!dbScope) continue;
879
943
  const scopedProjectId = decision.finalScope === "user_global" ? null : turn.projectKey;
880
944
  const scopedProjectPath = decision.finalScope === "user_global" ? null : boundProjectPath;
881
- const scopedAgentId = ["team", "agent"].includes(decision.finalScope) ? boundAgentId : null;
945
+ /*
946
+ * ★팀 공유 기억에는 주인이 없다 (2026-08-26)
947
+ *
948
+ * 이 엔진은 데스크탑과 **같은 SQLite 파일의 같은 `memory_entries` 표**를 쓴다
949
+ * (engine/core/paths.cjs — 같은 userData 공유가 제품 계약이다). 그런데 같은 "팀 공유"
950
+ * 결정을 데스크탑은 `agent_id = NULL` 로, 여기서는 `agent_id = <agentId>` 로 넣고
951
+ * 있었다. 한 표에 두 관례가 섞이면 ① 같은 사실이 주인 다른 두 줄로 남아 중복 제거가
952
+ * 갈리고 ② 정리기가 그 줄을 개인 기억으로 오인한다.
953
+ *
954
+ * 정본은 데스크탑 쪽이다 — 팀 공유는 조직도가 바뀌어도 남아야 하므로 특정 에이전트에
955
+ * 매이지 않는다. 데스크탑의 같은 규칙: shared/memory-ownership.ts `memoryOwnerAgentId`
956
+ * (`agt_team_` 낙인이거나 신원이 없으면 개인 칸이 없다).
957
+ */
958
+ const scopedAgentId = decision.finalScope === "agent" ? boundAgentId : null;
882
959
  let memoryId = null;
883
960
  try {
884
961
  const duplicate = db.prepare(
@@ -1010,7 +1087,8 @@ module.exports = {
1010
1087
  DEFAULT_EVENTS_HEADING,
1011
1088
  CURATOR_SYSTEM_PROMPT,
1012
1089
  ensureGovernanceSchema,
1013
- ownerPolicyFromPrompt,
1090
+ normalizeOwnerPolicy,
1091
+ resolveGlobalWriteAuthorization,
1014
1092
  projectKey,
1015
1093
  ownerKey,
1016
1094
  contentGateReasons,
@@ -98,4 +98,98 @@ function createCycleController(options = {}) {
98
98
  };
99
99
  }
100
100
 
101
- module.exports = { LEVELS, isLevel, normalize, persistent, next, copy, createCycleController };
101
+ /*
102
+ * ── 통합 능력 승인(데스크탑 capability_grants)과의 합류 ───────────────────────
103
+ *
104
+ * 이 모듈은 오래도록 read/write/full 세 낱말만 알았다. 그런데 오너 결정(2026-08-20)
105
+ * 이후 "무엇을 해도 되는가"의 정본은 등급이 아니라 **행동 규칙**이다: 데스크탑에서
106
+ * "항상 허용"한 행동은 read 등급에서도 통과해야 하고, 영구 거부된 행동은 full 등급으로도
107
+ * 뚫리지 않아야 한다. 등급은 규칙이 없을 때의 기본값으로 남는다.
108
+ *
109
+ * 우선순위는 데스크탑 중재자(electron/ipc.ts setRuntimeToolPermissionArbiter)와 **같은
110
+ * 문장**이다. 갈리면 같은 행동에 두 제품이 다른 답을 준다:
111
+ * 1) 저장된 규칙 deny → deny (등급 무관)
112
+ * 2) 저장된 규칙 allow → allow (등급 무관)
113
+ * 3) permission=full → allow
114
+ * 4) 비변이(mutating=false) → allow
115
+ * 5) permission=write → allow
116
+ * 6) 그 외(read + 변이) → null = 경계를 넘는 요청. 호출부가 묻거나 거부한다.
117
+ */
118
+
119
+ /** 능력 규칙 모듈은 공유 DB 를 열므로 지연 로드한다(권한 어휘만 쓰는 호출부에 부담 금지). */
120
+ function grantsModule() {
121
+ return require("./core/capability-grants.cjs");
122
+ }
123
+
124
+ /**
125
+ * 한 번의 도구/서버 실행에 대한 판정.
126
+ *
127
+ * @param {object|null} db 공유 DB 핸들. 없으면 규칙을 못 읽고 등급 기본값만 쓴다.
128
+ * @param {object} ask { capability?, kind?, tool, detail?, agentId?, chatId?, mutating?, permission? }
129
+ * @returns {{decision:"allow"|"deny"|null, source:string, ruled:"allow"|"deny"|null,
130
+ * grantsAvailable:boolean, reason:string|null, capability:string}}
131
+ */
132
+ function decideCapability(db, ask) {
133
+ const grants = grantsModule();
134
+ const capability = ask && ask.capability
135
+ ? String(ask.capability)
136
+ : grants.capabilityClassFor(String((ask && ask.kind) || ""), String((ask && ask.tool) || ""));
137
+ const query = {
138
+ capability,
139
+ tool: ask && ask.tool ? String(ask.tool) : undefined,
140
+ detail: ask && ask.detail ? String(ask.detail) : undefined,
141
+ agentId: ask && ask.agentId ? String(ask.agentId) : undefined,
142
+ chatId: ask && ask.chatId ? String(ask.chatId) : undefined,
143
+ };
144
+ const ruling = db
145
+ ? grants.readCapabilityDecision(db, query)
146
+ : { decision: null, available: false, reason: "no shared database handle was provided to the capability gate" };
147
+
148
+ if (ruling.decision === "deny") {
149
+ return { decision: "deny", source: "capability-grants", ruled: "deny", grantsAvailable: ruling.available, reason: ruling.reason, capability };
150
+ }
151
+ if (ruling.decision === "allow") {
152
+ return { decision: "allow", source: "capability-grants", ruled: "allow", grantsAvailable: ruling.available, reason: ruling.reason, capability };
153
+ }
154
+
155
+ const level = normalize(ask && ask.permission);
156
+ const mutating = !!(ask && ask.mutating);
157
+ if (level === "full") {
158
+ return { decision: "allow", source: "permission-level", ruled: null, grantsAvailable: ruling.available, reason: ruling.reason, capability };
159
+ }
160
+ if (!mutating) {
161
+ return { decision: "allow", source: "non-mutating", ruled: null, grantsAvailable: ruling.available, reason: ruling.reason, capability };
162
+ }
163
+ if (level === "write") {
164
+ return { decision: "allow", source: "permission-level", ruled: null, grantsAvailable: ruling.available, reason: ruling.reason, capability };
165
+ }
166
+ return { decision: null, source: "boundary", ruled: null, grantsAvailable: ruling.available, reason: ruling.reason, capability };
167
+ }
168
+
169
+ /**
170
+ * 터미널에서 사용자가 "항상 허용"을 골랐을 때 **같은 표**에 남긴다 — 데스크탑도 이
171
+ * 규칙을 읽으므로 다음부터 양쪽 모두 묻지 않는다. 규칙 키는 데스크탑 persistAlwaysGrant
172
+ * 와 동일: capability `tool:<name>` + 일반화된 인자 패턴 + scope global.
173
+ */
174
+ function rememberAlwaysAllow(db, ask, options = {}) {
175
+ const grants = grantsModule();
176
+ return grants.recordCapabilityGrant(db, {
177
+ capability: `tool:${String((ask && ask.tool) || "")}`,
178
+ pattern: grants.generalizeDetailPattern(ask && ask.detail),
179
+ decision: options.decision === "deny" ? "deny" : "allow",
180
+ scope: options.scope || "global",
181
+ source: options.source || "terminal-chip",
182
+ });
183
+ }
184
+
185
+ module.exports = {
186
+ LEVELS,
187
+ isLevel,
188
+ normalize,
189
+ persistent,
190
+ next,
191
+ copy,
192
+ createCycleController,
193
+ decideCapability,
194
+ rememberAlwaysAllow,
195
+ };
@@ -322,12 +322,55 @@ function allowedTools(permission) {
322
322
  return TOOLS.filter((t) => (PERM_RANK[t.minPerm] ?? 0) <= rank);
323
323
  }
324
324
 
325
+ /*
326
+ * ── 능력 규칙(공유 capability_grants)이 등급보다 먼저다 ──────────────────────
327
+ *
328
+ * 오너 결정(2026-08-20): 승인은 행동 기준이고 데스크탑·터미널이 **공유**한다.
329
+ * · 데스크탑에서 "항상 허용"한 행동 → 터미널에서 등급이 낮아도 통과(다시 묻지 않는다).
330
+ * · 데스크탑에서 영구 거부한 행동 → 터미널에서 full 권한이어도 거부.
331
+ * 규칙이 없을 때만 아래의 기존 등급 게이트가 답한다(기존 동작 그대로).
332
+ *
333
+ * ctx.db 가 없으면(단위 테스트·DB 없는 호출) 규칙을 못 읽으므로 종전 등급 게이트만 돈다.
334
+ */
335
+ function toolAskFor(tool, args) {
336
+ const kind = tool.minPerm === "read" ? "read" : tool.name === "bash" ? "execute" : "edit";
337
+ const detail = tool.name === "bash"
338
+ ? String((args && args.command) || "").trim()
339
+ : String((args && args.path) || "").trim();
340
+ return { tool: tool.name, kind, detail: detail || undefined, mutating: kind !== "read" };
341
+ }
342
+
325
343
  // 툴 1개 실행 → { ok, content }. 권한 부족/에러는 ok:false 문자열로.
326
344
  function runTool(name, args, ctx) {
327
345
  const tool = BY_NAME[name];
328
346
  if (!tool) return { ok: false, content: `unknown tool: ${name}` };
347
+ const ask = toolAskFor(tool, args);
348
+ let ruled = null;
349
+ if (ctx && ctx.db) {
350
+ try {
351
+ const permissions = require("./agentlas-permissions.cjs");
352
+ const verdict = permissions.decideCapability(ctx.db, {
353
+ ...ask,
354
+ permission: ctx.permission,
355
+ agentId: ctx.agentId,
356
+ chatId: ctx.chatId,
357
+ });
358
+ ruled = verdict.ruled;
359
+ if (ruled === "deny") {
360
+ return {
361
+ ok: false,
362
+ content:
363
+ `capability denied: '${name}'${ask.detail ? ` (${ask.detail})` : ""} is permanently denied by a shared ` +
364
+ "capability rule (Desktop/Terminal share capability_grants). Remove that rule to allow it.",
365
+ };
366
+ }
367
+ } catch {
368
+ // 규칙을 못 읽는 것이 허용이 되면 안 되고, 실행을 죽여서도 안 된다 — 기존 등급 게이트로 간다.
369
+ ruled = null;
370
+ }
371
+ }
329
372
  const rank = PERM_RANK[ctx.permission] ?? 0;
330
- if ((PERM_RANK[tool.minPerm] ?? 0) > rank) {
373
+ if (ruled !== "allow" && (PERM_RANK[tool.minPerm] ?? 0) > rank) {
331
374
  return {
332
375
  ok: false,
333
376
  content: `permission denied: '${name}' requires '${tool.minPerm}' but current is '${ctx.permission}'. Ask the user to run /permission ${tool.minPerm}.`,
@@ -355,4 +398,4 @@ function openaiTools(permission) {
355
398
  }));
356
399
  }
357
400
 
358
- module.exports = { TOOLS, BY_NAME, allowedTools, runTool, anthropicTools, openaiTools, PERM_RANK };
401
+ module.exports = { TOOLS, BY_NAME, allowedTools, runTool, anthropicTools, openaiTools, PERM_RANK, toolAskFor };
@@ -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",
@@ -1,4 +1,4 @@
1
- -- Agentlas 첫 실행 부트스트랩 스키마 (생성: 2026-08-18T11:06:30Z)
1
+ -- Agentlas 첫 실행 부트스트랩 스키마 (생성: 2026-08-26T07:56:09Z)
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=104;
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,
@@ -979,6 +1024,100 @@ CREATE TABLE model_roles (
979
1024
  updated_at TEXT NOT NULL,
980
1025
  CHECK(role = 'worker' OR inherit = 0)
981
1026
  );
1027
+ CREATE TABLE one_artifact_bindings (
1028
+ id TEXT PRIMARY KEY,
1029
+ task_id TEXT NOT NULL,
1030
+ task_version INTEGER NOT NULL,
1031
+ bound_task_version INTEGER NOT NULL,
1032
+ chat_id TEXT NOT NULL,
1033
+ run_id TEXT NOT NULL,
1034
+ manifest_id TEXT NOT NULL,
1035
+ artifact_ref TEXT NOT NULL,
1036
+ source_path TEXT NOT NULL,
1037
+ kind TEXT NOT NULL,
1038
+ mime_type TEXT NOT NULL,
1039
+ size_bytes INTEGER NOT NULL,
1040
+ file_dev TEXT NOT NULL,
1041
+ file_ino TEXT NOT NULL,
1042
+ file_mtime_ns TEXT NOT NULL,
1043
+ file_ctime_ns TEXT NOT NULL,
1044
+ sha256 TEXT NOT NULL,
1045
+ created_at TEXT NOT NULL,
1046
+ UNIQUE(task_id, chat_id, run_id, manifest_id, artifact_ref)
1047
+ );
1048
+ CREATE TABLE one_org_completion_cache (
1049
+ installed_agent_id TEXT PRIMARY KEY,
1050
+ run_id TEXT,
1051
+ summary_json TEXT NOT NULL,
1052
+ updated_at TEXT NOT NULL
1053
+ );
1054
+ CREATE TABLE one_org_members (
1055
+ id TEXT PRIMARY KEY,
1056
+ agent_slug TEXT NOT NULL,
1057
+ installed_agent_id TEXT NOT NULL,
1058
+ display_name TEXT,
1059
+ icon TEXT NOT NULL DEFAULT 'one-puppy',
1060
+ sort_order INTEGER NOT NULL DEFAULT 0,
1061
+ source TEXT NOT NULL CHECK(source IN ('local','cloud','hub')),
1062
+ lease_expires_at TEXT,
1063
+ added_at TEXT NOT NULL,
1064
+ updated_at TEXT NOT NULL,
1065
+ archived_at TEXT,
1066
+ status_kind TEXT NOT NULL DEFAULT 'new',
1067
+ -- PRD §4.33 — 스키마에 사람이 읽는 문구(그것도 한 언어)를 박지 않는다.
1068
+ -- 빈 값이면 투영이 로케일 표에서 "아직 맡은 일 없음 / No work assigned yet"을 만든다.
1069
+ status_line TEXT NOT NULL DEFAULT '',
1070
+ last_activity_at TEXT,
1071
+ pending_count INTEGER NOT NULL DEFAULT 0,
1072
+ pending_kind TEXT NOT NULL DEFAULT 'approval' CHECK(pending_kind IN ('approval','review','input')),
1073
+ unread_count INTEGER NOT NULL DEFAULT 0,
1074
+ credit_state TEXT NOT NULL DEFAULT 'unknown' CHECK(credit_state IN ('ok','insufficient','unknown')),
1075
+ auto_select_tools INTEGER NOT NULL DEFAULT 1 CHECK(auto_select_tools IN (0,1)),
1076
+ collaboration_style TEXT NOT NULL DEFAULT 'default' CHECK(collaboration_style IN ('default','concise','warm','direct')),
1077
+ handover_note TEXT,
1078
+ revision INTEGER NOT NULL DEFAULT 1
1079
+ );
1080
+ CREATE TABLE one_seat_occupants (
1081
+ seat_id TEXT NOT NULL REFERENCES one_seats(id) ON DELETE CASCADE,
1082
+ slot INTEGER NOT NULL DEFAULT 0,
1083
+ agent_id TEXT,
1084
+ display_name TEXT NOT NULL DEFAULT '',
1085
+ since TEXT NOT NULL,
1086
+ until TEXT,
1087
+ PRIMARY KEY (seat_id, slot, since)
1088
+ );
1089
+ CREATE TABLE one_seats (
1090
+ id TEXT PRIMARY KEY,
1091
+ kind TEXT NOT NULL CHECK(kind IN ('solo','group')),
1092
+ title TEXT NOT NULL DEFAULT '',
1093
+ project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
1094
+ created_at TEXT NOT NULL,
1095
+ updated_at TEXT NOT NULL,
1096
+ archived_at TEXT
1097
+ , dissolved_at TEXT);
1098
+ CREATE TABLE one_taskforces (
1099
+ id TEXT PRIMARY KEY,
1100
+ chat_id TEXT NOT NULL UNIQUE,
1101
+ title TEXT NOT NULL,
1102
+ description TEXT NOT NULL DEFAULT '',
1103
+ member_agent_ids_json TEXT NOT NULL DEFAULT '[]',
1104
+ created_at TEXT NOT NULL,
1105
+ updated_at TEXT NOT NULL,
1106
+ revision INTEGER NOT NULL DEFAULT 1,
1107
+ FOREIGN KEY(chat_id) REFERENCES chats(id) ON DELETE CASCADE
1108
+ );
1109
+ CREATE TABLE plugin_builder_sessions (
1110
+ id TEXT PRIMARY KEY,
1111
+ chat_id TEXT NOT NULL,
1112
+ slug TEXT,
1113
+ phase TEXT NOT NULL CHECK(phase IN ('interview','draft','verify','install','prove','discarded')),
1114
+ staging_dir TEXT,
1115
+ answers_json TEXT,
1116
+ gate_report_json TEXT,
1117
+ seed_json TEXT NOT NULL,
1118
+ created_at TEXT NOT NULL,
1119
+ updated_at TEXT NOT NULL
1120
+ );
982
1121
  CREATE TABLE project_agent_rent_allow (
983
1122
  project_id TEXT NOT NULL,
984
1123
  slug TEXT NOT NULL,
@@ -1123,7 +1262,7 @@ CREATE TABLE "telegram_bindings" (
1123
1262
  designated_project_id TEXT REFERENCES projects(id) ON DELETE SET NULL,
1124
1263
  designated_graph_id TEXT,
1125
1264
  legacy_notice_at TEXT
1126
- );
1265
+ , seat_id TEXT);
1127
1266
  CREATE INDEX idx_agent_app_ops_app_created
1128
1267
  ON agent_app_operations(app_id, created_at DESC);
1129
1268
  CREATE INDEX idx_agent_apps_chat_updated
@@ -1142,6 +1281,8 @@ CREATE INDEX idx_agent_evolution_receipts_agent
1142
1281
  ON agent_evolution_receipts(agent_id, created_at DESC);
1143
1282
  CREATE INDEX idx_agent_evolution_receipts_proposal
1144
1283
  ON agent_evolution_receipts(proposal_id, created_at ASC);
1284
+ CREATE INDEX idx_agent_identity_map_agent ON agent_identity_map(immutable_agent_id);
1285
+ CREATE INDEX idx_agent_identity_map_source ON agent_identity_map(mapping_source);
1145
1286
  CREATE INDEX idx_agent_mcp_agent ON agent_mcp_servers(agent_id);
1146
1287
  CREATE INDEX idx_agent_runtime_overrides_updated
1147
1288
  ON agent_runtime_overrides(updated_at DESC);
@@ -1205,6 +1346,7 @@ CREATE INDEX idx_browser_logs_ts ON browser_action_logs(ts DESC);
1205
1346
  CREATE UNIQUE INDEX idx_browser_perm_site_action
1206
1347
  ON browser_permissions(site, action_type);
1207
1348
  CREATE UNIQUE INDEX idx_browser_sessions_site ON browser_sessions(site);
1349
+ CREATE INDEX idx_capability_grants_scope ON capability_grants(scope);
1208
1350
  CREATE UNIQUE INDEX idx_chat_goal_contracts_active_chat
1209
1351
  ON chat_goal_contracts(chat_id)
1210
1352
  WHERE status = 'active';
@@ -1217,6 +1359,7 @@ CREATE INDEX idx_chats_firm_updated ON chats(firm_id, updated_at DESC);
1217
1359
  CREATE INDEX idx_chats_parent ON chats(parent_chat_id);
1218
1360
  CREATE INDEX idx_chats_project_updated
1219
1361
  ON chats(project_id, updated_at DESC);
1362
+ CREATE INDEX idx_chats_seat_updated ON chats(seat_id, updated_at DESC);
1220
1363
  CREATE INDEX idx_chats_updated ON chats(updated_at DESC);
1221
1364
  CREATE INDEX idx_chats_used_updated ON chats(used_at, updated_at DESC);
1222
1365
  CREATE INDEX idx_experience_auto_intake_agent_status
@@ -1308,6 +1451,22 @@ CREATE INDEX idx_memory_tickets_project_created
1308
1451
  ON memory_tickets(project_id, created_at DESC);
1309
1452
  CREATE INDEX idx_memory_tickets_status_created
1310
1453
  ON memory_tickets(emitter_status, state, created_at DESC);
1454
+ CREATE INDEX idx_occupants_seat_time ON one_seat_occupants(seat_id, since);
1455
+ CREATE INDEX idx_one_artifact_binding_chat
1456
+ ON one_artifact_bindings(chat_id, created_at);
1457
+ CREATE INDEX idx_one_artifact_binding_exact
1458
+ ON one_artifact_bindings(task_id, chat_id, run_id, manifest_id, artifact_ref);
1459
+ CREATE INDEX idx_one_org_members_agent
1460
+ ON one_org_members(installed_agent_id);
1461
+ CREATE INDEX idx_one_org_members_order
1462
+ ON one_org_members(archived_at, sort_order, added_at);
1463
+ CREATE INDEX idx_one_seats_updated ON one_seats(updated_at DESC);
1464
+ CREATE INDEX idx_one_taskforces_updated
1465
+ ON one_taskforces(updated_at DESC);
1466
+ CREATE INDEX idx_plugin_builder_sessions_chat_updated
1467
+ ON plugin_builder_sessions(chat_id, updated_at DESC);
1468
+ CREATE INDEX idx_plugin_builder_sessions_slug_phase
1469
+ ON plugin_builder_sessions(slug, phase);
1311
1470
  CREATE INDEX idx_run_events_agent_kind_ts
1312
1471
  ON run_events(agent_id, kind, ts DESC);
1313
1472
  CREATE INDEX idx_run_events_agent_ts ON run_events(agent_id, ts DESC);
@@ -1320,6 +1479,10 @@ CREATE INDEX idx_run_events_run_seq
1320
1479
  CREATE INDEX idx_run_events_ts
1321
1480
  ON run_events(ts DESC);
1322
1481
  CREATE INDEX idx_run_history_automation ON run_history(automation_id);
1482
+ CREATE INDEX idx_seat_occupants_agent
1483
+ ON one_seat_occupants(agent_id) WHERE agent_id IS NOT NULL;
1484
+ CREATE UNIQUE INDEX idx_seat_occupants_current
1485
+ ON one_seat_occupants(seat_id, slot) WHERE until IS NULL;
1323
1486
  CREATE INDEX idx_task_participants_agent ON task_agent_participants(agent_id);
1324
1487
  CREATE INDEX idx_tasks_firm_updated ON tasks(firm_id, updated_at DESC);
1325
1488
  CREATE INDEX idx_tasks_origin_chat ON tasks(origin_chat_id);
@@ -1337,7 +1500,8 @@ CREATE INDEX idx_telegram_bindings_chat
1337
1500
  ON telegram_bindings(telegram_chat_id);
1338
1501
  CREATE INDEX idx_telegram_bindings_enabled
1339
1502
  ON telegram_bindings(enabled, status);
1340
- CREATE UNIQUE INDEX idx_telegram_bindings_one_singleton
1341
- ON telegram_bindings(target_kind) WHERE target_kind = 'one';
1503
+ CREATE UNIQUE INDEX idx_telegram_bindings_one_room
1504
+ ON telegram_bindings(telegram_chat_id)
1505
+ WHERE target_kind = 'one' AND telegram_chat_id IS NOT NULL;
1342
1506
  CREATE INDEX idx_telegram_bindings_target
1343
1507
  ON telegram_bindings(target_kind, target_id);