agentlas 0.9.10 → 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 +80 -0
  2. package/README.md +319 -341
  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 -12367
  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,239 @@
1
+ "use strict";
2
+ /*
3
+ * cloud/hub-client — Agentlas Hub/Cloud HTTP 클라이언트 (v1에서 검증된 강화판 이식).
4
+ *
5
+ * 강화 계약(절대 약화 금지 — v1 timeout-regression이 지키던 속성):
6
+ * - connect/idle/total 3중 타임아웃 (AGENTLAS_HUB_*_TIMEOUT_MS로 조정)
7
+ * - 응답 크기 상한 16MB (HUB_RESPONSE_MAX_BYTES)
8
+ * - 업스트림 AbortSignal 전파
9
+ * 세션 쿠키 해석 순서: AGENTLAS_SESSION env → auth/cli-session.v1.json → (레거시) keytar.
10
+ */
11
+ const fs = require("node:fs");
12
+ const path = require("node:path");
13
+ const { userDataDir } = require("../core/paths.cjs");
14
+
15
+ const HUB_RESPONSE_MAX_BYTES = 16 * 1024 * 1024;
16
+ const HUB_TIMEOUT_DEFAULTS = Object.freeze({ connectMs: 15_000, idleMs: 30_000, totalMs: 180_000 });
17
+
18
+ function mcpBaseUrl() {
19
+ return (process.env.AGENTLAS_MCP_BASE_URL || "https://agentlas.cloud/api/mcp/v1").replace(/\/$/, "");
20
+ }
21
+
22
+ function webBaseUrl() {
23
+ return (process.env.AGENTLAS_WEB_BASE_URL || "https://agentlas.cloud").replace(/\/$/, "");
24
+ }
25
+
26
+ function finiteTimeoutMs(value, fallback, min, max) {
27
+ const parsed = Number(value);
28
+ if (!Number.isFinite(parsed)) return fallback;
29
+ return Math.min(max, Math.max(min, Math.trunc(parsed)));
30
+ }
31
+
32
+ function hubTimeoutConfig(env = process.env) {
33
+ const totalMs = finiteTimeoutMs(env.AGENTLAS_HUB_TOTAL_TIMEOUT_MS, HUB_TIMEOUT_DEFAULTS.totalMs, 5_000, 900_000);
34
+ return {
35
+ connectMs: Math.min(totalMs, finiteTimeoutMs(env.AGENTLAS_HUB_CONNECT_TIMEOUT_MS, HUB_TIMEOUT_DEFAULTS.connectMs, 1_000, 120_000)),
36
+ idleMs: Math.min(totalMs, finiteTimeoutMs(env.AGENTLAS_HUB_IDLE_TIMEOUT_MS, HUB_TIMEOUT_DEFAULTS.idleMs, 1_000, 300_000)),
37
+ totalMs,
38
+ };
39
+ }
40
+
41
+ function directHubTimeoutConfig(value = {}) {
42
+ const totalMs = finiteTimeoutMs(value.totalMs, HUB_TIMEOUT_DEFAULTS.totalMs, 10, 900_000);
43
+ return {
44
+ connectMs: Math.min(totalMs, finiteTimeoutMs(value.connectMs, HUB_TIMEOUT_DEFAULTS.connectMs, 10, 120_000)),
45
+ idleMs: Math.min(totalMs, finiteTimeoutMs(value.idleMs, HUB_TIMEOUT_DEFAULTS.idleMs, 10, 300_000)),
46
+ totalMs,
47
+ };
48
+ }
49
+
50
+ function hubTimeoutError(kind, ms) {
51
+ const message = kind === "connect"
52
+ ? `Hub 연결 제한 시간(${ms}ms)을 초과했습니다.`
53
+ : kind === "idle"
54
+ ? `Hub 응답이 ${ms}ms 동안 멈췄습니다.`
55
+ : `Hub 요청 전체 제한 시간(${ms}ms)을 초과했습니다.`;
56
+ const error = new Error(message);
57
+ error.code = `AGENTLAS_HUB_${kind.toUpperCase()}_TIMEOUT`;
58
+ return error;
59
+ }
60
+
61
+ /** Hub/Cloud fetch + body reader. Headers 전 connect, chunk 사이 idle, 전 구간 total timeout. */
62
+ async function fetchHub(url, init = {}, options = {}) {
63
+ const fetchImpl = options.fetch || globalThis.fetch;
64
+ if (typeof fetchImpl !== "function") throw new Error("fetch is unavailable in this runtime.");
65
+ const timeout = options.timeoutConfig ? directHubTimeoutConfig(options.timeoutConfig) : hubTimeoutConfig(options.env || process.env);
66
+ const controller = new AbortController();
67
+ const upstreamSignal = init.signal;
68
+ let connectTimer = null;
69
+ let idleTimer = null;
70
+ let totalTimer = null;
71
+ let reader = null;
72
+ let terminalError = null;
73
+ let rejectTerminal;
74
+ const terminal = new Promise((_, reject) => { rejectTerminal = reject; });
75
+ const stop = (error) => {
76
+ if (terminalError) return;
77
+ terminalError = error;
78
+ try { controller.abort(error); } catch { controller.abort(); }
79
+ rejectTerminal(error);
80
+ };
81
+ const onUpstreamAbort = () => {
82
+ const reason = upstreamSignal && upstreamSignal.reason;
83
+ const error = reason instanceof Error ? reason : new Error("Hub 요청이 취소되었습니다.");
84
+ if (!error.code) error.code = "ABORT_ERR";
85
+ stop(error);
86
+ };
87
+ const armIdle = () => {
88
+ if (idleTimer) clearTimeout(idleTimer);
89
+ idleTimer = setTimeout(() => stop(hubTimeoutError("idle", timeout.idleMs)), timeout.idleMs);
90
+ };
91
+
92
+ if (upstreamSignal) {
93
+ if (upstreamSignal.aborted) onUpstreamAbort();
94
+ else upstreamSignal.addEventListener("abort", onUpstreamAbort, { once: true });
95
+ }
96
+ connectTimer = setTimeout(() => stop(hubTimeoutError("connect", timeout.connectMs)), timeout.connectMs);
97
+ totalTimer = setTimeout(() => stop(hubTimeoutError("total", timeout.totalMs)), timeout.totalMs);
98
+
99
+ try {
100
+ const response = await Promise.race([
101
+ Promise.resolve().then(() => fetchImpl(url, { ...init, signal: controller.signal })),
102
+ terminal,
103
+ ]);
104
+ if (connectTimer) clearTimeout(connectTimer);
105
+ connectTimer = null;
106
+ const chunks = [];
107
+ let bytes = 0;
108
+ armIdle();
109
+ if (response.body && typeof response.body.getReader === "function") {
110
+ reader = response.body.getReader();
111
+ while (true) {
112
+ const part = await Promise.race([reader.read(), terminal]);
113
+ if (part.done) break;
114
+ armIdle();
115
+ const chunk = Buffer.from(part.value || []);
116
+ bytes += chunk.length;
117
+ if (bytes > HUB_RESPONSE_MAX_BYTES) {
118
+ const error = new Error(`Hub 응답이 허용 크기(${HUB_RESPONSE_MAX_BYTES} bytes)를 초과했습니다.`);
119
+ error.code = "AGENTLAS_HUB_RESPONSE_TOO_LARGE";
120
+ stop(error);
121
+ throw error;
122
+ }
123
+ chunks.push(chunk);
124
+ }
125
+ } else {
126
+ const raw = Buffer.from(await Promise.race([response.arrayBuffer(), terminal]));
127
+ bytes = raw.length;
128
+ if (bytes > HUB_RESPONSE_MAX_BYTES) throw new Error(`Hub response exceeds the allowed size (${HUB_RESPONSE_MAX_BYTES} bytes).`);
129
+ chunks.push(raw);
130
+ }
131
+ if (idleTimer) clearTimeout(idleTimer);
132
+ idleTimer = null;
133
+ const text = Buffer.concat(chunks, bytes).toString("utf8");
134
+ return { ok: response.ok, status: response.status, headers: response.headers, text };
135
+ } catch (error) {
136
+ if (terminalError) throw terminalError;
137
+ throw error;
138
+ } finally {
139
+ if (connectTimer) clearTimeout(connectTimer);
140
+ if (idleTimer) clearTimeout(idleTimer);
141
+ if (totalTimer) clearTimeout(totalTimer);
142
+ if (upstreamSignal) upstreamSignal.removeEventListener?.("abort", onUpstreamAbort);
143
+ if (reader && terminalError) {
144
+ try { await reader.cancel(terminalError); } catch { /* ignore */ }
145
+ }
146
+ }
147
+ }
148
+
149
+ function parseHubJson(response, label) {
150
+ try {
151
+ return JSON.parse(response.text || "null");
152
+ } catch {
153
+ throw new Error(`${label} returned invalid JSON.`);
154
+ }
155
+ }
156
+
157
+ // ── 세션 쿠키 ──
158
+ function cliSessionPath() {
159
+ return path.join(userDataDir(), "auth", "cli-session.v1.json");
160
+ }
161
+
162
+ function readCliSessionValue() {
163
+ try {
164
+ const j = JSON.parse(fs.readFileSync(cliSessionPath(), "utf8"));
165
+ return (j && typeof j.value === "string" && j.value) || null;
166
+ } catch {
167
+ return null;
168
+ }
169
+ }
170
+
171
+ function readKeytar() {
172
+ try {
173
+ return require("keytar");
174
+ } catch {
175
+ return null;
176
+ }
177
+ }
178
+
179
+ async function cloudSessionCookie() {
180
+ if (process.env.AGENTLAS_SESSION) return `agentlas_session=${process.env.AGENTLAS_SESSION}`;
181
+ const fileValue = readCliSessionValue();
182
+ if (fileValue) return `agentlas_session=${fileValue}`;
183
+ const keytar = readKeytar();
184
+ if (!keytar) return null;
185
+ try {
186
+ const value = await keytar.getPassword("Agentlas Session", "default");
187
+ return value ? `agentlas_session=${value}` : null;
188
+ } catch {
189
+ return null;
190
+ }
191
+ }
192
+
193
+ class HubError extends Error {
194
+ constructor(message, { code, status } = {}) {
195
+ super(message);
196
+ if (code) this.code = code;
197
+ if (status) this.status = status;
198
+ }
199
+ }
200
+
201
+ /**
202
+ * Hub MCP tool 호출 (POST <mcp base>/tools/call).
203
+ * 서버가 exact refusal(insufficient_credits 등)을 주면 그 문구를 그대로 전달한다 —
204
+ * 로컬 대체 실행으로 위장하지 않는다(오너 결정).
205
+ */
206
+ async function callHubTool(name, args, { requireSession = false, fetch: fetchImpl, timeoutConfig } = {}) {
207
+ const headers = { "content-type": "application/json" };
208
+ const cookie = await cloudSessionCookie();
209
+ if (requireSession && !cookie) {
210
+ throw new HubError("Agent Cloud sign-in is required. Run `agentlas login` first.", { code: "not_signed_in" });
211
+ }
212
+ if (cookie) headers.cookie = cookie;
213
+ const resp = await fetchHub(`${mcpBaseUrl()}/tools/call`, {
214
+ method: "POST",
215
+ headers,
216
+ body: JSON.stringify({ method: name, params: { name, arguments: args || {} } }),
217
+ }, { fetch: fetchImpl, timeoutConfig });
218
+ if (!resp.ok) throw new HubError(`${name} failed with HTTP ${resp.status}`, { status: resp.status });
219
+ const json = parseHubJson(resp, name);
220
+ if (json.error) throw new HubError(`${name}: ${json.error.message || "unknown error"}`, { code: json.error.code });
221
+ return json.result || null;
222
+ }
223
+
224
+ module.exports = {
225
+ HUB_RESPONSE_MAX_BYTES,
226
+ HUB_TIMEOUT_DEFAULTS,
227
+ mcpBaseUrl,
228
+ webBaseUrl,
229
+ hubTimeoutConfig,
230
+ directHubTimeoutConfig,
231
+ hubTimeoutError,
232
+ fetchHub,
233
+ parseHubJson,
234
+ cliSessionPath,
235
+ readCliSessionValue,
236
+ cloudSessionCookie,
237
+ callHubTool,
238
+ HubError,
239
+ };
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ /*
3
+ * cloud-assets/cargo — Agent Cloud(cargo.*) MCP tool 얇은 래퍼.
4
+ *
5
+ * cargo.*는 소유자 세션이 필요한 유일한 도구군이다(마켓 검색과 달리).
6
+ * 여기서는 어떤 폴백/로컬 위장도 하지 않는다 — 서버 거절(insufficient_credits,
7
+ * agent_not_found 등)은 그대로 중계한다.
8
+ */
9
+ const { callHubTool } = require("../cloud/hub-client.cjs");
10
+
11
+ function ownerCall(name, args) {
12
+ return callHubTool(name, args, { requireSession: true });
13
+ }
14
+
15
+ /** 소유 자산 검색 (q="" = 전체 나열). */
16
+ function cargoSearchAgents(args = {}) {
17
+ return ownerCall("cargo.search_agents", args);
18
+ }
19
+
20
+ /** 소유 자산 나열 — 서버에 별도 list 도구가 없으면 빈 질의 검색과 동치다. */
21
+ function cargoListAgents(limit = 100) {
22
+ const safeLimit = Math.max(1, Math.min(100, Number.isFinite(limit) ? Math.floor(limit) : 100));
23
+ return ownerCall("cargo.search_agents", { q: "", limit: safeLimit });
24
+ }
25
+
26
+ function cargoGetManifest(slug) {
27
+ return ownerCall("cargo.get_manifest", { slug });
28
+ }
29
+
30
+ function cargoRestorePackage(slug) {
31
+ return ownerCall("cargo.restore_package", { slug });
32
+ }
33
+
34
+ function cargoSaveCombination(payload) {
35
+ return ownerCall("cargo.save_combination", payload || {});
36
+ }
37
+
38
+ function cargoDeleteAgent(slug) {
39
+ return ownerCall("cargo.delete_agent", { slug });
40
+ }
41
+
42
+ module.exports = {
43
+ cargoSearchAgents,
44
+ cargoListAgents,
45
+ cargoGetManifest,
46
+ cargoRestorePackage,
47
+ cargoSaveCombination,
48
+ cargoDeleteAgent,
49
+ };
@@ -0,0 +1,235 @@
1
+ "use strict";
2
+ /*
3
+ * cloud-assets/cas — Agent Cloud 조건부(Compare-And-Swap) 등록/삭제 클라이언트.
4
+ *
5
+ * 멀티 호스트 계약 (약화 금지 — v1 cloud-cas-client가 지키던 속성):
6
+ * - 새 자산 생성은 If-None-Match: "*" — 다른 PC가 먼저 만든 자산을 절대 덮지 않는다.
7
+ * - 갱신/삭제는 관측한 베이스 리비전의 If-Match + x-agentlas-cloud-id 를 함께 보낸다.
8
+ * 관측한 베이스가 없으면 갱신을 시도조차 하지 않는다 (조용한 덮어쓰기 금지).
9
+ * - 412(cloud_agent_revision_conflict)는 "다른 PC가 먼저 변경"의 정직한 신호:
10
+ * 서버 리비전을 자동 채택하지 않고 restore→병합을 안내한다.
11
+ * - 503 + cloud_mutations_maintenance(WRITE_MODE blocked)는 fail-closed 거절.
12
+ * 서버가 그 코드를 줄 때만 maintenance 문구를 쓴다 — 일반 503을 "점검"으로
13
+ * 추측 번역하지 않고 서버 error 본문을 그대로 전달한다.
14
+ * - 2xx라도 영수증(schema/operation/scope/hash/revision/ETag/no-store)이 전부
15
+ * 맞아야 성공이다. 어긋나면 합성 성공으로 승격하지 않는다.
16
+ */
17
+ const fs = require("node:fs");
18
+ const crypto = require("node:crypto");
19
+ const { fetchHub, parseHubJson, cloudSessionCookie, webBaseUrl } = require("../cloud/hub-client.cjs");
20
+ const { cloudRevisionEtag, normalizeCloudAssetDescriptor } = require("../hub/install.cjs");
21
+ const state = require("./state.cjs");
22
+
23
+ function cloudScopeForVisibility(visibility) {
24
+ return visibility === "marketplace" ? "hub-public" : "owner-private";
25
+ }
26
+
27
+ /**
28
+ * 같은 논리 변이의 재시도는 같은 키를 공유하도록 결정론적으로 만든다.
29
+ * (scope/slug/패키지 정체성/베이스 리비전이 같으면 같은 요청이다 —
30
+ * 네트워크 재시도가 서버에서 이중 커밋되는 것을 막는 멱등성 힌트.)
31
+ */
32
+ function cloudIdempotencyKey(kind, scope, slug, packageHash, packageHashVersion, baseRevision) {
33
+ return crypto.createHash("sha256")
34
+ .update(`agentlas-cloud-${kind}:${scope}:${slug}:${packageHash}:${packageHashVersion}:${baseRevision || "*"}`)
35
+ .digest("hex");
36
+ }
37
+
38
+ function cloudCasResponseError(response, label) {
39
+ let body = null;
40
+ try { body = JSON.parse(response.text || "null"); } catch { /* generic below */ }
41
+ const code = body && typeof body.code === "string" ? body.code : "cloud_request_failed";
42
+ let message = `${label} failed with HTTP ${response.status}`;
43
+ if (response.status === 412 && code === "cloud_agent_revision_conflict") {
44
+ const current = body && body.current ? body.current : body && body.conflict && body.conflict.current;
45
+ message = current
46
+ ? `다른 PC에서 이 Agent Cloud 자산이 변경되었습니다. 자동 덮어쓰기는 중단했습니다. \`agentlas cloud list\`로 최신 revision을 확인하고 \`agentlas cloud restore ${current.slug || "<slug>"}\`로 복원한 뒤 변경 사항을 병합하세요.`
47
+ : "이 Agent Cloud 자산은 다른 PC에서 삭제되었거나 다른 식별자로 다시 생성되었습니다. 자동 재생성은 중단했습니다. `agentlas cloud list`로 현재 상태를 확인하세요.";
48
+ } else if (response.status === 428 && code === "client_upgrade_required") {
49
+ message = "No base revision is available to safely update the existing Cloud asset. The server revision will not be copied automatically. Check `agentlas cloud list`, restore with `agentlas cloud restore <slug>`, then save again.";
50
+ } else if (response.status === 503 && code === "cloud_mutations_maintenance") {
51
+ const retryAfter = response.headers && typeof response.headers.get === "function" ? response.headers.get("retry-after") : null;
52
+ message = `Agent Cloud save/delete is temporarily under maintenance${retryAfter ? ` (retry in about ${retryAfter} seconds)` : ""}. Read, list, and restore remain available.`;
53
+ } else if (body && typeof body.error === "string") {
54
+ // WRITE_MODE=blocked 등 서버의 fail-closed 거절 — 문구를 그대로 중계한다.
55
+ message = `${label} failed with HTTP ${response.status}: ${body.error.slice(0, 300)}`;
56
+ }
57
+ const error = new Error(message);
58
+ error.code = code;
59
+ error.status = response.status;
60
+ if (body && body.current) error.current = body.current;
61
+ if (body && body.conflict) error.conflict = body.conflict;
62
+ return error;
63
+ }
64
+
65
+ /** 등록(생성/갱신). 성공 시 검증된 리비전 디스크립터(+operation/url)를 반환한다. */
66
+ async function registerCloudAgent(manifest, bundlePath, review, visibility, options = {}) {
67
+ const cookie = await cloudSessionCookie();
68
+ if (!cookie) throw new Error("Agent Cloud sign-in is required. Sign in through Desktop or set AGENTLAS_SESSION.");
69
+ if (typeof fetch !== "function") throw new Error("fetch is unavailable in this runtime (run through the app runtime).");
70
+ const base = webBaseUrl();
71
+ const bundle = JSON.parse(fs.readFileSync(bundlePath, "utf8"));
72
+ const expectedScope = cloudScopeForVisibility(visibility);
73
+ const baseDescriptor = options.baseDescriptor
74
+ ? normalizeCloudAssetDescriptor(options.baseDescriptor, "base revision")
75
+ : null;
76
+ if (baseDescriptor && (baseDescriptor.slug !== manifest.slug || baseDescriptor.scope !== expectedScope)) {
77
+ throw new Error("Agent Cloud base revision does not match the requested slug/scope.");
78
+ }
79
+ const headers = {
80
+ "content-type": "application/json",
81
+ cookie,
82
+ origin: base,
83
+ "idempotency-key": cloudIdempotencyKey(
84
+ "register", expectedScope, manifest.slug, manifest.packageHash, manifest.packageHashVersion,
85
+ baseDescriptor && baseDescriptor.revision,
86
+ ),
87
+ };
88
+ if (baseDescriptor) {
89
+ headers["if-match"] = baseDescriptor.etag;
90
+ headers["x-agentlas-cloud-id"] = baseDescriptor.cloudId;
91
+ } else {
92
+ headers["if-none-match"] = "*";
93
+ }
94
+ const resp = await fetchHub(`${base}/api/cloud-agents/v1/register`, {
95
+ method: "POST",
96
+ headers,
97
+ body: JSON.stringify({ manifest, bundle, review, visibility, billing: { modelCallsPaidBy: review.costOwner, localRuntime: review.runtimeLabel || null } }),
98
+ });
99
+ if (!resp.ok) throw cloudCasResponseError(resp, "Agentlas Cloud 등록");
100
+ const json = parseHubJson(resp, "Agentlas Cloud 등록");
101
+ const expectedSource = visibility === "marketplace" ? "hub" : "agent-cloud";
102
+ const expectedVisibility = visibility === "marketplace" ? "marketplace" : "owner-private";
103
+ const etag = resp.headers && typeof resp.headers.get === "function" ? resp.headers.get("etag") : null;
104
+ const cacheControl = resp.headers && typeof resp.headers.get === "function" ? resp.headers.get("cache-control") : null;
105
+ const expectedOperations = baseDescriptor ? new Set(["updated", "unchanged"]) : new Set(["created"]);
106
+ if (
107
+ json.schema !== "agentlas.agent_cloud.registration.v1" ||
108
+ !expectedOperations.has(json.operation) ||
109
+ json.source !== expectedSource ||
110
+ json.visibility !== expectedVisibility ||
111
+ json.scope !== expectedScope ||
112
+ json.owner !== true ||
113
+ json.publicHubPublished !== (visibility === "marketplace") ||
114
+ json.dryRun !== false ||
115
+ typeof json.cloudId !== "string" || !json.cloudId.trim() ||
116
+ json.slug !== manifest.slug ||
117
+ json.packageHash !== manifest.packageHash ||
118
+ json.packageHashVersion !== manifest.packageHashVersion ||
119
+ typeof json.revision !== "string" || etag !== cloudRevisionEtag(json.revision) ||
120
+ typeof json.registeredAt !== "string" || !Number.isFinite(Date.parse(json.registeredAt)) ||
121
+ !String(cacheControl || "").toLowerCase().includes("no-store") ||
122
+ (baseDescriptor && json.cloudId !== baseDescriptor.cloudId)
123
+ ) {
124
+ throw new Error("Agentlas Cloud register returned an invalid or mismatched registration receipt.");
125
+ }
126
+ const descriptor = normalizeCloudAssetDescriptor({
127
+ cloudId: json.cloudId,
128
+ slug: json.slug,
129
+ scope: json.scope,
130
+ packageHash: json.packageHash,
131
+ packageHashVersion: json.packageHashVersion,
132
+ revision: json.revision,
133
+ etag,
134
+ updatedAt: json.savedAt || json.registeredAt,
135
+ }, "registration receipt");
136
+ return {
137
+ ...descriptor,
138
+ operation: json.operation,
139
+ ...(typeof json.url === "string" ? { url: json.url } : {}),
140
+ ...(typeof json.marketplaceUrl === "string" ? { marketplaceUrl: json.marketplaceUrl } : {}),
141
+ registeredAt: json.registeredAt,
142
+ dryRun: false,
143
+ };
144
+ }
145
+
146
+ /** 관측한 정확한 리비전 하나만 조건부 삭제. 성공 시 상태 저널/마커에서 베이스를 걷어낸다. */
147
+ async function deleteCloudAgent(slug, options = {}) {
148
+ // cloudSlug()의 "agentlas-cloud-agent" 폴백을 쓰면 빈 인자가 실존 슬러그로 위장된다 — 인라인 정규화 유지(v1 동일).
149
+ const safeSlug = String(slug || "").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
150
+ if (!safeSlug) throw new Error("usage: agentlas cloud delete <slug> [--json]");
151
+ const cookie = await cloudSessionCookie();
152
+ if (!cookie) throw new Error("Agent Cloud sign-in is required. Sign in through Desktop or set AGENTLAS_SESSION.");
153
+ if (typeof fetch !== "function") throw new Error("fetch is unavailable in this runtime (run through the app runtime).");
154
+ const scope = options.scope == null ? null : state.normalizeCloudScopeFlag(options.scope);
155
+ if (options.scope != null && !scope) throw new Error("--scope must be owner-private or hub-public");
156
+ const localEntry = state.findCloudAssetDescriptor(safeSlug, scope);
157
+ if (!localEntry) {
158
+ // 베이스 리비전을 관측한 적 없으면 삭제하지 않는다 — 다른 PC의 자산 오폭 방지.
159
+ throw new Error(`No observed base revision for ${safeSlug}${scope ? ` (${scope})` : ""}. Run \`agentlas cloud list\` first, then retry the exact asset deletion.`);
160
+ }
161
+ const descriptor = localEntry.descriptor;
162
+ const base = webBaseUrl();
163
+ const query = new URLSearchParams({ slug: safeSlug, scope: descriptor.scope, cloudId: descriptor.cloudId });
164
+ const resp = await fetchHub(`${base}/api/cloud-agents/v1/register?${query.toString()}`, {
165
+ method: "DELETE",
166
+ headers: {
167
+ "content-type": "application/json",
168
+ cookie,
169
+ origin: base,
170
+ "if-match": descriptor.etag,
171
+ "x-agentlas-cloud-id": descriptor.cloudId,
172
+ "idempotency-key": cloudIdempotencyKey(
173
+ "delete", descriptor.scope, descriptor.slug, descriptor.packageHash, descriptor.packageHashVersion, descriptor.revision,
174
+ ),
175
+ },
176
+ });
177
+ if (!resp.ok) throw cloudCasResponseError(resp, "Agentlas Cloud 삭제");
178
+ const json = parseHubJson(resp, "Agentlas Cloud 삭제");
179
+ const responseEtag = resp.headers && typeof resp.headers.get === "function" ? resp.headers.get("etag") : null;
180
+ const cacheControl = resp.headers && typeof resp.headers.get === "function" ? resp.headers.get("cache-control") : null;
181
+ const expectedSource = descriptor.scope === "hub-public" ? "hub" : "agent-cloud";
182
+ const expectedVisibility = descriptor.scope === "hub-public" ? "marketplace" : "owner-private";
183
+ const deletionTimestamp = descriptor.scope === "hub-public" ? json.unpublishedAt : json.deletedAt;
184
+ if (
185
+ json.schema !== "agentlas.agent_cloud.delete.v1" || json.ok !== true ||
186
+ json.source !== expectedSource || json.visibility !== expectedVisibility ||
187
+ json.scope !== descriptor.scope || json.cloudId !== descriptor.cloudId || json.slug !== descriptor.slug ||
188
+ json.packageHash !== descriptor.packageHash || json.packageHashVersion !== descriptor.packageHashVersion ||
189
+ json.revision !== descriptor.revision ||
190
+ responseEtag !== descriptor.etag || !String(cacheControl || "").toLowerCase().includes("no-store") ||
191
+ (descriptor.scope === "hub-public" && json.operation !== "unpublished") ||
192
+ typeof deletionTimestamp !== "string" || !Number.isFinite(Date.parse(deletionTimestamp))
193
+ ) {
194
+ throw new Error("Agentlas Cloud delete returned an invalid or mismatched deletion receipt.");
195
+ }
196
+ const stateValue = state.readCloudAssetState();
197
+ const key = state.cloudDescriptorKey(descriptor);
198
+ const roots = stateValue.assets[key]?.sourceRoots || [];
199
+ const warnings = [];
200
+ for (const rootPath of roots) {
201
+ // 톰스톤: 삭제된 베이스는 재저장 시 절대 If-Match 베이스로 재사용되지 않는다.
202
+ stateValue.deletedBases.push({ rootPath, slug: descriptor.slug, scope: descriptor.scope, cloudId: descriptor.cloudId, revision: descriptor.revision });
203
+ }
204
+ delete stateValue.assets[key];
205
+ stateValue.deletedBases = stateValue.deletedBases.slice(-256);
206
+ try {
207
+ state.writeCloudAssetState(stateValue);
208
+ } catch (error) {
209
+ const stateError = new Error(
210
+ `Cloud delete committed on the server, but this machine could not persist the deletion tombstone. ` +
211
+ "Run `agentlas cloud list` before saving this slug again. " +
212
+ `Local state error: ${error.message || error}`,
213
+ );
214
+ stateError.code = "AGENTLAS_CLOUD_LOCAL_STATE_COMMIT_FAILED";
215
+ stateError.receipt = json;
216
+ throw stateError;
217
+ }
218
+ for (const rootPath of roots) {
219
+ try {
220
+ const marker = state.readCloudSourceMarker(rootPath);
221
+ if (marker) state.writeCloudSourceMarker(rootPath, null, null, { previousMarker: marker, removeDescriptor: descriptor });
222
+ } catch (error) {
223
+ warnings.push(`Could not clear ${rootPath}: ${error.message || error}`);
224
+ }
225
+ }
226
+ return { ...json, ...(warnings.length ? { localStateWarnings: warnings } : {}) };
227
+ }
228
+
229
+ module.exports = {
230
+ cloudScopeForVisibility,
231
+ cloudIdempotencyKey,
232
+ cloudCasResponseError,
233
+ registerCloudAgent,
234
+ deleteCloudAgent,
235
+ };