agentlas 0.9.9 → 1.0.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.
Files changed (145) hide show
  1. package/CHANGELOG.md +92 -0
  2. package/README.md +319 -342
  3. package/bin/agentlas.cjs +32 -9
  4. package/engine/agentlas-banner.cjs +24 -3
  5. package/engine/agentlas-composer.cjs +239 -31
  6. package/engine/agentlas-config.cjs +103 -7
  7. package/engine/agentlas-core-harness.cjs +48 -4
  8. package/engine/agentlas-evolution.cjs +3 -4
  9. package/engine/agentlas-i18n.cjs +88 -32
  10. package/engine/agentlas-input.cjs +234 -18
  11. package/engine/agentlas-memory-governance.cjs +3 -5
  12. package/engine/agentlas-memory-import.cjs +3 -3
  13. package/engine/agentlas-native-host.cjs +200 -9
  14. package/engine/agentlas-onboard.cjs +112 -20
  15. package/engine/agentlas-permissions.cjs +14 -7
  16. package/engine/agentlas-sqlite-policy.cjs +34 -0
  17. package/engine/agentlas-ui.cjs +93 -42
  18. package/engine/agentlas-workforce.cjs +318 -8
  19. package/engine/agentlas-workload-routing.cjs +8 -3
  20. package/engine/agentlas.cjs +140 -12336
  21. package/engine/agents/files.cjs +61 -0
  22. package/engine/agents/import-local.cjs +237 -0
  23. package/engine/agents/registry.cjs +158 -0
  24. package/engine/agents/router.cjs +565 -0
  25. package/engine/agents/routes.cjs +43 -0
  26. package/engine/architecture.data.json +2 -2
  27. package/engine/automation/daemon.cjs +335 -0
  28. package/engine/automation/schedule.cjs +181 -0
  29. package/engine/automation/store.cjs +209 -0
  30. package/engine/cloud/auth.cjs +279 -0
  31. package/engine/cloud/hub-client.cjs +239 -0
  32. package/engine/cloud-assets/cargo.cjs +49 -0
  33. package/engine/cloud-assets/cas.cjs +235 -0
  34. package/engine/cloud-assets/commands.cjs +273 -0
  35. package/engine/cloud-assets/package.cjs +936 -0
  36. package/engine/cloud-assets/restore.cjs +172 -0
  37. package/engine/cloud-assets/state.cjs +268 -0
  38. package/engine/commands/automation.cjs +195 -0
  39. package/engine/commands/billing.cjs +96 -0
  40. package/engine/commands/browser.cjs +20 -0
  41. package/engine/commands/build.cjs +33 -0
  42. package/engine/commands/call.cjs +24 -0
  43. package/engine/commands/career-graph.cjs +51 -0
  44. package/engine/commands/cd.cjs +22 -0
  45. package/engine/commands/chat.cjs +12 -0
  46. package/engine/commands/chats.cjs +28 -0
  47. package/engine/commands/cloud.cjs +17 -0
  48. package/engine/commands/connect.cjs +20 -0
  49. package/engine/commands/context.cjs +66 -0
  50. package/engine/commands/creds.cjs +203 -0
  51. package/engine/commands/doctor.cjs +68 -0
  52. package/engine/commands/env.cjs +33 -0
  53. package/engine/commands/evolve.cjs +23 -0
  54. package/engine/commands/experience.cjs +34 -0
  55. package/engine/commands/film.cjs +8 -0
  56. package/engine/commands/firm.cjs +115 -0
  57. package/engine/commands/help.cjs +65 -0
  58. package/engine/commands/hep.cjs +23 -0
  59. package/engine/commands/import.cjs +35 -0
  60. package/engine/commands/index.cjs +136 -0
  61. package/engine/commands/install.cjs +27 -0
  62. package/engine/commands/journal.cjs +31 -0
  63. package/engine/commands/legacy-network.cjs +29 -0
  64. package/engine/commands/list.cjs +49 -0
  65. package/engine/commands/login.cjs +68 -0
  66. package/engine/commands/logout.cjs +28 -0
  67. package/engine/commands/mcp.cjs +91 -0
  68. package/engine/commands/memory.cjs +21 -0
  69. package/engine/commands/multimodal.cjs +91 -0
  70. package/engine/commands/native.cjs +34 -0
  71. package/engine/commands/netadmin.cjs +31 -0
  72. package/engine/commands/oberon.cjs +70 -0
  73. package/engine/commands/ontology.cjs +24 -0
  74. package/engine/commands/open.cjs +48 -0
  75. package/engine/commands/plugin.cjs +101 -0
  76. package/engine/commands/project.cjs +47 -0
  77. package/engine/commands/research.cjs +36 -0
  78. package/engine/commands/route.cjs +37 -0
  79. package/engine/commands/run.cjs +149 -0
  80. package/engine/commands/search.cjs +55 -0
  81. package/engine/commands/setup.cjs +45 -0
  82. package/engine/commands/storm.cjs +75 -0
  83. package/engine/commands/swarm.cjs +75 -0
  84. package/engine/commands/telegram.cjs +32 -0
  85. package/engine/commands/uninstall.cjs +68 -0
  86. package/engine/commands/update.cjs +54 -0
  87. package/engine/commands/upload.cjs +18 -0
  88. package/engine/commands/usage.cjs +35 -0
  89. package/engine/commands/variant.cjs +25 -0
  90. package/engine/commands/version.cjs +9 -0
  91. package/engine/commands/whoami.cjs +38 -0
  92. package/engine/commands/workforce.cjs +103 -0
  93. package/engine/core/db.cjs +160 -0
  94. package/engine/core/paths.cjs +35 -0
  95. package/engine/experience/build.cjs +181 -0
  96. package/engine/experience/intents.cjs +492 -0
  97. package/engine/experience/runtime.cjs +242 -0
  98. package/engine/experience/variant.cjs +196 -0
  99. package/engine/firms/orchestrate.cjs +333 -0
  100. package/engine/hephaestus/runtime.cjs +697 -0
  101. package/engine/hub/install.cjs +872 -0
  102. package/engine/hub/plugins.cjs +213 -0
  103. package/engine/mcp/consent.cjs +289 -0
  104. package/engine/mcp/contract.cjs +202 -0
  105. package/engine/mcp/index.cjs +43 -0
  106. package/engine/mcp/inventory.cjs +322 -0
  107. package/engine/mcp/plan.cjs +286 -0
  108. package/engine/mcp/probe.cjs +151 -0
  109. package/engine/memory-cli/curate.cjs +163 -0
  110. package/engine/oberon/common.cjs +69 -0
  111. package/engine/oberon/manifest.cjs +164 -0
  112. package/engine/oberon/outputs.cjs +70 -0
  113. package/engine/oberon/render.cjs +164 -0
  114. package/engine/project/career-graph.cjs +249 -0
  115. package/engine/project/credentials.cjs +262 -0
  116. package/engine/project/env-file.cjs +46 -0
  117. package/engine/project/index.cjs +27 -0
  118. package/engine/project/memory-context.cjs +453 -0
  119. package/engine/project/ontology.cjs +467 -0
  120. package/engine/project/paths.cjs +39 -0
  121. package/engine/project/seed.cjs +200 -0
  122. package/engine/project/state.cjs +403 -0
  123. package/engine/project/super-ontology-seed.json +3288 -0
  124. package/engine/runtimes/detect.cjs +54 -0
  125. package/engine/runtimes/overrides.cjs +139 -0
  126. package/engine/runtimes/resolve.cjs +64 -0
  127. package/engine/sessions/apply-fences.cjs +188 -0
  128. package/engine/sessions/fences.cjs +362 -0
  129. package/engine/sessions/orchestrator.cjs +170 -0
  130. package/engine/sessions/prompt.cjs +212 -0
  131. package/engine/sessions/session.cjs +245 -0
  132. package/engine/sessions/sink.cjs +54 -0
  133. package/engine/sessions/store.cjs +79 -0
  134. package/engine/storm/deps.cjs +88 -0
  135. package/engine/storm/storm.cjs +218 -0
  136. package/engine/storm/swarm.cjs +422 -0
  137. package/engine/ui/palette.cjs +105 -0
  138. package/engine/ui/renderer.cjs +85 -0
  139. package/engine/ui/repl.cjs +444 -0
  140. package/engine/workforce/capture.cjs +701 -0
  141. package/engine/workforce/deps.cjs +472 -0
  142. package/package.json +3 -7
  143. package/engine/agentlas-experience-mcp.cjs +0 -1709
  144. package/engine/agentlas-parity.cjs +0 -1499
  145. package/engine/agentlas-repl.cjs +0 -1780
