agentlas 1.0.14 → 1.0.15
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.15 — 2026-07-29
|
|
4
|
+
|
|
5
|
+
- **Agent Cloud saves work from a new machine or fresh clone.** Before writing,
|
|
6
|
+
the terminal resolves the signed-in owner's exact asset revision by
|
|
7
|
+
`slug` and scope, then keeps the conditional-write guard on that revision.
|
|
8
|
+
A missing local receipt no longer turns an owned asset into a false create
|
|
9
|
+
conflict.
|
|
10
|
+
- **A real stale-copy conflict stays fail-closed.** The terminal stops before
|
|
11
|
+
replacing a newer server revision, reports its identity, and offers an
|
|
12
|
+
explicit `--overwrite` only when the owner deliberately chooses the current
|
|
13
|
+
folder over the newer copy.
|
|
14
|
+
- Conflict messages now describe ownership and the next command instead of
|
|
15
|
+
exposing storage precondition terminology.
|
|
16
|
+
|
|
3
17
|
## 1.0.14 — 2026-07-29
|
|
4
18
|
|
|
5
19
|
- **Write-capable commands now prepare every project automatically.** The first
|
|
@@ -74,7 +74,7 @@ function buildManifest(root, options = {}) {
|
|
|
74
74
|
memoryPolicy: { writeBack: "ask", publicCopy: "reset" },
|
|
75
75
|
memory: files.filter((file) => [".agentlas/memory-map.json", ".agentlas/agent-card.json"].includes(file.path)).map((file) => file.path),
|
|
76
76
|
allowRead: ["README.md", "AGENTS.md", "agent.md", "skills/**", ".agentlas/*.json"],
|
|
77
|
-
denyRead: [".env", ".env.*", "**/secrets/**", "
|
|
77
|
+
denyRead: [".env", ".env.*", "secrets/**", "**/secrets/**", "credentials/**", "**/credentials/**", "cookies/**", "**/cookies/**"],
|
|
78
78
|
publicExportPolicy: "clean-copy",
|
|
79
79
|
requiredRuntime: ["mcp-client"],
|
|
80
80
|
license: "call-only-default",
|
|
@@ -92,7 +92,13 @@ function scanFiles(files) {
|
|
|
92
92
|
findings.push({ verdict, type, path: file.path, ...(line ? { line } : {}), message, redacted: true });
|
|
93
93
|
}
|
|
94
94
|
for (const file of files) {
|
|
95
|
-
|
|
95
|
+
// A credential store is a path SEGMENT, never a filename substring. The old
|
|
96
|
+
// `/token|secret/i` test blocked ordinary vocabulary — measured 2026-07-29,
|
|
97
|
+
// it gave the live `web-master` package a BLOCK verdict for its own design
|
|
98
|
+
// system (`token-architecture.md`, `reference-token-db.json`). Dropping it
|
|
99
|
+
// costs no coverage: the per-line SECRET_PATTERNS scan below reads every
|
|
100
|
+
// packaged file and is the check that actually looks at values.
|
|
101
|
+
if (/^\.env(\.|$)/.test(file.path.split("/").pop() || "") || /(?:^|\/)(secrets|credentials|cookies)\//i.test(file.path)) {
|
|
96
102
|
add("BLOCK", "credential-path", file, null, "Credential-like file path is excluded from Cloud package and public publish.");
|
|
97
103
|
}
|
|
98
104
|
file.content.split(/\r?\n/).forEach((line, index) => {
|
|
@@ -42,9 +42,14 @@ function cloudCasResponseError(response, label) {
|
|
|
42
42
|
let message = `${label} failed with HTTP ${response.status}`;
|
|
43
43
|
if (response.status === 412 && code === "cloud_agent_revision_conflict") {
|
|
44
44
|
const current = body && body.current ? body.current : body && body.conflict && body.conflict.current;
|
|
45
|
+
// 서버가 알려준 사실만 사람 말로 전달한다. 내부 개념(precondition, base, 연결)은
|
|
46
|
+
// 사용자에게 아무 의미가 없다 — 기준은 "이 이름이 내 계정 것인가" 하나다.
|
|
45
47
|
message = current
|
|
46
|
-
?
|
|
47
|
-
|
|
48
|
+
? `업로드하지 않았습니다. "${current.slug || "이 이름"}"의 서버 버전이 지금 보낸 것과 맞지 않습니다` +
|
|
49
|
+
`${current.updatedAt ? ` (서버 쪽 마지막 저장: ${current.updatedAt})` : ""}.\n` +
|
|
50
|
+
` 내 계정 자산이면: agentlas cloud restore ${current.slug || "<slug>"} 로 받아서 확인한 뒤 다시 저장하세요.\n` +
|
|
51
|
+
` (서버 cloudId ${current.cloudId || "?"} · revision ${current.revision || "?"})`
|
|
52
|
+
: "업로드하지 않았습니다. 이 이름의 자산이 삭제됐거나 다른 계정 소유입니다. `agentlas cloud list`로 내 자산을 확인하세요.";
|
|
48
53
|
} else if (response.status === 428 && code === "client_upgrade_required") {
|
|
49
54
|
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
55
|
} else if (response.status === 503 && code === "cloud_mutations_maintenance") {
|
|
@@ -103,6 +103,7 @@ const CLOUD_HELP = [
|
|
|
103
103
|
" publish <path> [--dry-run] [--slug name]",
|
|
104
104
|
" explicitly publish to the public Agentlas Hub",
|
|
105
105
|
" package <path> [--json] [--visibility private-link|marketplace]",
|
|
106
|
+
" --overwrite 서버에 더 새 버전이 있어도 지금 폴더 내용으로 덮어쓰기",
|
|
106
107
|
" package only; defaults to private-save checks",
|
|
107
108
|
" list [--json] list packages in your private Agent Cloud",
|
|
108
109
|
" restore <slug> [--json] restore an owned Cloud package on this machine",
|
|
@@ -241,6 +242,7 @@ async function runCloud(ctx, args) {
|
|
|
241
242
|
slug: typeof flags.slug === "string" ? flags.slug : undefined,
|
|
242
243
|
visibility,
|
|
243
244
|
llmReview: Boolean(flags["llm-review"]),
|
|
245
|
+
overwriteRemote: Boolean(flags.overwrite),
|
|
244
246
|
dryRun,
|
|
245
247
|
});
|
|
246
248
|
if (flags.json) {
|
|
@@ -33,10 +33,12 @@ const {
|
|
|
33
33
|
cloudPortablePathConflict,
|
|
34
34
|
cloudPackageHashVersion,
|
|
35
35
|
cloudHashPackage,
|
|
36
|
+
normalizeCloudAssetDescriptor,
|
|
36
37
|
} = require("../hub/install.cjs");
|
|
37
38
|
const { SECRET_PATTERNS } = require("../agentlas-secret-patterns.cjs");
|
|
38
39
|
const { userDataDir } = require("../core/paths.cjs");
|
|
39
40
|
const state = require("./state.cjs");
|
|
41
|
+
const { cargoSearchAgents } = require("./cargo.cjs");
|
|
40
42
|
const cas = require("./cas.cjs");
|
|
41
43
|
|
|
42
44
|
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"]);
|
|
@@ -748,6 +750,28 @@ function cloudSecuritySummary(findings) {
|
|
|
748
750
|
|
|
749
751
|
// ── 메인: 패키지(+등록) ──
|
|
750
752
|
|
|
753
|
+
/**
|
|
754
|
+
* 이 계정이 소유한 자산의 현재 리비전을 서버에서 조회한다.
|
|
755
|
+
* 로컬 관측 기록이 전혀 없는 새 PC/새 클론에서 갱신을 가능하게 하는 유일한 경로다.
|
|
756
|
+
* 소유자 세션이 없거나 자산이 없으면 null — 조용한 폴백은 만들지 않는다.
|
|
757
|
+
*/
|
|
758
|
+
async function lookupOwnedCloudDescriptor(slug, scope) {
|
|
759
|
+
let response;
|
|
760
|
+
try {
|
|
761
|
+
response = await cargoSearchAgents({ q: slug, limit: 20 });
|
|
762
|
+
} catch {
|
|
763
|
+
return null;
|
|
764
|
+
}
|
|
765
|
+
const rows = Array.isArray(response && response.results) ? response.results : [];
|
|
766
|
+
const row = rows.find((item) => item && item.slug === slug && item.scope === scope);
|
|
767
|
+
if (!row) return null;
|
|
768
|
+
try {
|
|
769
|
+
return normalizeCloudAssetDescriptor(row, "owner cloud search result");
|
|
770
|
+
} catch {
|
|
771
|
+
return null;
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
|
|
751
775
|
async function packageCloudAgent(db, root, opts = {}) {
|
|
752
776
|
const requestedRoot = path.resolve(root);
|
|
753
777
|
let st;
|
|
@@ -796,7 +820,7 @@ async function packageCloudAgent(db, root, opts = {}) {
|
|
|
796
820
|
const name = cloudReadName(snapshot, path.basename(rootPath));
|
|
797
821
|
const slug = cloudSlug(opts.slug || cloudReadStableSlug(snapshot) || name || path.basename(rootPath));
|
|
798
822
|
const scope = cas.cloudScopeForVisibility(visibility);
|
|
799
|
-
|
|
823
|
+
let baseDescriptor = state.cloudBaseDescriptorForSource(scan.localPackageMarker, rootPath, slug, scope);
|
|
800
824
|
const packageHashVersion = CLOUD_PACKAGE_HASH_V2;
|
|
801
825
|
const packageHash = cloudHashPackage(scan.included, packageHashVersion);
|
|
802
826
|
const manifest = {
|
|
@@ -844,6 +868,30 @@ async function packageCloudAgent(db, root, opts = {}) {
|
|
|
844
868
|
let registration = null;
|
|
845
869
|
let status = blocked ? "blocked" : opts.dryRun ? "dry-run" : "ready";
|
|
846
870
|
if (!blocked && !opts.dryRun) {
|
|
871
|
+
// 자산의 정체성은 (로그인 계정, slug)다 — 어느 폴더에서 올리는지는 중요하지 않다.
|
|
872
|
+
// 쓰기 전에 서버가 들고 있는 내 자산의 현재 버전을 조회해서:
|
|
873
|
+
// · 이 폴더에 기록이 없으면(새 PC, 새 클론) 그 버전을 기준으로 업데이트한다.
|
|
874
|
+
// · 이 폴더 기록이 서버보다 오래됐으면(다른 곳에서 먼저 올림) 덮어쓰기 전에 멈춘다.
|
|
875
|
+
// 이것은 412 뒤의 자동 재시도가 아니라 쓰기 전 refresh다. 등록 자체는 여전히
|
|
876
|
+
// 조회한 정확한 리비전에 대한 조건부 쓰기(If-Match)라 경합은 서버가 막는다.
|
|
877
|
+
const remote = await lookupOwnedCloudDescriptor(slug, scope);
|
|
878
|
+
if (!baseDescriptor && remote) {
|
|
879
|
+
baseDescriptor = state.rememberCloudAssetDescriptor(remote, { sourceRoot: rootPath });
|
|
880
|
+
} else if (baseDescriptor && remote && baseDescriptor.revision !== remote.revision) {
|
|
881
|
+
if (opts.overwriteRemote) {
|
|
882
|
+
baseDescriptor = state.rememberCloudAssetDescriptor(remote, { sourceRoot: rootPath });
|
|
883
|
+
} else {
|
|
884
|
+
const error = new Error(
|
|
885
|
+
`업로드하지 않았습니다. "${slug}"에 더 새 버전이 있습니다 (${remote.updatedAt || "시각 미상"}에 다른 곳에서 저장됨).\n` +
|
|
886
|
+
` 지금 폴더 내용으로 그 버전을 덮어쓰려면: --overwrite\n` +
|
|
887
|
+
` 먼저 그 버전을 받아서 비교하려면: agentlas cloud restore ${slug}\n` +
|
|
888
|
+
` (서버 cloudId ${remote.cloudId} · revision ${remote.revision})`,
|
|
889
|
+
);
|
|
890
|
+
error.code = "cloud_agent_revision_conflict";
|
|
891
|
+
error.current = remote;
|
|
892
|
+
throw error;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
847
895
|
registration = await cas.registerCloudAgent(manifest, bundlePath, review, visibility, { baseDescriptor });
|
|
848
896
|
let descriptor;
|
|
849
897
|
try {
|
|
@@ -174,6 +174,39 @@ function cloudBaseDescriptorFromMarker(marker, slug, scope) {
|
|
|
174
174
|
* 톰스톤에 걸린 마커 디스크립터는 무시(삭제 후 재저장은 새 생성이어야 한다).
|
|
175
175
|
* 둘 다 있으면 같은 cloudId에서 더 최신 관측을 채택한다.
|
|
176
176
|
*/
|
|
177
|
+
/**
|
|
178
|
+
* 이 slug/scope의 관측된 리비전은 있는데, 이 소스 루트에는 아직 연결되지 않은
|
|
179
|
+
* 경우를 돌려준다.
|
|
180
|
+
*
|
|
181
|
+
* 이 구분이 없으면 base 조회가 null을 돌려주고, 호출부는 그것을 "새 자산"으로
|
|
182
|
+
* 읽어 create precondition을 보낸다. 서버에는 자산이 멀쩡히 존재하므로 412로
|
|
183
|
+
* 거절되고, 사용자는 "다른 PC에서 변경됨 / restore 하세요"라는 엉뚱한 안내를
|
|
184
|
+
* 받는다. restore는 디스크립터를 설치 경로에 묶으므로 소스 루트에서 발행하는
|
|
185
|
+
* 한 영원히 해소되지 않는다. 모름을 그럴듯한 값으로 메꾸지 않기 위한 분기다.
|
|
186
|
+
*/
|
|
187
|
+
function cloudUnboundDescriptorForSource(rootPath, slug, scope) {
|
|
188
|
+
const state = readCloudAssetState();
|
|
189
|
+
const entry = state.assets[`${scope}:${slug}`];
|
|
190
|
+
if (!entry) return null;
|
|
191
|
+
return entry.sourceRoots.includes(path.resolve(rootPath)) ? null : entry.descriptor;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** 이 소스 루트를 이미 관측된 클라우드 자산에 명시적으로 연결한다. */
|
|
195
|
+
function bindCloudAssetSourceRoot(rootPath, slug, scope) {
|
|
196
|
+
const state = readCloudAssetState();
|
|
197
|
+
const key = `${scope}:${slug}`;
|
|
198
|
+
const entry = state.assets[key];
|
|
199
|
+
if (!entry) throw new Error(`No observed Cloud revision for ${key} to bind.`);
|
|
200
|
+
const normalizedRoot = path.resolve(rootPath);
|
|
201
|
+
const roots = [...new Set([...entry.sourceRoots, normalizedRoot])].slice(0, 32);
|
|
202
|
+
state.assets[key] = { descriptor: entry.descriptor, sourceRoots: roots };
|
|
203
|
+
state.deletedBases = state.deletedBases.filter(
|
|
204
|
+
(item) => !(item.rootPath === normalizedRoot && item.slug === slug && item.scope === scope),
|
|
205
|
+
);
|
|
206
|
+
writeCloudAssetState(state);
|
|
207
|
+
return entry.descriptor;
|
|
208
|
+
}
|
|
209
|
+
|
|
177
210
|
function cloudBaseDescriptorForSource(marker, rootPath, slug, scope) {
|
|
178
211
|
const state = readCloudAssetState();
|
|
179
212
|
const normalizedRoot = path.resolve(rootPath);
|
|
@@ -263,6 +296,8 @@ module.exports = {
|
|
|
263
296
|
cloudMarkerDescriptors,
|
|
264
297
|
cloudBaseDescriptorFromMarker,
|
|
265
298
|
cloudBaseDescriptorForSource,
|
|
299
|
+
cloudUnboundDescriptorForSource,
|
|
300
|
+
bindCloudAssetSourceRoot,
|
|
266
301
|
writeCloudSourceMarker,
|
|
267
302
|
readCloudSourceMarker,
|
|
268
303
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentlas",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.15",
|
|
4
4
|
"description": "Agentlas agent terminal — chat with your installed AI agents and teams from the terminal, Claude Code style. Standalone: no desktop app required.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"agentlas": "bin/agentlas.cjs"
|