@@ -0,0 +1,936 @@
1
+ "use strict";
2
+ /*
3
+ * cloud-assets/package — 폴더 스캔 → 패키지 번들 → (dry-run이 아니면) CAS 등록.
4
+ *
5
+ * v1 monolith "Agentlas Cloud packaging" 절의 충실 이식. 핵심 계약(약화 금지):
6
+ * - 패키징/보안 리뷰는 전부 로컬에서 돈다. Agent Cloud에는 패키지 데이터·해시·
7
+ * 로컬 리뷰 증적만 올라간다 (플랫폼 LLM 호출 없음).
8
+ * - 시크릿 발견 = blocker = 등록 fetch 0회. (agentlas-secret-patterns 공유 모듈 +
9
+ * 이 파일의 구조적 credential 검사 + 파일명 차단 목록의 3중 게이트)
10
+ * - 파일 읽기는 no-follow + 전/후 fstat 대조 — 스캔 중 바꿔치기(symlink swap,
11
+ * append)는 전부 blocker다. TOCTOU로 패키지에 외부 파일이 새는 것을 막는다.
12
+ * - .agentlas 로컬 상태(경험 계보 experience-relations.jsonl 계열, CAS 마커,
13
+ * cloud-asset-state)는 절대 업로드되지 않고 베이스 패키지 해시도 흔들지 않는다.
14
+ * - 등록은 관측한 베이스 리비전(If-Match) 또는 명시적 새 생성(If-None-Match: "*")
15
+ * 으로만 — 조용한 덮어쓰기 금지 (cas.cjs).
16
+ */
17
+ const fs = require("node:fs");
18
+ const os = require("node:os");
19
+ const path = require("node:path");
20
+ const {
21
+ CLOUD_MAX_TOTAL_BYTES,
22
+ CLOUD_MAX_FILE_BYTES,
23
+ CLOUD_MAX_FILES,
24
+ CLOUD_PACKAGE_HASH_V1,
25
+ CLOUD_PACKAGE_HASH_V2,
26
+ CLOUD_RESTORE_MARKER_PATH,
27
+ cloudSlug,
28
+ sha,
29
+ cloudCodePointPathOrder,
30
+ cloudIsLocalExperienceLineagePath,
31
+ cloudPortablePathKey,
32
+ cloudPortableRelativePath,
33
+ cloudPortablePathConflict,
34
+ cloudPackageHashVersion,
35
+ cloudHashPackage,
36
+ } = require("../hub/install.cjs");
37
+ const { SECRET_PATTERNS } = require("../agentlas-secret-patterns.cjs");
38
+ const { userDataDir } = require("../core/paths.cjs");
39
+ const state = require("./state.cjs");
40
+ const cas = require("./cas.cjs");
41
+
42
+ const CLOUD_TEXT_EXTS = new Set([".cfg", ".cjs", ".conf", ".config", ".css", ".csv", ".env", ".html", ".ini", ".js", ".json", ".jsonl", ".md", ".mjs", ".properties", ".ps1", ".psd1", ".psm1", ".py", ".sh", ".toml", ".ts", ".tsx", ".txt", ".xml", ".yaml", ".yml"]);
43
+ const CLOUD_AGENT_FILES = new Set(["AGENT.md", "AGENTS.md", "CLAUDE.md", "GEMINI.md", "README.md", "agent.md", "manifest.md", "system-prompt.md"]);
44
+ const CLOUD_SKIP_DIRS = new Set([".git", ".next", ".studio-runtime", ".turbo", "build", "coverage", "dist", "node_modules", "out", "release"]);
45
+ 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];
46
+ const CLOUD_ROUTING_CARD_PATH = ".agentlas/routing-card.json";
47
+ const CLOUD_ROUTING_CARD_CAPABILITY_RE = /^[a-z][a-z0-9]*(_[a-z0-9]+)+$/;
48
+ const CLOUD_ROUTING_CARD_STATUSES = new Set(["draft", "searchable", "candidate", "routing_ready", "trusted"]);
49
+ const CLOUD_SECRET_RE = [
50
+ ["private-key", /-----BEGIN (?:RSA |EC |OPENSSH |DSA )?PRIVATE KEY-----/i, "private key material"],
51
+ ["openai-key", /\bsk-[A-Za-z0-9_-]{20,}\b/, "OpenAI-style API key"],
52
+ ["github-token", /\bgh[pousr]_[A-Za-z0-9_]{30,}\b/, "GitHub token"],
53
+ ["gitlab-token", /\bglpat-[A-Za-z0-9_-]{20,}\b/, "GitLab token"],
54
+ ["google-api-key", /\bAIza[0-9A-Za-z_-]{35}\b/, "Google API key"],
55
+ ["npm-token", /\bnpm_[A-Za-z0-9]{30,}\b/, "npm access token"],
56
+ ["stripe-secret", /\bsk_(?:live|test)_[A-Za-z0-9]{16,}\b/, "Stripe secret key"],
57
+ ["slack-token", /\bxox[baprs]-[A-Za-z0-9-]{20,}\b/, "Slack token"],
58
+ ["aws-key", /\bAKIA[0-9A-Z]{16}\b/, "AWS access key"],
59
+ ["generic-secret", /\b(?:api[_-]?key|secret|token|password)\s*[:=]\s*['"][^'"]{8,}['"]/i, "hard-coded credential"],
60
+ ];
61
+
62
+ // ── 텍스트/credential 디코딩 ──
63
+
64
+ function cloudDecodeUtf16CredentialText(bytes) {
65
+ if (bytes.length < 4) return null;
66
+ if (bytes[0] === 0xff && bytes[1] === 0xfe) {
67
+ return bytes.subarray(2, bytes.length - ((bytes.length - 2) % 2)).toString("utf16le");
68
+ }
69
+ if (bytes[0] === 0xfe && bytes[1] === 0xff) {
70
+ const body = Buffer.from(bytes.subarray(2, bytes.length - ((bytes.length - 2) % 2)));
71
+ body.swap16();
72
+ return body.toString("utf16le");
73
+ }
74
+ // BOM 없는 UTF-16 휴리스틱 — 짝수/홀수 바이트 NUL 비율로 엔디언 추정.
75
+ const sampleLength = Math.min(bytes.length - (bytes.length % 2), 4096);
76
+ if (sampleLength < 8) return null;
77
+ let oddNuls = 0;
78
+ let evenNuls = 0;
79
+ for (let index = 0; index < sampleLength; index += 2) {
80
+ if (bytes[index] === 0) evenNuls++;
81
+ if (bytes[index + 1] === 0) oddNuls++;
82
+ }
83
+ const pairs = sampleLength / 2;
84
+ const fullLength = bytes.length - (bytes.length % 2);
85
+ if (oddNuls / pairs > 0.3) return bytes.subarray(0, fullLength).toString("utf16le");
86
+ if (evenNuls / pairs > 0.3) {
87
+ const body = Buffer.from(bytes.subarray(0, fullLength));
88
+ body.swap16();
89
+ return body.toString("utf16le");
90
+ }
91
+ return null;
92
+ }
93
+
94
+ function cloudDecodeTextAsset(bytes) {
95
+ const utf16 = cloudDecodeUtf16CredentialText(bytes);
96
+ if (utf16 !== null) return { ok: true, text: utf16 };
97
+ try {
98
+ return { ok: true, text: new TextDecoder("utf-8", { fatal: true }).decode(bytes) };
99
+ } catch {
100
+ return { ok: false };
101
+ }
102
+ }
103
+
104
+ /** 값이 진짜 credential처럼 보이는가 — 플레이스홀더(${KEY}, changeme 등)는 통과시킨다. */
105
+ function cloudCredentialValueLooksReal(rawValue) {
106
+ let value = String(rawValue || "").trim().replace(/^['"]|['"]$/g, "").trim();
107
+ try { value = decodeURIComponent(value); } catch { /* keep raw */ }
108
+ if (value.length < 8) return false;
109
+ if (/^(?:\$\{[^}]+\}|\$[A-Z_][A-Z0-9_]*|\{\{[^}]+\}\}|<[^>]+>)$/i.test(value)) return false;
110
+ if (/^(?:process\.env\.|os\.environ|env\(|secret\(|vault:)/i.test(value)) return false;
111
+ const compact = value.toLowerCase().replace(/[^a-z0-9]+/g, "");
112
+ if (/^(?:your|example|sample|dummy|placeholder|configure|configureonthismachine|changeme|replaceme|replacewith|redacted|masked|notareal|none|null|undefined|x+|star+)(?:api)?(?:key|secret|token|password)?(?:here)?$/.test(compact)) return false;
113
+ if (/^(?:\*+|x+|_+|-+)$/.test(value)) return false;
114
+ return true;
115
+ }
116
+
117
+ function cloudTextContainsStructuredCredential(text) {
118
+ const assignment = /(?:^|\n)\s*["']?(?:api[_-]?key|access[_-]?token|auth[_-]?token|client[_-]?secret|secret|token|password|passwd|pwd)["']?\s*[:=]\s*([^\r\n#;]+)/gi;
119
+ for (const match of text.matchAll(assignment)) {
120
+ if (cloudCredentialValueLooksReal(match[1])) return true;
121
+ }
122
+ const urlCredential = /\bhttps?:\/\/[^/\s:@]+:([^@\s/]{8,})@/gi;
123
+ for (const match of text.matchAll(urlCredential)) {
124
+ if (cloudCredentialValueLooksReal(match[1])) return true;
125
+ }
126
+ const queryCredential = /[?&](?:api[_-]?key|access[_-]?token|auth[_-]?token|client[_-]?secret|secret|token|password)=([^&#\s]+)/gi;
127
+ for (const match of text.matchAll(queryCredential)) {
128
+ if (cloudCredentialValueLooksReal(match[1])) return true;
129
+ }
130
+ return false;
131
+ }
132
+
133
+ function cloudAddSecretFindingsFromBytes(bytes, relativePath, addFinding) {
134
+ const candidates = new Set([bytes.toString("utf8")]);
135
+ const utf16 = cloudDecodeUtf16CredentialText(bytes);
136
+ if (utf16) candidates.add(utf16);
137
+ for (const text of candidates) {
138
+ for (const [id, re, label] of CLOUD_SECRET_RE) {
139
+ if (re.test(text)) addFinding(id, "blocker", "secret", `Possible ${label} found in package content.`, relativePath, "Remove the value and require users to configure their own key.");
140
+ }
141
+ if (cloudTextContainsStructuredCredential(text)) {
142
+ addFinding("generic-unquoted-secret", "blocker", "secret", "Possible unquoted or URL-embedded credential found in package content.", relativePath, "Replace the value with an environment/BYOK placeholder.");
143
+ }
144
+ // 공유 시크릿 패턴(agentlas-secret-patterns)도 같은 게이트에 태운다.
145
+ // 단, 할당형(password: …, authorization: …) 매치는 값이 진짜처럼 보일 때만
146
+ // blocker로 승격한다 — "password: configure_on_this_machine" 같은 플레이스홀더를
147
+ // 오탐으로 막으면 패키징 게이트 자체가 불신받는다(content_guard 오탐 사고 계열).
148
+ let sharedPatternHit = false;
149
+ for (const re of SECRET_PATTERNS) {
150
+ // 패턴에 g 플래그가 없으므로 전역 사본으로 모든 매치를 훑는다 — 첫 매치가
151
+ // 플레이스홀더라고 같은 파일의 두 번째 진짜 키를 놓치면 안 된다.
152
+ const globalRe = new RegExp(re.source, re.flags.includes("g") ? re.flags : `${re.flags}g`);
153
+ for (const match of text.matchAll(globalRe)) {
154
+ const matched = String(match[0] || "");
155
+ const assignmentSplit = matched.match(/^[^:=]{0,40}[:=]\s*(.+)$/s);
156
+ if (assignmentSplit && !cloudCredentialValueLooksReal(assignmentSplit[1])) continue;
157
+ addFinding("shared-secret-pattern", "blocker", "secret", "Possible live credential (shared secret-pattern match) found in package content.", relativePath, "Remove the value and require users to configure their own key.");
158
+ sharedPatternHit = true;
159
+ break;
160
+ }
161
+ if (sharedPatternHit) break;
162
+ }
163
+ }
164
+ }
165
+
166
+ // ── 스냅샷 읽기 도우미 ──
167
+
168
+ function cloudPackageSnapshot(files) {
169
+ return new Map(files.map((file) => [file.path, file]));
170
+ }
171
+
172
+ function cloudReadSnapshotText(snapshot, relativePath) {
173
+ const file = snapshot.get(relativePath);
174
+ return file ? Buffer.from(file.contentBase64, "base64").toString("utf8") : "";
175
+ }
176
+
177
+ function cloudReadSnapshotJson(snapshot, relativePath) {
178
+ try {
179
+ const parsed = JSON.parse(cloudReadSnapshotText(snapshot, relativePath));
180
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
181
+ } catch { return {}; }
182
+ }
183
+
184
+ function cloudReadPackageJson(snapshot) {
185
+ return {
186
+ agentlas: cloudReadSnapshotJson(snapshot, "agentlas.json"),
187
+ manifest: cloudReadSnapshotJson(snapshot, "manifest.json"),
188
+ agentCard: cloudReadSnapshotJson(snapshot, ".agentlas/agent-card.json"),
189
+ routingCard: cloudReadSnapshotJson(snapshot, ".agentlas/routing-card.json"),
190
+ };
191
+ }
192
+
193
+ function stringFirst(...values) {
194
+ for (const value of values) {
195
+ if (typeof value === "string" && value.trim()) return value.trim();
196
+ }
197
+ return "";
198
+ }
199
+
200
+ function cloudReadFirst(snapshot, names, maxChars) {
201
+ for (const name of names) {
202
+ const text = cloudReadSnapshotText(snapshot, name);
203
+ if (text) return text.slice(0, maxChars);
204
+ }
205
+ return "";
206
+ }
207
+
208
+ function cloudReadName(snapshot, fallbackName) {
209
+ const manifest = cloudReadPackageJson(snapshot);
210
+ const explicit = stringFirst(
211
+ manifest.agentlas?.displayName,
212
+ manifest.agentlas?.name,
213
+ manifest.manifest?.name,
214
+ manifest.agentCard?.name,
215
+ manifest.routingCard?.name,
216
+ );
217
+ if (explicit) return explicit.replace(/\s+/g, " ").trim().slice(0, 80);
218
+ const text = cloudReadFirst(snapshot, ["agent.md", "AGENT.md", "README.md", "CLAUDE.md", "AGENTS.md"], 2000);
219
+ const heading = text.match(/^#\s+(.+)$/m);
220
+ return (heading ? heading[1] : fallbackName).replace(/\s+/g, " ").trim().slice(0, 80);
221
+ }
222
+
223
+ function cloudReadTagline(snapshot) {
224
+ const manifest = cloudReadPackageJson(snapshot);
225
+ const explicit = stringFirst(
226
+ manifest.agentlas?.summary,
227
+ manifest.agentlas?.description,
228
+ manifest.manifest?.description,
229
+ manifest.agentCard?.summary,
230
+ manifest.routingCard?.summary,
231
+ );
232
+ if (explicit) return explicit.replace(/\s+/g, " ").trim().slice(0, 160);
233
+ const text = cloudReadFirst(snapshot, ["README.md", "agent.md", "AGENT.md"], 3000);
234
+ for (const line of text.split(/\r?\n/)) {
235
+ const t = line.trim();
236
+ if (t && !t.startsWith("#") && !t.startsWith(">")) return t.slice(0, 160);
237
+ }
238
+ return "Portable Agentlas cloud agent package.";
239
+ }
240
+
241
+ /*
242
+ * 공개 Hub 발행 이중 언어 메타데이터 게이트 — 데스크탑 package.ts:435-449 동형.
243
+ * 데스크탑은 게이트 전에 연결된 모델로 자동 번역을 시도하지만(package.ts:428-434),
244
+ * v2 터미널은 로컬 런타임 리뷰 계층이 아직 미배선이라(--llm-review 정직 정지와
245
+ * 동일 계열) 번역 없이 게이트만 적용한다 — 조용한 무검증 발행보다 정직한 차단.
246
+ */
247
+ function cloudCleanLocalizedField(value, max) {
248
+ return typeof value === "string"
249
+ ? value.normalize("NFKC").replace(/\s+/g, " ").trim().slice(0, max).trim()
250
+ : "";
251
+ }
252
+
253
+ function cloudNormalizeLocalizedListing(value) {
254
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
255
+ const localized = {
256
+ titleEn: cloudCleanLocalizedField(value.titleEn, 96),
257
+ titleKo: cloudCleanLocalizedField(value.titleKo, 96),
258
+ descriptionEn: cloudCleanLocalizedField(value.descriptionEn, 640),
259
+ descriptionKo: cloudCleanLocalizedField(value.descriptionKo, 640),
260
+ };
261
+ return Object.values(localized).some(Boolean) ? localized : undefined;
262
+ }
263
+
264
+ function cloudReadLocalizedListing(snapshot) {
265
+ const manifest = cloudReadPackageJson(snapshot);
266
+ for (const source of [manifest.agentCard, manifest.agentlas, manifest.manifest, manifest.routingCard]) {
267
+ const nested = cloudNormalizeLocalizedListing(source && source.localized);
268
+ if (nested) return nested;
269
+ const flat = cloudNormalizeLocalizedListing(source);
270
+ if (flat) return flat;
271
+ }
272
+ return undefined;
273
+ }
274
+
275
+ // 데스크탑 localizedListingProblems(package.ts:1777-1794) 토씨 동일.
276
+ function cloudLocalizedListingProblems(value) {
277
+ if (!value) return ["localized object missing"];
278
+ const issues = [];
279
+ if (!value.titleEn) issues.push("titleEn missing");
280
+ if (!value.titleKo) issues.push("titleKo missing");
281
+ if (!value.descriptionEn) issues.push("descriptionEn missing");
282
+ if (!value.descriptionKo) issues.push("descriptionKo missing");
283
+ if (/[가-힣]/.test(value.titleEn)) issues.push("titleEn contains Hangul");
284
+ if (/[가-힣]/.test(value.descriptionEn)) issues.push("descriptionEn contains Hangul");
285
+ if (
286
+ value.descriptionEn
287
+ && value.descriptionEn === value.descriptionKo
288
+ && /[가-힣]/.test(value.descriptionKo)
289
+ ) {
290
+ issues.push("English description is not translated");
291
+ }
292
+ return issues;
293
+ }
294
+
295
+ function cloudReadStableSlug(snapshot) {
296
+ const manifest = cloudReadPackageJson(snapshot);
297
+ return stringFirst(
298
+ manifest.agentlas?.slug,
299
+ manifest.agentlas?.id,
300
+ manifest.manifest?.package,
301
+ manifest.manifest?.slug,
302
+ manifest.agentCard?.slug,
303
+ manifest.agentCard?.id,
304
+ manifest.routingCard?.agent_card_ref?.slug,
305
+ );
306
+ }
307
+
308
+ function cloudInferKind(snapshot) {
309
+ const paths = [...snapshot.keys()];
310
+ if (paths.some((file) => file === "TEAM.md" || file === "team.json" || /^(?:agents|team|departments|hr-departments)\//.test(file))) return "team";
311
+ return "agent";
312
+ }
313
+
314
+ function cloudDetectRuntimeLabels(snapshot) {
315
+ const paths = new Set(snapshot.keys());
316
+ const labels = [];
317
+ if (paths.has("CLAUDE.md") || [...paths].some((file) => file.startsWith(".claude/"))) labels.push("claude-code");
318
+ if (paths.has("AGENTS.md")) labels.push("codex");
319
+ if (paths.has("GEMINI.md")) labels.push("gemini");
320
+ if (paths.has(".cursorrules") || [...paths].some((file) => file.startsWith(".cursor/"))) labels.push("cursor");
321
+ return labels.length ? labels : ["generic"];
322
+ }
323
+
324
+ function cloudPackageDir(slug) {
325
+ const stamp = new Date().toISOString().replace(/[:.]/g, "-");
326
+ return path.join(userDataDir(), "cloud-agent-packages", `${slug}-${stamp}`);
327
+ }
328
+
329
+ // ── Windows 실행 비트 복원 ──
330
+ // Windows에는 모드 비트가 없으므로, 마지막 restore 마커의 executablePaths가
331
+ // 포터블 실행 비트의 진실이다. 이게 없으면 win32 재저장이 실행 비트를 전부
332
+ // 잃어 패키지 해시(v2)가 바뀐다.
333
+ function cloudReadRestoreExecutablePaths(rootPath) {
334
+ if (process.platform !== "win32") return new Set();
335
+ const marker = path.join(rootPath, CLOUD_RESTORE_MARKER_PATH);
336
+ try {
337
+ const parsed = JSON.parse(fs.readFileSync(marker, "utf8"));
338
+ if (cloudPackageHashVersion(parsed.packageHashVersion) !== CLOUD_PACKAGE_HASH_V2) return new Set();
339
+ if (!Array.isArray(parsed.executablePaths)) return new Set();
340
+ return new Set(parsed.executablePaths
341
+ .filter((value) => cloudPortableRelativePath(value))
342
+ .map((value) => cloudPortablePathKey(value)));
343
+ } catch {
344
+ return new Set();
345
+ }
346
+ }
347
+
348
+ function cloudPortableExecutableForFile(relativePath, statMode, restoredExecutablePaths, platform = process.platform) {
349
+ if (platform === "win32") return restoredExecutablePaths.has(cloudPortablePathKey(relativePath));
350
+ return Boolean(statMode & 0o111);
351
+ }
352
+
353
+ // ── 폴더 스캔 (TOCTOU-안전) ──
354
+
355
+ function scanCloudFolder(rootPath) {
356
+ const files = [];
357
+ const included = [];
358
+ const findings = [];
359
+ const restoredExecutablePaths = cloudReadRestoreExecutablePaths(rootPath);
360
+ let localPackageMarker = null;
361
+ let totalBytes = 0;
362
+ let count = 0;
363
+ let hasDefinition = false;
364
+ function addFinding(kind, severity, category, message, file, remediation) {
365
+ findings.push({ id: `${kind}-${sha(file || message).slice(0, 10)}`, severity, category, message, ...(file ? { file } : {}), ...(remediation ? { remediation } : {}) });
366
+ }
367
+ function insideRoot(candidate) {
368
+ const relative = path.relative(rootPath, candidate);
369
+ return relative === "" || (relative && !relative.startsWith("..") && !path.isAbsolute(relative));
370
+ }
371
+ // no-follow open + 전/후 fstat/realpath 대조: 읽는 동안 파일이 바뀌면(스왑·append)
372
+ // 무조건 실패한다. 캡처한 바이트와 디스크 상태가 다르면 패키지에 넣지 않는다.
373
+ function readStableFile(file, rel) {
374
+ const beforeReal = fs.realpathSync.native(file);
375
+ if (!insideRoot(beforeReal)) throw new Error("file resolves outside the approved package root");
376
+ const noFollow = fs.constants.O_NOFOLLOW || 0;
377
+ const nonBlock = fs.constants.O_NONBLOCK || 0;
378
+ const fd = fs.openSync(file, fs.constants.O_RDONLY | noFollow | nonBlock);
379
+ try {
380
+ const before = fs.fstatSync(fd);
381
+ if (!before.isFile()) throw new Error("package entry is not a regular file");
382
+ if (before.size > CLOUD_MAX_FILE_BYTES) throw new Error(`file exceeds ${CLOUD_MAX_FILE_BYTES} bytes`);
383
+ const chunks = [];
384
+ let actualBytes = 0;
385
+ for (;;) {
386
+ const capacity = Math.min(64 * 1024, CLOUD_MAX_FILE_BYTES + 1 - actualBytes);
387
+ if (capacity <= 0) throw new Error(`file exceeds ${CLOUD_MAX_FILE_BYTES} bytes`);
388
+ const chunk = Buffer.allocUnsafe(capacity);
389
+ const read = fs.readSync(fd, chunk, 0, chunk.length, null);
390
+ if (read === 0) break;
391
+ actualBytes += read;
392
+ if (actualBytes > CLOUD_MAX_FILE_BYTES) throw new Error(`file exceeds ${CLOUD_MAX_FILE_BYTES} bytes`);
393
+ chunks.push(chunk.subarray(0, read));
394
+ }
395
+ const after = fs.fstatSync(fd);
396
+ const afterReal = fs.realpathSync.native(file);
397
+ const pathStat = fs.statSync(file);
398
+ if (
399
+ !insideRoot(afterReal) || beforeReal !== afterReal ||
400
+ before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size ||
401
+ before.mode !== after.mode || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs ||
402
+ after.dev !== pathStat.dev || after.ino !== pathStat.ino || after.mode !== pathStat.mode ||
403
+ actualBytes !== after.size
404
+ ) {
405
+ throw new Error("package entry changed while it was being read");
406
+ }
407
+ return {
408
+ bytes: Buffer.concat(chunks, actualBytes),
409
+ executable: cloudPortableExecutableForFile(rel, after.mode, restoredExecutablePaths),
410
+ };
411
+ } finally {
412
+ fs.closeSync(fd);
413
+ }
414
+ }
415
+ function walk(dir) {
416
+ let directoryBefore;
417
+ let directoryRealBefore;
418
+ try {
419
+ directoryBefore = fs.lstatSync(dir);
420
+ directoryRealBefore = fs.realpathSync.native(dir);
421
+ if (!directoryBefore.isDirectory() || directoryBefore.isSymbolicLink() || !insideRoot(directoryRealBefore)) {
422
+ throw new Error("directory is not stable inside the approved root");
423
+ }
424
+ } catch (error) {
425
+ addFinding("unsafe-directory", "blocker", "policy", `Package directory could not be read safely: ${error.message || error}`, path.relative(rootPath, dir).split(path.sep).join("/"), "Remove linked or changing directories and retry.");
426
+ return;
427
+ }
428
+ let entries;
429
+ try {
430
+ entries = fs.readdirSync(dir, { withFileTypes: true });
431
+ } catch (error) {
432
+ addFinding("unsafe-directory", "blocker", "policy", `Package directory could not be read safely: ${error.message || error}`, path.relative(rootPath, dir).split(path.sep).join("/"), "Remove linked or changing directories and retry.");
433
+ return;
434
+ }
435
+ for (const entry of entries) {
436
+ if (entry.name.startsWith("._")) continue;
437
+ const abs = path.join(dir, entry.name);
438
+ const rel = path.relative(rootPath, abs).split(path.sep).join("/");
439
+ if (cloudIsLocalExperienceLineagePath(rel)) {
440
+ // 경험 계보는 로컬에서 재구축 가능한 별도 자산 — 절대 업로드되지 않고
441
+ // 베이스 패키지 해시에도 참여하지 않는다.
442
+ let bytes = 0;
443
+ try { bytes = Number(fs.lstatSync(abs).size) || 0; } catch { /* excluded local state */ }
444
+ files.push({ path: rel, bytes, sha256: "", kind: "text", included: false, reason: "experience-lineage-separate-asset" });
445
+ continue;
446
+ }
447
+ if (cloudPortablePathKey(rel) === cloudPortablePathKey(CLOUD_RESTORE_MARKER_PATH)) {
448
+ // 로컬 restore/CAS 메타데이터는 런타임 상태다 — 포터블 자산이 아니지만
449
+ // 같은 no-follow 안정성 게이트로 캡처해 베이스 리비전으로 쓴다.
450
+ if (entry.isSymbolicLink() || !entry.isFile()) {
451
+ addFinding("unsafe-local-state", "blocker", "policy", "Agent Cloud local revision marker must be a stable regular file.", rel, "Remove the linked or special marker and restore/list the asset again.");
452
+ continue;
453
+ }
454
+ try {
455
+ const stableMarker = readStableFile(abs, rel);
456
+ localPackageMarker = JSON.parse(stableMarker.bytes.toString("utf8"));
457
+ } catch (error) {
458
+ addFinding("invalid-local-state", "blocker", "policy", `Agent Cloud local revision marker could not be read safely: ${error.message || error}`, rel, "Repair or remove the marker, then restore/list the asset again.");
459
+ }
460
+ continue;
461
+ }
462
+ if (entry.isSymbolicLink()) {
463
+ addFinding("symlink", "blocker", "policy", "Symbolic links are not allowed in cloud agent packages.", rel, "Replace the symlink with an ordinary file or remove it.");
464
+ files.push({ path: rel, bytes: 0, sha256: "", kind: "binary", included: false, reason: "symlink-blocked" });
465
+ continue;
466
+ }
467
+ if (entry.isDirectory()) {
468
+ if (CLOUD_SKIP_DIRS.has(entry.name)) continue;
469
+ walk(abs);
470
+ continue;
471
+ }
472
+ if (!entry.isFile()) {
473
+ addFinding("unsupported-entry", "blocker", "policy", "Only stable ordinary files and directories are allowed in Cloud packages.", rel, "Remove sockets, FIFOs, devices, and other special filesystem entries.");
474
+ files.push({ path: rel, bytes: 0, sha256: "", kind: "binary", included: false, reason: "unsupported-entry" });
475
+ continue;
476
+ }
477
+ if (!cloudPortableRelativePath(rel)) {
478
+ addFinding("unsafe-path", "blocker", "policy", "File path is not portable across supported hosts.", rel, "Rename the file to a Unicode NFC, relative, cross-platform-safe path.");
479
+ files.push({ path: rel, bytes: 0, sha256: "", kind: "binary", included: false, reason: "unsafe-path" });
480
+ continue;
481
+ }
482
+ count++;
483
+ if (count > CLOUD_MAX_FILES) {
484
+ addFinding("file-count-limit", "blocker", "size", `Package has more than ${CLOUD_MAX_FILES} files.`, "", "Publish a focused agent/team folder.");
485
+ continue;
486
+ }
487
+ if (CLOUD_AGENT_FILES.has(entry.name)) hasDefinition = true;
488
+ let hint;
489
+ try { hint = fs.lstatSync(abs); } catch { hint = { size: 0 }; }
490
+ if (CLOUD_BLOCKED_FILE_RE.some((re) => re.test(entry.name))) {
491
+ addFinding("blocked-file", "blocker", "secret", "Secret-bearing file names are not allowed in cloud packages.", rel, "Remove credentials and publish only env key names.");
492
+ files.push({ path: rel, bytes: Number(hint.size) || 0, sha256: "", kind: "binary", included: false, reason: "secret-file-blocked" });
493
+ continue;
494
+ }
495
+ if (Number(hint.size) > CLOUD_MAX_FILE_BYTES) {
496
+ addFinding("large-file", "blocker", "size", `File exceeds ${CLOUD_MAX_FILE_BYTES} bytes.`, rel, "Move large assets out of the package.");
497
+ files.push({ path: rel, bytes: Number(hint.size), sha256: "", kind: "binary", included: false, reason: "file-too-large" });
498
+ continue;
499
+ }
500
+ const ext = path.extname(entry.name).toLowerCase();
501
+ const isText = CLOUD_TEXT_EXTS.has(ext) || CLOUD_AGENT_FILES.has(entry.name);
502
+ let stable;
503
+ try {
504
+ stable = readStableFile(abs, rel);
505
+ } catch (error) {
506
+ addFinding("unstable-file", "blocker", "policy", `Package file could not be read safely: ${error.message || error}`, rel, "Remove linked or concurrently changing files and retry.");
507
+ files.push({ path: rel, bytes: Number(hint.size) || 0, sha256: "", kind: isText ? "text" : "binary", included: false, reason: "unstable-file" });
508
+ continue;
509
+ }
510
+ const content = stable.bytes;
511
+ const executable = stable.executable;
512
+ totalBytes += content.length;
513
+ const digest = sha(content);
514
+ cloudAddSecretFindingsFromBytes(content, rel, addFinding);
515
+ if (isText) {
516
+ const decoded = cloudDecodeTextAsset(content);
517
+ if (!decoded.ok) {
518
+ addFinding("invalid-text-encoding", "blocker", "policy", "A text agent asset is not valid UTF-8 or BOM-marked UTF-16.", rel, "Save the file as UTF-8 or BOM-marked UTF-16 before packaging.");
519
+ files.push({ path: rel, bytes: content.length, sha256: digest, kind: "text", executable, included: false, reason: "invalid-text-encoding" });
520
+ continue;
521
+ }
522
+ const text = decoded.text;
523
+ if (/(?:curl|wget)[^\n|&;]+[|]\s*(?:sh|bash)/i.test(text)) {
524
+ addFinding("curl-pipe-shell", "high", "network", "Remote shell install pattern detected.", rel, "Use explicit, reviewable install steps.");
525
+ }
526
+ }
527
+ files.push({ path: rel, bytes: content.length, sha256: digest, kind: isText ? "text" : "binary", executable, included: true });
528
+ included.push({ path: rel, bytes: content.length, sha256: digest, executable, contentBase64: content.toString("base64") });
529
+ }
530
+ try {
531
+ const directoryAfter = fs.lstatSync(dir);
532
+ const directoryRealAfter = fs.realpathSync.native(dir);
533
+ if (
534
+ !directoryAfter.isDirectory() || directoryAfter.isSymbolicLink() || !insideRoot(directoryRealAfter) ||
535
+ directoryRealBefore !== directoryRealAfter || directoryBefore.dev !== directoryAfter.dev ||
536
+ directoryBefore.ino !== directoryAfter.ino || directoryBefore.mtimeMs !== directoryAfter.mtimeMs ||
537
+ directoryBefore.ctimeMs !== directoryAfter.ctimeMs
538
+ ) {
539
+ throw new Error("directory changed while it was scanned");
540
+ }
541
+ } catch (error) {
542
+ addFinding("unstable-directory", "blocker", "policy", `Package directory changed while it was scanned: ${error.message || error}`, path.relative(rootPath, dir).split(path.sep).join("/"), "Stop concurrent edits and retry.");
543
+ }
544
+ }
545
+ walk(rootPath);
546
+ const pathConflict = cloudPortablePathConflict(included.map((file) => file.path));
547
+ if (pathConflict) {
548
+ addFinding(pathConflict.code, "blocker", "policy", pathConflict.message, "", "Rename aliased paths so every file and ancestor directory has one portable identity.");
549
+ }
550
+ if (!hasDefinition) addFinding("missing-agent-definition", "blocker", "structure", "No agent definition file was found.", "", "Add AGENTS.md, CLAUDE.md, GEMINI.md, AGENT.md, or README.md at the package root.");
551
+ if (totalBytes > CLOUD_MAX_TOTAL_BYTES) addFinding("package-size-limit", "blocker", "size", `Package exceeds ${CLOUD_MAX_TOTAL_BYTES} bytes.`, "", "Publish a smaller agent folder.");
552
+ files.sort(cloudCodePointPathOrder);
553
+ included.sort(cloudCodePointPathOrder);
554
+ return { files, included, findings, totalBytes, localPackageMarker };
555
+ }
556
+
557
+ // ── 라우팅 카드 (공개 Hub 발행 전용 게이트) ──
558
+
559
+ function cloudRoutingCardFinding(id, message, remediation) {
560
+ return {
561
+ finding: {
562
+ id,
563
+ severity: "blocker",
564
+ category: "structure",
565
+ file: CLOUD_ROUTING_CARD_PATH,
566
+ message,
567
+ remediation,
568
+ },
569
+ };
570
+ }
571
+
572
+ function cloudRoutingCardProblem(card) {
573
+ if (card.schemaVersion !== "routing-card/2.0") return "schemaVersion must be routing-card/2.0";
574
+ if (typeof card.id !== "string" || !card.id.trim()) return "id must be a non-empty string";
575
+ if (card.type !== "agent" && card.type !== "team" && card.type !== "plugin") return "type must be agent, team, or plugin";
576
+ if (typeof card.name !== "string" || !card.name.trim()) return "name must be a non-empty string";
577
+ if (typeof card.summary !== "string" || !card.summary.trim()) return "summary must be a non-empty string";
578
+ if (!Array.isArray(card.capabilities) || card.capabilities.length === 0) return "capabilities must be a non-empty array";
579
+ for (const capability of card.capabilities) {
580
+ if (typeof capability !== "string" || !CLOUD_ROUTING_CARD_CAPABILITY_RE.test(capability)) {
581
+ return `capability ${JSON.stringify(capability)} must be snake_case with at least two words`;
582
+ }
583
+ }
584
+ if (typeof card.routing_status !== "string" || !CLOUD_ROUTING_CARD_STATUSES.has(card.routing_status)) {
585
+ return "routing_status must be draft, searchable, candidate, routing_ready, or trusted";
586
+ }
587
+ return null;
588
+ }
589
+
590
+ function readCloudRoutingCard(snapshot) {
591
+ const file = snapshot.get(CLOUD_ROUTING_CARD_PATH);
592
+ if (!file) {
593
+ return {
594
+ finding: {
595
+ id: "routing-card-required",
596
+ severity: "blocker",
597
+ category: "structure",
598
+ file: CLOUD_ROUTING_CARD_PATH,
599
+ message: "Cloud registration requires a Hephaestus Network routing card.",
600
+ remediation: "Add .agentlas/routing-card.json before publishing. In Hephaestus packages, run the routing-card migration or package verifier.",
601
+ },
602
+ };
603
+ }
604
+ try {
605
+ const parsed = JSON.parse(Buffer.from(file.contentBase64, "base64").toString("utf8"));
606
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
607
+ return cloudRoutingCardFinding("routing-card-invalid", "Routing card must be a JSON object.", "Replace .agentlas/routing-card.json with a routing-card/2.0 object.");
608
+ }
609
+ const problem = cloudRoutingCardProblem(parsed);
610
+ if (problem) {
611
+ return cloudRoutingCardFinding("routing-card-invalid", `Routing card is invalid: ${problem}`, "Fix .agentlas/routing-card.json before publishing.");
612
+ }
613
+ return { card: parsed };
614
+ } catch {
615
+ return cloudRoutingCardFinding("routing-card-invalid-json", "Routing card is not valid JSON.", "Fix .agentlas/routing-card.json before publishing.");
616
+ }
617
+ }
618
+
619
+ // ── Career Graph 공개 카드 (redact 후에만 발행 패키지에 들어간다) ──
620
+
621
+ function cloudCareerFinding(id, category, message) {
622
+ return {
623
+ id,
624
+ severity: "blocker",
625
+ category,
626
+ file: ".agentlas/public-career-card.json",
627
+ message,
628
+ remediation: "Regenerate a redacted aggregate-only public Career Graph card before publishing.",
629
+ };
630
+ }
631
+
632
+ function cloudContainsAbsoluteLocalPath(value) {
633
+ return (
634
+ (os.homedir() && value.includes(os.homedir())) ||
635
+ /(?:^|["'\s:(])\/(?:Users|home|var|tmp|private|Volumes|opt|etc)\//i.test(value) ||
636
+ /(?:^|["'\s:(])[A-Za-z]:[\\/]/.test(value) ||
637
+ /(?:^|["'\s:(])\\\\[^\\\s]+\\/.test(value)
638
+ );
639
+ }
640
+
641
+ function cloudSanitizeCountRecord(value) {
642
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
643
+ const result = {};
644
+ for (const [key, count] of Object.entries(value).slice(0, 200)) {
645
+ if (/^[A-Za-z0-9_.:-]{1,80}$/.test(key) && Number.isSafeInteger(count) && count >= 0) result[key] = count;
646
+ }
647
+ return Object.keys(result).length ? result : undefined;
648
+ }
649
+
650
+ function cloudSanitizePublicCareerCard(parsed) {
651
+ const card = { kind: "agentlas-public-career-card" };
652
+ for (const [key, max] of [["schemaVersion", 80], ["generatedAt", 80], ["projectName", 200], ["indexStatus", 80], ["policy", 160]]) {
653
+ if (typeof parsed[key] === "string" && parsed[key].length <= max) card[key] = parsed[key];
654
+ }
655
+ card.privacy = {
656
+ rawLocalPathsIncluded: false,
657
+ rawPromptsIncluded: false,
658
+ rawTranscriptsIncluded: false,
659
+ sourceTextIncluded: false,
660
+ };
661
+ for (const key of ["counts", "sourceKinds", "nodeTypes", "edgeTypes"]) {
662
+ const safe = cloudSanitizeCountRecord(parsed[key]);
663
+ if (safe) card[key] = safe;
664
+ }
665
+ for (const key of ["canonicalSources", "staleSourceCount"]) {
666
+ if (Number.isSafeInteger(parsed[key]) && parsed[key] >= 0) card[key] = parsed[key];
667
+ }
668
+ return card;
669
+ }
670
+
671
+ function cloudReadPublicCareerCard(snapshot, findings) {
672
+ const relativePath = ".agentlas/public-career-card.json";
673
+ const file = snapshot.get(relativePath);
674
+ if (!file) return undefined;
675
+ let parsed;
676
+ try { parsed = JSON.parse(Buffer.from(file.contentBase64, "base64").toString("utf8")); }
677
+ catch {
678
+ findings.push(cloudCareerFinding("career-card-invalid-json", "structure", "Career Graph public card is not valid JSON."));
679
+ return undefined;
680
+ }
681
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed) || parsed.kind !== "agentlas-public-career-card") {
682
+ findings.push(cloudCareerFinding("career-card-invalid-kind", "structure", "Career Graph public card has an invalid kind."));
683
+ return undefined;
684
+ }
685
+ const privacy = parsed.privacy && typeof parsed.privacy === "object" && !Array.isArray(parsed.privacy) ? parsed.privacy : {};
686
+ for (const key of ["rawLocalPathsIncluded", "rawPromptsIncluded", "rawTranscriptsIncluded", "sourceTextIncluded"]) {
687
+ if (privacy[key] !== false) findings.push(cloudCareerFinding(`career-card-privacy-${key}`, "policy", `Career Graph public card must set privacy.${key}=false.`));
688
+ }
689
+ if (cloudContainsAbsoluteLocalPath(JSON.stringify(parsed))) {
690
+ findings.push(cloudCareerFinding("career-card-local-path", "policy", "Career Graph public card contains a local absolute path."));
691
+ }
692
+ if (findings.some((finding) => finding.severity === "blocker" && finding.id.startsWith("career-card-"))) return undefined;
693
+ return cloudSanitizePublicCareerCard(parsed);
694
+ }
695
+
696
+ function cloudReplacePublicCareerCard(scan, card) {
697
+ const relativePath = ".agentlas/public-career-card.json";
698
+ const includedIndex = scan.included.findIndex((file) => file.path === relativePath);
699
+ const existing = includedIndex >= 0 ? scan.included[includedIndex] : null;
700
+ if (includedIndex >= 0) scan.included.splice(includedIndex, 1);
701
+ const fileRecord = scan.files.find((file) => file.path === relativePath);
702
+ if (!card) {
703
+ // redact 실패 시 원본 카드는 절대 발행 패키지에 실리지 않는다.
704
+ if (fileRecord) { fileRecord.included = false; fileRecord.reason = "public-career-card-blocked"; }
705
+ return;
706
+ }
707
+ const bytes = Buffer.from(JSON.stringify(card, null, 2) + "\n", "utf8");
708
+ const replacement = { path: relativePath, bytes: bytes.length, sha256: sha(bytes), contentBase64: bytes.toString("base64"), executable: false };
709
+ scan.included.push(replacement);
710
+ scan.included.sort(cloudCodePointPathOrder);
711
+ scan.totalBytes += bytes.length - (existing?.bytes || 0);
712
+ if (fileRecord) Object.assign(fileRecord, { bytes: bytes.length, sha256: replacement.sha256, kind: "text", executable: false, included: true, reason: undefined });
713
+ else scan.files.push({ path: relativePath, bytes: bytes.length, sha256: replacement.sha256, kind: "text", executable: false, included: true });
714
+ }
715
+
716
+ // ── 리뷰/요약 ──
717
+
718
+ /** 개인 저장(save)에서는 구조 게이트(정의 문서/라우팅 카드)를 요구하지 않는다 — 보안·크기만. */
719
+ function privateCloudSafetyFindings(findings) {
720
+ return findings.filter((finding) =>
721
+ (finding.severity === "blocker" && !finding.id.startsWith("missing-agent-definition"))
722
+ || finding.category === "secret"
723
+ || finding.category === "size");
724
+ }
725
+
726
+ function cloudStaticReview(findings, scope = "hub-public") {
727
+ const blockers = findings.filter((f) => f.severity === "blocker").length;
728
+ const high = findings.filter((f) => f.severity === "high").length;
729
+ return {
730
+ mode: "static-only",
731
+ verdict: blockers ? "fail" : high ? "needs-review" : "pass",
732
+ costOwner: "none",
733
+ summary: blockers || high
734
+ ? `${blockers} blocker(s), ${high} high-risk finding(s).`
735
+ : scope === "owner-private"
736
+ ? "Private Agent Cloud safety checks passed."
737
+ : "Static public package review passed.",
738
+ findings,
739
+ reviewedAt: new Date().toISOString(),
740
+ };
741
+ }
742
+
743
+ function cloudSecuritySummary(findings) {
744
+ const blockerCount = findings.filter((f) => f.severity === "blocker").length;
745
+ const highCount = findings.filter((f) => f.severity === "high").length;
746
+ return { verdict: blockerCount ? "fail" : highCount ? "needs-review" : "pass", blockerCount, highCount, findingCount: findings.length };
747
+ }
748
+
749
+ // ── 메인: 패키지(+등록) ──
750
+
751
+ async function packageCloudAgent(db, root, opts = {}) {
752
+ const requestedRoot = path.resolve(root);
753
+ let st;
754
+ try { st = fs.lstatSync(requestedRoot); } catch { throw new Error(`Folder not found: ${root}`); }
755
+ if (!st.isDirectory() || st.isSymbolicLink()) throw new Error(`Not a real directory: ${root}`);
756
+ const rootPath = fs.realpathSync.native(requestedRoot);
757
+ const visibility = opts.visibility || "private-link";
758
+ const isPublicHubPublish = visibility === "marketplace";
759
+ if (isPublicHubPublish && opts.llmReview) {
760
+ // 정직 정지: v1의 로컬 런타임 LLM 리뷰(runCloudLocalReviewCli)는 아직 v2 실행
761
+ // 계층에 배선되지 않았다. 조용히 static 리뷰로 다운그레이드해 "리뷰됨"으로
762
+ // 위장하지 않는다 (no-facade 이주 정책).
763
+ const error = new Error(
764
+ "--llm-review is not wired into the v2 engine yet (v1 reference: git tag legacy-v1-engine-snapshot). " +
765
+ "Publish runs the static security review; rerun without --llm-review.",
766
+ );
767
+ error.code = "AGENTLAS_V2_NOT_WIRED";
768
+ throw error;
769
+ }
770
+ const scan = scanCloudFolder(rootPath);
771
+ let snapshot = cloudPackageSnapshot(scan.included);
772
+ let careerGraph;
773
+ if (isPublicHubPublish) {
774
+ careerGraph = cloudReadPublicCareerCard(snapshot, scan.findings);
775
+ cloudReplacePublicCareerCard(scan, careerGraph);
776
+ snapshot = cloudPackageSnapshot(scan.included);
777
+ }
778
+ const routingCard = isPublicHubPublish ? readCloudRoutingCard(snapshot) : {};
779
+ if (routingCard.finding) scan.findings.push(routingCard.finding);
780
+ if (isPublicHubPublish) {
781
+ // 데스크탑 package.ts:435-449 동형: 공개 Hub 리스팅은 검증된 EN/KO 메타데이터 필수.
782
+ const localizedProblems = cloudLocalizedListingProblems(cloudReadLocalizedListing(snapshot));
783
+ if (localizedProblems.length > 0) {
784
+ scan.findings.push({
785
+ id: "localized-metadata-required",
786
+ severity: "blocker",
787
+ category: "structure",
788
+ file: ".agentlas/agent-card.json",
789
+ message: `Public Hub metadata needs verified English and Korean fields: ${localizedProblems.join(", ")}.`,
790
+ remediation:
791
+ "Add localized.titleEn, titleKo, descriptionEn, and descriptionKo to .agentlas/agent-card.json, or use local-runtime review so Agentlas can translate them with your connected model.",
792
+ });
793
+ }
794
+ }
795
+ const packageFindings = isPublicHubPublish ? scan.findings : privateCloudSafetyFindings(scan.findings);
796
+ const name = cloudReadName(snapshot, path.basename(rootPath));
797
+ const slug = cloudSlug(opts.slug || cloudReadStableSlug(snapshot) || name || path.basename(rootPath));
798
+ const scope = cas.cloudScopeForVisibility(visibility);
799
+ const baseDescriptor = state.cloudBaseDescriptorForSource(scan.localPackageMarker, rootPath, slug, scope);
800
+ const packageHashVersion = CLOUD_PACKAGE_HASH_V2;
801
+ const packageHash = cloudHashPackage(scan.included, packageHashVersion);
802
+ const manifest = {
803
+ version: "0.1",
804
+ kind: "agentlas-cloud-agent",
805
+ slug,
806
+ name,
807
+ tagline: cloudReadTagline(snapshot),
808
+ agentKind: cloudInferKind(snapshot),
809
+ runtimeLabels: cloudDetectRuntimeLabels(snapshot),
810
+ visibility,
811
+ // Content-derived and host-independent. Never persist an absolute local
812
+ // path fingerprint into a portable Cloud package.
813
+ rootFingerprint: sha(`agentlas-package-root:${packageHash}`),
814
+ packageHash,
815
+ packageHashVersion,
816
+ fileCount: scan.files.length,
817
+ includedFileCount: scan.included.length,
818
+ totalBytes: scan.included.reduce((sum, file) => sum + file.bytes, 0),
819
+ createdAt: new Date().toISOString(),
820
+ billingMode: "static-only",
821
+ costOwner: "none",
822
+ security: cloudSecuritySummary(packageFindings),
823
+ ...(careerGraph ? { careerGraph } : {}),
824
+ };
825
+ if (routingCard.card) manifest.routingCard = routingCard.card;
826
+ const packageDir = cloudPackageDir(slug);
827
+ fs.mkdirSync(packageDir, { recursive: true });
828
+ const manifestPath = path.join(packageDir, "package.manifest.json");
829
+ const bundlePath = path.join(packageDir, "package.bundle.json");
830
+ const bundle = {
831
+ manifest,
832
+ files: scan.included,
833
+ source: { packagedBy: "agentlas-cli", packagedAt: manifest.createdAt, costOwner: manifest.costOwner },
834
+ ...(careerGraph ? { careerGraph } : {}),
835
+ };
836
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf8");
837
+ fs.writeFileSync(bundlePath, JSON.stringify(bundle, null, 2) + "\n", "utf8");
838
+ const review = cloudStaticReview(packageFindings, isPublicHubPublish ? "hub-public" : "owner-private");
839
+ const allFindings = [...packageFindings, ...review.findings.filter((f) => !packageFindings.some((s) => s.id === f.id))];
840
+ manifest.security = cloudSecuritySummary(allFindings);
841
+ fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf8");
842
+ fs.writeFileSync(bundlePath, JSON.stringify({ ...bundle, manifest }, null, 2) + "\n", "utf8");
843
+ const blocked = review.verdict === "fail" || allFindings.some((f) => f.severity === "blocker");
844
+ let registration = null;
845
+ let status = blocked ? "blocked" : opts.dryRun ? "dry-run" : "ready";
846
+ if (!blocked && !opts.dryRun) {
847
+ registration = await cas.registerCloudAgent(manifest, bundlePath, review, visibility, { baseDescriptor });
848
+ let descriptor;
849
+ try {
850
+ descriptor = state.rememberCloudAssetDescriptor(registration, { sourceRoot: rootPath });
851
+ } catch (error) {
852
+ // 서버에는 커밋됐는데 로컬 관측 상태를 못 남기면 다음 save가 stale 베이스로
853
+ // 충돌한다 — 눈 감고 재시도하지 말라는 정직한 오류로 승격.
854
+ const stateError = new Error(
855
+ `Cloud save committed on the server, but this machine could not persist revision ${registration.revision}. ` +
856
+ "Do not retry blindly; run `agentlas cloud list` and restore the asset before the next update. " +
857
+ `Local state error: ${error.message || error}`,
858
+ );
859
+ stateError.code = "AGENTLAS_CLOUD_LOCAL_STATE_COMMIT_FAILED";
860
+ stateError.receipt = registration;
861
+ throw stateError;
862
+ }
863
+ try {
864
+ state.writeCloudSourceMarker(rootPath, scan, descriptor, {
865
+ previousMarker: scan.localPackageMarker,
866
+ packageHash,
867
+ packageHashVersion,
868
+ fileCount: scan.included.length,
869
+ totalBytes: manifest.totalBytes,
870
+ executablePaths: packageHashVersion === CLOUD_PACKAGE_HASH_V2
871
+ ? scan.included.filter((file) => file.executable).map((file) => file.path).sort()
872
+ : undefined,
873
+ });
874
+ } catch (error) {
875
+ registration.localStateWarning = `Cloud save succeeded, but the source marker could not be updated: ${error.message || error}`;
876
+ }
877
+ status = "registered";
878
+ }
879
+ return {
880
+ status,
881
+ rootPath,
882
+ packageDir,
883
+ manifestPath,
884
+ bundlePath,
885
+ manifest,
886
+ files: scan.files,
887
+ review,
888
+ registration,
889
+ summary: status === "registered"
890
+ ? isPublicHubPublish
891
+ ? `Published ${slug} publicly to Agentlas Hub.`
892
+ : `Saved ${slug} privately in Agent Cloud.`
893
+ : status === "blocked"
894
+ ? isPublicHubPublish
895
+ ? `Hub publish blocked: ${review.summary}`
896
+ : `Private Agent Cloud save blocked: ${review.summary}`
897
+ : isPublicHubPublish
898
+ ? `Hub package ready: ${slug}.`
899
+ : `Private Agent Cloud package ready: ${slug}.`,
900
+ };
901
+ }
902
+
903
+ module.exports = {
904
+ CLOUD_TEXT_EXTS,
905
+ CLOUD_AGENT_FILES,
906
+ CLOUD_SKIP_DIRS,
907
+ CLOUD_BLOCKED_FILE_RE,
908
+ CLOUD_ROUTING_CARD_PATH,
909
+ CLOUD_SECRET_RE,
910
+ cloudDecodeUtf16CredentialText,
911
+ cloudDecodeTextAsset,
912
+ cloudCredentialValueLooksReal,
913
+ cloudTextContainsStructuredCredential,
914
+ cloudAddSecretFindingsFromBytes,
915
+ cloudPackageSnapshot,
916
+ cloudReadName,
917
+ cloudReadTagline,
918
+ cloudReadLocalizedListing,
919
+ cloudLocalizedListingProblems,
920
+ cloudReadStableSlug,
921
+ cloudInferKind,
922
+ cloudDetectRuntimeLabels,
923
+ cloudPackageDir,
924
+ cloudReadRestoreExecutablePaths,
925
+ cloudPortableExecutableForFile,
926
+ scanCloudFolder,
927
+ readCloudRoutingCard,
928
+ cloudRoutingCardProblem,
929
+ cloudReadPublicCareerCard,
930
+ cloudReplacePublicCareerCard,
931
+ cloudSanitizePublicCareerCard,
932
+ privateCloudSafetyFindings,
933
+ cloudStaticReview,
934
+ cloudSecuritySummary,
935
+ packageCloudAgent,
936
+ };