agentlas 0.6.0 → 0.9.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 (46) hide show
  1. package/CHANGELOG.md +190 -0
  2. package/README.md +220 -4
  3. package/bin/agentlas.cjs +8 -8
  4. package/engine/agentlas-capabilities.cjs +34 -3
  5. package/engine/agentlas-core-harness.cjs +205 -0
  6. package/engine/agentlas-desktop-loadout.cjs +527 -0
  7. package/engine/agentlas-doctor.cjs +1 -1
  8. package/engine/agentlas-experience-exchange.cjs +2151 -0
  9. package/engine/agentlas-experience-intake.cjs +444 -0
  10. package/engine/agentlas-experience-mcp.cjs +1709 -0
  11. package/engine/agentlas-i18n.cjs +10 -10
  12. package/engine/agentlas-input.cjs +5 -4
  13. package/engine/agentlas-mcp-env.cjs +219 -0
  14. package/engine/agentlas-mcp-wrapper.cjs +51 -0
  15. package/engine/agentlas-memory-governance.cjs +1029 -0
  16. package/engine/agentlas-native-host.cjs +129 -39
  17. package/engine/agentlas-parity.cjs +339 -154
  18. package/engine/agentlas-repl.cjs +327 -44
  19. package/engine/agentlas-workforce.cjs +2991 -0
  20. package/engine/agentlas-workload-routing.cjs +523 -0
  21. package/engine/agentlas.cjs +1886 -270
  22. package/engine/bootstrap-schema.sql +1 -1
  23. package/engine/experience-taxonomy-v1.json +49 -0
  24. package/package.json +8 -4
  25. package/scripts/gen-bootstrap-schema.sh +0 -23
  26. package/test/bootstrap-race.cjs +0 -47
  27. package/test/capture-runtime-guard.cjs +0 -122
  28. package/test/cloud-asset-restore.cjs +0 -423
  29. package/test/cloud-cas-client.cjs +0 -333
  30. package/test/cloud-owner-restore.cjs +0 -183
  31. package/test/cloud-runtime-paths.cjs +0 -40
  32. package/test/cloud-save-publish.cjs +0 -453
  33. package/test/credential-env-regression.cjs +0 -52
  34. package/test/login-loopback-security.cjs +0 -115
  35. package/test/mcp-config-isolation.cjs +0 -36
  36. package/test/permission-mapping.cjs +0 -180
  37. package/test/route-regression.cjs +0 -121
  38. package/test/run-api-regression.cjs +0 -322
  39. package/test/runtime-env-protection.cjs +0 -45
  40. package/test/semver-precedence.cjs +0 -39
  41. package/test/smoke.sh +0 -90
  42. package/test/sqlite-driver-probe.cjs +0 -22
  43. package/test/terminal-ui-regression.cjs +0 -472
  44. package/test/timeout-regression.cjs +0 -218
  45. package/test/tool-workspace-boundary.cjs +0 -165
  46. package/test/update-safety.cjs +0 -376
@@ -0,0 +1,2151 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Portable Experience Bundle v1 for the independent Agentlas Terminal.
5
+ *
6
+ * This module owns deterministic, model-free bundle validation, a private
7
+ * local cache, and authenticated Web API exchange. It does not activate a
8
+ * public Experience, create a Variant, accept evaluator authority, or turn a
9
+ * local item into reputation evidence.
10
+ */
11
+
12
+ const crypto = require("node:crypto");
13
+ const fs = require("node:fs");
14
+ const path = require("node:path");
15
+
16
+ const BUNDLE_SCHEMA = "agentlas.experience-bundle.v1";
17
+ const RECEIPT_SCHEMA = "agentlas.experience-upload-receipt.v1";
18
+ const BASE_RESOLUTION_SCHEMA = "agentlas.experience-base-resolution.v1";
19
+ const STATE_SCHEMA = "agentlas.terminal-experience-exchange.v1";
20
+ const MAX_BUNDLE_CANONICAL_BYTES = 3 * 1024 * 1024;
21
+ const MAX_BUNDLE_FILE_BYTES = 4 * 1024 * 1024;
22
+ const MAX_STATE_BYTES = 4 * 1024 * 1024;
23
+ const MAX_STORED_ITEMS = 256;
24
+ const MAX_MCP_REQUIREMENTS = 64;
25
+ const MAX_EVIDENCE_REFS_PER_ITEM = 24;
26
+ const MAX_INSTRUCTIONS_PER_ITEM = 8;
27
+ const MAX_TASK_SIGNATURES_PER_ITEM = 32;
28
+ const MAX_SOURCE_ATTESTATIONS = MAX_STORED_ITEMS * MAX_EVIDENCE_REFS_PER_ITEM;
29
+ const EXPERIENCE_RETRIEVAL_MAX_ITEMS = 8;
30
+ const EXPERIENCE_RETRIEVAL_MAX_TOKENS = 800;
31
+ const EXCHANGE_LOCK_STALE_MS = 30_000;
32
+ const EXCHANGE_LOCK_WAIT_MS = 2_000;
33
+ const EXPERIENCE_TAXONOMY_PATH = path.join(__dirname, "experience-taxonomy-v1.json");
34
+ const EXPERIENCE_TAXONOMY_CHECKSUM = "sha256:413833472e423352518f9591cd0e051c5bc0a7971e53ab3dc7b5aaf7d50c37ab";
35
+
36
+ function deepFreeze(value) {
37
+ if (!value || typeof value !== "object" || Object.isFrozen(value)) return value;
38
+ Object.freeze(value);
39
+ for (const child of Object.values(value)) deepFreeze(child);
40
+ return value;
41
+ }
42
+
43
+ function validateExperienceTaxonomyContract(value) {
44
+ const issues = [];
45
+ if (!value || typeof value !== "object" || Array.isArray(value)) issues.push("taxonomy must be an object");
46
+ else {
47
+ if (value.schema !== "agentlas.experience-taxonomy.v1") issues.push("taxonomy schema drifted");
48
+ if (value.kind !== "agentlas-experience-taxonomy") issues.push("taxonomy kind drifted");
49
+ if (value.taskSignaturePrefix !== "agentlas.task.v1/") issues.push("taxonomy task prefix drifted");
50
+ if (!Array.isArray(value.taskSlugs) || value.taskSlugs.length !== 23 || value.taskSlugs.includes("general")) issues.push("taxonomy task catalog drifted");
51
+ const environment = value.environment;
52
+ if (
53
+ !environment || environment.osPrefix !== "agentlas.env.v1/os/" ||
54
+ environment.archPrefix !== "agentlas.env.v1/arch/" || environment.runtimePrefix !== "agentlas.env.v1/runtime/" ||
55
+ JSON.stringify(environment.osValues) !== JSON.stringify(["macos", "windows", "linux", "ios", "android", "unknown"]) ||
56
+ JSON.stringify(environment.archValues) !== JSON.stringify(["arm64", "x64", "unknown"]) ||
57
+ environment.runtimePattern !== "^[a-z0-9][a-z0-9._-]{1,63}$" ||
58
+ environment.matching !== "all-canonical-constraints-must-match" ||
59
+ environment.unknownConstraint !== "item-ineligible-base-unaffected"
60
+ ) issues.push("taxonomy environment contract drifted");
61
+ const normalization = value.normalization;
62
+ if (
63
+ !normalization || normalization.unicode !== "NFKC" || normalization.trim !== true || normalization.case !== "lower" ||
64
+ normalization.portableSource !== "canonical-id-only" || normalization.runtimeProfile !== "canonical-id-or-exact-bare-slug" ||
65
+ normalization.fuzzySimilarity !== false || normalization.generalAutoMatch !== false
66
+ ) issues.push("taxonomy normalization contract drifted");
67
+ const checksum = `sha256:${crypto.createHash("sha256").update(canonicalJson(value), "utf8").digest("hex")}`;
68
+ if (checksum !== EXPERIENCE_TAXONOMY_CHECKSUM) issues.push("taxonomy checksum drifted");
69
+ }
70
+ if (issues.length) {
71
+ const error = new Error(issues.join("; "));
72
+ error.code = "experience_taxonomy_drift";
73
+ error.issues = issues;
74
+ throw error;
75
+ }
76
+ return deepFreeze(JSON.parse(canonicalJson(value)));
77
+ }
78
+
79
+ function loadExperienceTaxonomyContract() {
80
+ const stat = fs.lstatSync(EXPERIENCE_TAXONOMY_PATH);
81
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 64 * 1024) throw new Error("Experience taxonomy artifact is unsafe");
82
+ return validateExperienceTaxonomyContract(JSON.parse(fs.readFileSync(EXPERIENCE_TAXONOMY_PATH, "utf8")));
83
+ }
84
+
85
+ const OFFICIAL_EXPERIENCE_CLOUD_HOSTS = new Set([
86
+ "agentlas.cloud",
87
+ "www.agentlas.cloud",
88
+ "api.agentlas.cloud",
89
+ "staging.agentlas.cloud",
90
+ ]);
91
+
92
+ const ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:/@-]{2,255}$/;
93
+ const HASH_RE = /^sha256:[0-9a-f]{64}$/;
94
+ const BUNDLE_ID_RE = /^exb_[0-9a-f]{48}$/;
95
+ const UPLOAD_ID_RE = /^exu_[0-9a-f]{48}$/;
96
+ const SEMVER_RE = /^v?[0-9]+\.[0-9]+\.[0-9]+(?:[-+][A-Za-z0-9.-]+)?$/;
97
+ const ENV_RE = /^[A-Z][A-Z0-9_]*$/;
98
+ const SAFE_IDEMPOTENCY_RE = /^[A-Za-z0-9._:-]{8,200}$/;
99
+
100
+ const SECRET_PATTERNS = [
101
+ /\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}\b/i,
102
+ /\bxox[baprs]-[A-Za-z0-9-]{20,}\b/i,
103
+ /\bgh[pousr]_[A-Za-z0-9_]{20,}\b/,
104
+ /\bAKIA[0-9A-Z]{16}\b/,
105
+ /-----BEGIN (?:RSA |EC |OPENSSH |DSA )?PRIVATE KEY-----/i,
106
+ /\b(?:api[_-]?key|access[_-]?token|refresh[_-]?token|client[_-]?secret|password|passwd|private[_-]?key|cookie)\b\s*[:=]\s*['"]?[^\s'"]{8,}/i,
107
+ /\bauthorization\b\s*[:=]\s*['"]?(?:bearer|basic)\s+[A-Za-z0-9._~+/=-]{8,}/i,
108
+ ];
109
+ const PII_PATTERNS = [
110
+ /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i,
111
+ /(?<!\w)(?:\+?\d[\d ().-]{8,}\d)(?!\w)/,
112
+ /\b(?:account|customer|client|tenant|workspace|user)[ _-]?(?:id|key|number|no)\s*[:=#]?\s*[A-Za-z0-9_-]{4,}\b|(?:계정|고객|사용자)[ _-]?(?:id|아이디|번호)\s*[:=#]?\s*[A-Za-z0-9_-]{4,}/i,
113
+ ];
114
+ const LOCAL_PATH_PATTERNS = [
115
+ /(?:file:\/\/|(?:^|[\s"'`()\[\]{}=:,;])(?:\.\.[/\\]|~[/\\]|\/(?!\/|\s)(?:[^/\s"'`<>]+\/)*[^/\s"'`<>]+|[A-Za-z]:[/\\]\S+|\\\\[^\\/\s]+[\\/][^\\/\s]+))/i,
116
+ ];
117
+ const RAW_INTERACTION_PATTERNS = [
118
+ /(?:^|\n)\s*(?:system|assistant|user|tool|customer|agent)\s*:\s+/i,
119
+ /['"]role['"]\s*:\s*['"](?:system|assistant|user|tool)['"]/i,
120
+ /<\|(?:system|assistant|user|im_start|im_end)[^>]*\|>/i,
121
+ /BEGIN[ _-]?(?:SYSTEM[ _-]?PROMPT|BASE[ _-]?PROMPT|AGENT[ _-]?PACKAGE)/i,
122
+ /\b(?:AGENTS|CLAUDE|GEMINI)\.md\b|\.agentlas[/\\]/i,
123
+ ];
124
+ const PROMPT_INJECTION_PATTERNS = [
125
+ /\b(?:ignore|disregard|override)[\s_-]+(?:all[\s_-]+)?(?:previous|prior|system|developer|hidden)[\s_-]+(?:instructions?|prompts?|rules?)\b/i,
126
+ /\b(?:reveal|show|print|dump|expose|leak)[\s_-]+(?:(?:the|all)[\s_-]+)?(?:(?:hidden|system|developer)[\s_-]+)?(?:prompts?|instructions?|credentials?|secrets?|tokens?|api[\s_-]?keys?)\b/i,
127
+ /\b(?:exfiltrate|steal|upload|send)[^\n]{0,120}\b(?:secrets?|credentials?|tokens?|api[\s_-]?keys?|\.env)\b/i,
128
+ /\b(?:disable|bypass|skip|remove|turn[\s_-]+off)[\s_-]+(?:safety|guardrails?|approval|permission|security)\b/i,
129
+ ];
130
+ const BASE_PACKAGE_PATTERNS = [
131
+ /\bcontentBase64\b|\bcloudPackage\b\s*[:=]/i,
132
+ /\b(?:full|raw)\s+(?:system prompt|agent package|base package)\b/i,
133
+ /\bBEGIN AGENTLAS (?:AGENT|PACKAGE)\b/i,
134
+ ];
135
+ const OPAQUE_BLOB_RE = /(?:[A-Fa-f0-9]{128,}|[A-Za-z0-9+/]{124,}={0,2})/;
136
+ const PUBLIC_URL_RE = /\bhttps?:\/\/[^\s<>"']+/i;
137
+ const CUSTOMER_DATA_RE = /\b(?:customer|client|tenant|account|workspace|order|invoice)[ _-]?(?:name|email|address|phone|id|number|ref(?:erence)?)\s*[:=#]\s*\S+|(?:고객|클라이언트|계정|주문|송장)[ _-]?(?:이름|이메일|주소|전화|아이디|번호|참조)\s*[:=#]\s*\S+/i;
138
+ const FORBIDDEN_KEYS = new Set([
139
+ "basepackage", "basepackagefiles", "baseprompt", "cloudpackage", "contentbase64",
140
+ "files", "fulltranscript", "rawsource", "systemprompt", "transcript", "messages",
141
+ "command", "args", "cwd", "endpoint", "executable", "headers", "serverurl", "transportendpoint",
142
+ ]);
143
+
144
+ class ExperienceBundleValidationError extends Error {
145
+ constructor(issues) {
146
+ const unique = [...new Set((issues || []).map(String).filter(Boolean))];
147
+ super(unique.join("; ") || "invalid Portable Experience Bundle");
148
+ this.name = "ExperienceBundleValidationError";
149
+ this.code = "invalid_experience_bundle";
150
+ this.issues = unique;
151
+ }
152
+ }
153
+
154
+ function compareCodePoints(left, right) {
155
+ const a = Array.from(String(left));
156
+ const b = Array.from(String(right));
157
+ const length = Math.min(a.length, b.length);
158
+ for (let index = 0; index < length; index += 1) {
159
+ const delta = a[index].codePointAt(0) - b[index].codePointAt(0);
160
+ if (delta) return delta;
161
+ }
162
+ return a.length - b.length;
163
+ }
164
+
165
+ function normalizeJson(value, seen = new Set()) {
166
+ if (value == null || typeof value === "boolean") return value;
167
+ if (typeof value === "string") return value.normalize("NFC");
168
+ if (typeof value === "number") {
169
+ if (!Number.isFinite(value)) throw new ExperienceBundleValidationError(["canonical JSON forbids non-finite numbers"]);
170
+ return Object.is(value, -0) ? 0 : value;
171
+ }
172
+ if (Array.isArray(value)) {
173
+ if (seen.has(value)) throw new ExperienceBundleValidationError(["canonical JSON forbids cyclic values"]);
174
+ seen.add(value);
175
+ const result = value.map((child) => normalizeJson(child, seen));
176
+ seen.delete(value);
177
+ return result;
178
+ }
179
+ const prototype = value && typeof value === "object" ? Object.getPrototypeOf(value) : undefined;
180
+ if (!value || typeof value !== "object" || (prototype !== Object.prototype && prototype !== null)) {
181
+ throw new ExperienceBundleValidationError([`canonical JSON forbids ${typeof value}`]);
182
+ }
183
+ if (seen.has(value)) throw new ExperienceBundleValidationError(["canonical JSON forbids cyclic values"]);
184
+ seen.add(value);
185
+ const normalized = Object.create(null);
186
+ for (const rawKey of Object.keys(value)) {
187
+ const key = rawKey.normalize("NFC");
188
+ if (Object.prototype.hasOwnProperty.call(normalized, key)) {
189
+ throw new ExperienceBundleValidationError([`NFC-normalized object key collision: ${key}`]);
190
+ }
191
+ normalized[key] = normalizeJson(value[rawKey], seen);
192
+ }
193
+ seen.delete(value);
194
+ return normalized;
195
+ }
196
+
197
+ function canonicalValue(value) {
198
+ if (Array.isArray(value)) return value.map(canonicalValue);
199
+ if (value && typeof value === "object") {
200
+ const result = Object.create(null);
201
+ for (const key of Object.keys(value).sort(compareCodePoints)) result[key] = canonicalValue(value[key]);
202
+ return result;
203
+ }
204
+ return value;
205
+ }
206
+
207
+ function canonicalJson(value) {
208
+ return JSON.stringify(canonicalValue(normalizeJson(value)));
209
+ }
210
+
211
+ function canonicalHash(value) {
212
+ return `sha256:${crypto.createHash("sha256").update(canonicalJson(value), "utf8").digest("hex")}`;
213
+ }
214
+
215
+ // Load the frozen activation taxonomy only after the canonical JSON machinery
216
+ // and its error type have initialized. Any artifact drift stops startup.
217
+ const EXPERIENCE_TAXONOMY_V1 = loadExperienceTaxonomyContract();
218
+ const CANONICAL_TASK_PREFIX = EXPERIENCE_TAXONOMY_V1.taskSignaturePrefix;
219
+ const CANONICAL_ENV_PREFIX = "agentlas.env.v1/";
220
+ const CANONICAL_TASK_SLUGS = Object.freeze([...EXPERIENCE_TAXONOMY_V1.taskSlugs]);
221
+ const CANONICAL_TASK_IDS = Object.freeze(CANONICAL_TASK_SLUGS.map((slug) => `${CANONICAL_TASK_PREFIX}${slug}`));
222
+ const CANONICAL_TASK_ID_SET = new Set(CANONICAL_TASK_IDS);
223
+ const CANONICAL_OS_VALUES = new Set(EXPERIENCE_TAXONOMY_V1.environment.osValues);
224
+ const CANONICAL_ARCH_VALUES = new Set(EXPERIENCE_TAXONOMY_V1.environment.archValues);
225
+ const CANONICAL_RUNTIME_RE = new RegExp(EXPERIENCE_TAXONOMY_V1.environment.runtimePattern);
226
+
227
+ function sortedUnique(values) {
228
+ const byCanonical = new Map();
229
+ for (const value of values || []) byCanonical.set(canonicalJson(value), value);
230
+ return [...byCanonical.keys()].sort(compareCodePoints).map((key) => byCanonical.get(key));
231
+ }
232
+
233
+ function normalizeMcpRequirement(raw) {
234
+ const value = normalizeJson(raw);
235
+ for (const key of ["capabilities", "permissions", "alternatives"]) {
236
+ if (Array.isArray(value[key])) value[key] = sortedUnique(value[key]);
237
+ }
238
+ if (value.credentialMetadata && typeof value.credentialMetadata === "object" && !Array.isArray(value.credentialMetadata)) {
239
+ for (const key of ["env", "allowedHosts", "scopes"]) {
240
+ if (Array.isArray(value.credentialMetadata[key])) value.credentialMetadata[key] = sortedUnique(value.credentialMetadata[key]);
241
+ }
242
+ }
243
+ return value;
244
+ }
245
+
246
+ function normalizeExperienceItem(raw) {
247
+ const value = normalizeJson(raw);
248
+ for (const key of ["taskSignatures", "environmentConstraints", "evidenceReceiptIds", "supersedesItemIds"]) {
249
+ if (Array.isArray(value[key])) value[key] = sortedUnique(value[key]);
250
+ }
251
+ return value;
252
+ }
253
+
254
+ function normalizeExperienceBundle(payload) {
255
+ const value = normalizeJson(payload);
256
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new ExperienceBundleValidationError(["ExperienceBundle must be an object"]);
257
+ if (value.pack && typeof value.pack === "object" && !Array.isArray(value.pack)) {
258
+ if (value.pack.baseCompatibility && typeof value.pack.baseCompatibility === "object" && !Array.isArray(value.pack.baseCompatibility)) {
259
+ const ids = value.pack.baseCompatibility.compatibleBaseReleaseIds;
260
+ if (Array.isArray(ids)) value.pack.baseCompatibility.compatibleBaseReleaseIds = sortedUnique(ids);
261
+ }
262
+ for (const key of ["itemIds", "evidenceReceiptIds"]) {
263
+ if (Array.isArray(value.pack[key])) value.pack[key] = sortedUnique(value.pack[key]);
264
+ }
265
+ if (Array.isArray(value.pack.mcpRequirements)) {
266
+ value.pack.mcpRequirements = sortedUnique(value.pack.mcpRequirements.map((row) => normalizeMcpRequirement(row)));
267
+ }
268
+ }
269
+ if (Array.isArray(value.items)) value.items = sortedUnique(value.items.map((row) => normalizeExperienceItem(row)));
270
+ if (Array.isArray(value.sourceAttestations)) value.sourceAttestations = sortedUnique(value.sourceAttestations);
271
+ return value;
272
+ }
273
+
274
+ function experiencePackContentPayload(bundle) {
275
+ const value = normalizeExperienceBundle(bundle);
276
+ const pack = value.pack;
277
+ if (!pack || typeof pack !== "object" || !Array.isArray(value.items)) {
278
+ throw new ExperienceBundleValidationError(["ExperienceBundle needs pack and items before hashing"]);
279
+ }
280
+ return {
281
+ schemaVersion: pack.schemaVersion,
282
+ kind: pack.kind,
283
+ experiencePackId: pack.experiencePackId,
284
+ releaseId: pack.releaseId,
285
+ version: pack.version,
286
+ baseCompatibility: pack.baseCompatibility,
287
+ itemIds: pack.itemIds,
288
+ items: value.items,
289
+ evidenceReceiptIds: pack.evidenceReceiptIds,
290
+ mcpRequirements: pack.mcpRequirements,
291
+ containsBasePackageMaterial: pack.containsBasePackageMaterial,
292
+ };
293
+ }
294
+
295
+ function experiencePackContentHash(bundle) {
296
+ return canonicalHash(experiencePackContentPayload(bundle));
297
+ }
298
+
299
+ function experienceBundleHashPayload(bundle) {
300
+ const value = normalizeExperienceBundle(bundle);
301
+ return { content: experiencePackContentPayload(value), sourceAttestations: value.sourceAttestations, privacy: value.privacy };
302
+ }
303
+
304
+ function experienceBundleHash(bundle) {
305
+ return canonicalHash(experienceBundleHashPayload(bundle));
306
+ }
307
+
308
+ function experienceBundleId(bundle) {
309
+ return `exb_${experienceBundleHash(bundle).slice("sha256:".length, "sha256:".length + 48)}`;
310
+ }
311
+
312
+ function strictObject(value, required, allowed, label, issues) {
313
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
314
+ issues.push(`${label} must be an object`);
315
+ return {};
316
+ }
317
+ const keys = Object.keys(value);
318
+ const missing = [...required].filter((key) => !Object.prototype.hasOwnProperty.call(value, key)).sort(compareCodePoints);
319
+ const unknown = keys.filter((key) => !allowed.has(key)).sort(compareCodePoints);
320
+ if (missing.length) issues.push(`${label} missing required fields: ${missing.join(", ")}`);
321
+ if (unknown.length) issues.push(`${label} contains unknown fields: ${unknown.join(", ")}`);
322
+ return value;
323
+ }
324
+
325
+ function requiredObject(value, required, label, issues) {
326
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
327
+ issues.push(`${label} must be an object`);
328
+ return {};
329
+ }
330
+ const missing = [...required].filter((key) => !Object.prototype.hasOwnProperty.call(value, key)).sort(compareCodePoints);
331
+ if (missing.length) issues.push(`${label} missing required fields: ${missing.join(", ")}`);
332
+ return value;
333
+ }
334
+
335
+ function checkId(value, label, issues) {
336
+ if (typeof value !== "string" || !ID_RE.test(value)) issues.push(`${label} must be an opaque stable id`);
337
+ }
338
+
339
+ function checkHash(value, label, issues) {
340
+ if (typeof value !== "string" || !HASH_RE.test(value)) issues.push(`${label} must be sha256:<64 lowercase hex>`);
341
+ }
342
+
343
+ function checkString(value, label, minimum, maximum, issues) {
344
+ if (typeof value !== "string" || value.length < minimum || value.length > maximum) issues.push(`${label} must be a ${minimum}..${maximum} character string`);
345
+ }
346
+
347
+ function checkStringList(value, label, minimum, maximum, issues, options = {}) {
348
+ if (!Array.isArray(value) || value.length < minimum || value.some((item) => typeof item !== "string" || !item)) {
349
+ issues.push(`${label} must contain at least ${minimum} non-empty strings`);
350
+ return [];
351
+ }
352
+ if (maximum != null && value.length > maximum) issues.push(`${label} must contain at most ${maximum} values`);
353
+ if (new Set(value).size !== value.length) issues.push(`${label} must not contain duplicates`);
354
+ if (options.ids) value.forEach((item, index) => checkId(item, `${label}[${index}]`, issues));
355
+ return value;
356
+ }
357
+
358
+ function checkIso(value, label, issues, nullable = false) {
359
+ if (nullable && value == null) return;
360
+ if (typeof value !== "string") {
361
+ issues.push(`${label} must be an RFC3339 date-time${nullable ? " or null" : ""}`);
362
+ return;
363
+ }
364
+ const match = /^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d{1,9})?(Z|[+-]\d{2}:\d{2})$/.exec(value);
365
+ if (!match) {
366
+ issues.push(`${label} must be an RFC3339 date-time${nullable ? " or null" : ""}`);
367
+ return;
368
+ }
369
+ const [year, month, day, hour, minute, second] = match.slice(1, 7).map(Number);
370
+ const leap = year % 4 === 0 && (year % 100 !== 0 || year % 400 === 0);
371
+ const days = [0, 31, leap ? 29 : 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
372
+ const offset = match[7];
373
+ const offsetValid = offset === "Z" || (Number(offset.slice(1, 3)) <= 23 && Number(offset.slice(4, 6)) <= 59);
374
+ if (year < 1 || month < 1 || month > 12 || day < 1 || day > days[month] || hour > 23 || minute > 59 || second > 59 || !offsetValid || !Number.isFinite(Date.parse(value))) {
375
+ issues.push(`${label} must be a valid RFC3339 date-time${nullable ? " or null" : ""}`);
376
+ }
377
+ }
378
+
379
+ function validateCredentialMetadata(value, label, issues) {
380
+ const required = new Set(["provider", "env"]);
381
+ const allowed = new Set(["provider", "env", "allowedHosts", "scopes", "setupUrl", "brokerMode"]);
382
+ const data = strictObject(value, required, allowed, label, issues);
383
+ checkId(data.provider, `${label}.provider`, issues);
384
+ const env = checkStringList(data.env, `${label}.env`, 1, 32, issues);
385
+ env.forEach((item, index) => { if (!ENV_RE.test(item)) issues.push(`${label}.env[${index}] must be uppercase environment name`); });
386
+ if (data.allowedHosts != null) {
387
+ const hosts = checkStringList(data.allowedHosts, `${label}.allowedHosts`, 1, 64, issues);
388
+ const hostRe = /^(?:\*\.)?[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*$/;
389
+ hosts.forEach((host, index) => { if (host.length > 255 || !hostRe.test(host)) issues.push(`${label}.allowedHosts[${index}] is invalid`); });
390
+ }
391
+ if (data.scopes != null) {
392
+ const scopes = checkStringList(data.scopes, `${label}.scopes`, 1, 64, issues);
393
+ scopes.forEach((scope, index) => { if (!/^[A-Za-z0-9][A-Za-z0-9._:/@-]{0,127}$/.test(scope)) issues.push(`${label}.scopes[${index}] is invalid`); });
394
+ }
395
+ if (data.setupUrl != null && (
396
+ typeof data.setupUrl !== "string" ||
397
+ data.setupUrl.length > 2048 ||
398
+ !/^https:\/\/[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?(?:\.[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?)*(?:\/[A-Za-z0-9._~!$&'()*+,;=:@%/-]*)?$/.test(data.setupUrl)
399
+ )) {
400
+ issues.push(`${label}.setupUrl must be a value-free HTTPS provider page of at most 2048 characters`);
401
+ }
402
+ if (data.brokerMode != null && !["host-bound-broker", "runtime-env-injection", "provider-managed-oauth", "manual-provider-page"].includes(data.brokerMode)) {
403
+ issues.push(`${label}.brokerMode is invalid`);
404
+ }
405
+ }
406
+
407
+ function validateMcpRequirement(value, label, issues) {
408
+ const requiredFields = new Set(["schemaVersion", "kind", "requirementId", "catalogId", "reason", "capabilities", "required", "requiresKey", "priority", "permissions", "alternatives", "unavailablePolicy"]);
409
+ const data = strictObject(value, requiredFields, new Set([...requiredFields, "credentialMetadata"]), label, issues);
410
+ if (data.schemaVersion !== "agentlas.mcp-requirement.v1") issues.push(`${label}.schemaVersion is unsupported`);
411
+ if (data.kind !== "agentlas-mcp-requirement") issues.push(`${label}.kind is unsupported`);
412
+ checkId(data.requirementId, `${label}.requirementId`, issues);
413
+ checkId(data.catalogId, `${label}.catalogId`, issues);
414
+ checkString(data.reason, `${label}.reason`, 1, 300, issues);
415
+ checkStringList(data.capabilities, `${label}.capabilities`, 1, 32, issues, { ids: true });
416
+ checkStringList(data.permissions, `${label}.permissions`, 0, 64, issues, { ids: true });
417
+ const alternatives = checkStringList(data.alternatives, `${label}.alternatives`, 0, 32, issues, { ids: true });
418
+ if (alternatives.includes(data.catalogId)) issues.push(`${label}.alternatives must not contain catalogId`);
419
+ if (typeof data.required !== "boolean" || typeof data.requiresKey !== "boolean") issues.push(`${label}.required/requiresKey must be boolean`);
420
+ if (!Number.isInteger(data.priority) || data.priority < 1 || data.priority > 1000) issues.push(`${label}.priority must be 1..1000`);
421
+ if (data.credentialMetadata != null) validateCredentialMetadata(data.credentialMetadata, `${label}.credentialMetadata`, issues);
422
+ if (data.requiresKey === true && data.credentialMetadata == null) issues.push(`${label}.requiresKey=true requires credentialMetadata`);
423
+ const policy = strictObject(data.unavailablePolicy, new Set(["build", "rental", "execution"]), new Set(["build", "rental", "execution"]), `${label}.unavailablePolicy`, issues);
424
+ if (policy.build !== "degrade") issues.push(`${label}.unavailablePolicy.build must be degrade`);
425
+ const expectedRental = data.required === true ? "exclude-variant" : "continue-degraded";
426
+ if (policy.rental !== expectedRental) issues.push(`${label}.unavailablePolicy.rental must be ${expectedRental}`);
427
+ if (!["use-alternative", "disable-capability", "continue-degraded"].includes(policy.execution)) issues.push(`${label}.unavailablePolicy.execution is invalid`);
428
+ }
429
+
430
+ function validatePack(pack, issues) {
431
+ const required = new Set(["schemaVersion", "kind", "experiencePackId", "releaseId", "ownerRef", "version", "baseCompatibility", "itemIds", "evidenceReceiptIds", "mcpRequirements", "containsBasePackageMaterial", "contentHash", "visibility", "status"]);
432
+ const data = strictObject(pack, required, new Set([...required, "createdAt", "releasedAt", "withdrawnAt"]), "pack", issues);
433
+ if (data.schemaVersion !== "agentlas.experience-pack.v1") issues.push("pack.schemaVersion is unsupported");
434
+ if (data.kind !== "agentlas-experience-pack") issues.push("pack.kind is unsupported");
435
+ for (const key of ["experiencePackId", "releaseId", "ownerRef"]) checkId(data[key], `pack.${key}`, issues);
436
+ if (typeof data.version !== "string" || data.version.length > 64 || !SEMVER_RE.test(data.version)) issues.push("pack.version must be semantic version");
437
+ const base = strictObject(data.baseCompatibility, new Set(["agentDefinitionId", "compatibleBaseReleaseIds"]), new Set(["agentDefinitionId", "compatibleBaseReleaseIds"]), "pack.baseCompatibility", issues);
438
+ checkId(base.agentDefinitionId, "pack.baseCompatibility.agentDefinitionId", issues);
439
+ checkStringList(base.compatibleBaseReleaseIds, "pack.baseCompatibility.compatibleBaseReleaseIds", 1, 64, issues, { ids: true });
440
+ checkStringList(data.itemIds, "pack.itemIds", data.status === "active" ? 1 : 0, MAX_STORED_ITEMS, issues, { ids: true });
441
+ checkStringList(data.evidenceReceiptIds, "pack.evidenceReceiptIds", 0, MAX_SOURCE_ATTESTATIONS, issues, { ids: true });
442
+ if (!Array.isArray(data.mcpRequirements) || data.mcpRequirements.length > MAX_MCP_REQUIREMENTS) issues.push(`pack.mcpRequirements must contain at most ${MAX_MCP_REQUIREMENTS} requirements`);
443
+ else data.mcpRequirements.forEach((row, index) => validateMcpRequirement(row, `pack.mcpRequirements[${index}]`, issues));
444
+ if (data.containsBasePackageMaterial !== false) issues.push("pack.containsBasePackageMaterial must be false");
445
+ checkHash(data.contentHash, "pack.contentHash", issues);
446
+ if (!["private", "unlisted", "public"].includes(data.visibility)) issues.push("pack.visibility is invalid");
447
+ if (!["draft", "active", "suspended", "withdrawn", "deleted"].includes(data.status)) issues.push("pack.status is invalid");
448
+ if (data.createdAt != null) checkIso(data.createdAt, "pack.createdAt", issues);
449
+ if (Object.prototype.hasOwnProperty.call(data, "releasedAt")) checkIso(data.releasedAt, "pack.releasedAt", issues, true);
450
+ if (Object.prototype.hasOwnProperty.call(data, "withdrawnAt")) checkIso(data.withdrawnAt, "pack.withdrawnAt", issues, true);
451
+ }
452
+
453
+ function validateItem(item, index, issues) {
454
+ const label = `items[${index}]`;
455
+ const required = new Set(["schemaVersion", "kind", "experienceItemId", "experiencePackId", "experiencePackReleaseId", "type", "summary", "instructions", "taskSignatures", "environmentConstraints", "evidenceReceiptIds", "supersedesItemIds", "confidence", "status", "privacyScope"]);
456
+ const data = strictObject(item, required, new Set([...required, "createdAt"]), label, issues);
457
+ if (data.schemaVersion !== "agentlas.experience-item.v1") issues.push(`${label}.schemaVersion is unsupported`);
458
+ if (data.kind !== "agentlas-experience-item") issues.push(`${label}.kind is unsupported`);
459
+ for (const key of ["experienceItemId", "experiencePackId", "experiencePackReleaseId"]) checkId(data[key], `${label}.${key}`, issues);
460
+ if (!["procedure", "failure-recovery", "environment-gotcha", "tool-affordance", "warning", "supersedes"].includes(data.type)) issues.push(`${label}.type is invalid`);
461
+ checkString(data.summary, `${label}.summary`, 1, 320, issues);
462
+ if (!Array.isArray(data.instructions) || data.instructions.length < 1 || data.instructions.length > MAX_INSTRUCTIONS_PER_ITEM) issues.push(`${label}.instructions must contain 1..${MAX_INSTRUCTIONS_PER_ITEM} values`);
463
+ else data.instructions.forEach((step, stepIndex) => checkString(step, `${label}.instructions[${stepIndex}]`, 1, 600, issues));
464
+ checkStringList(data.taskSignatures, `${label}.taskSignatures`, 1, MAX_TASK_SIGNATURES_PER_ITEM, issues, { ids: true });
465
+ const environmentConstraints = checkStringList(data.environmentConstraints, `${label}.environmentConstraints`, 0, 32, issues);
466
+ environmentConstraints.forEach((constraint, constraintIndex) => {
467
+ if (constraint.length > 240) issues.push(`${label}.environmentConstraints[${constraintIndex}] must be at most 240 characters`);
468
+ });
469
+ checkStringList(data.evidenceReceiptIds, `${label}.evidenceReceiptIds`, 1, MAX_EVIDENCE_REFS_PER_ITEM, issues, { ids: true });
470
+ checkStringList(data.supersedesItemIds, `${label}.supersedesItemIds`, 0, MAX_STORED_ITEMS, issues, { ids: true });
471
+ if (typeof data.confidence !== "number" || !Number.isFinite(data.confidence) || data.confidence < 0 || data.confidence > 1) issues.push(`${label}.confidence must be 0..1`);
472
+ if (!["candidate", "promoted", "deprecated", "rejected"].includes(data.status)) issues.push(`${label}.status is invalid`);
473
+ if (!["private", "public-safe"].includes(data.privacyScope)) issues.push(`${label}.privacyScope is invalid`);
474
+ if (data.createdAt != null) checkIso(data.createdAt, `${label}.createdAt`, issues);
475
+ }
476
+
477
+ function metadataString(value) {
478
+ return HASH_RE.test(value) || BUNDLE_ID_RE.test(value) || UPLOAD_ID_RE.test(value) || /^[a-z]{3}_[0-9a-f]{32,64}$/.test(value) || /^\d{4}-\d{2}-\d{2}T\S+$/.test(value);
479
+ }
480
+
481
+ function validateBundleSecurity(value, issues) {
482
+ const strings = [];
483
+ function walk(node, prefix = "") {
484
+ if (Array.isArray(node)) return node.forEach((child, index) => walk(child, `${prefix}[${index}]`));
485
+ if (node && typeof node === "object") {
486
+ for (const [key, child] of Object.entries(node)) {
487
+ const normalizedKey = key.toLowerCase().replace(/[^a-z0-9]/g, "");
488
+ const next = prefix ? `${prefix}.${key}` : key;
489
+ if (FORBIDDEN_KEYS.has(normalizedKey)) issues.push(`ExperienceBundle forbids executable/raw field ${next}`);
490
+ walk(child, next);
491
+ }
492
+ return;
493
+ }
494
+ if (typeof node === "string") strings.push(node);
495
+ }
496
+ walk(value);
497
+ const nonMetadata = strings.filter((text) => !metadataString(text));
498
+ const checks = [
499
+ [SECRET_PATTERNS, strings, "secret or credential value"],
500
+ [PII_PATTERNS, nonMetadata, "personal/customer identifier"],
501
+ [LOCAL_PATH_PATTERNS, strings, "absolute local path or file URL"],
502
+ [RAW_INTERACTION_PATTERNS, strings, "raw prompt, transcript, or base package marker"],
503
+ [PROMPT_INJECTION_PATTERNS, strings, "prompt-injection instruction"],
504
+ [BASE_PACKAGE_PATTERNS, strings, "base package material"],
505
+ ];
506
+ for (const [patterns, candidates, label] of checks) {
507
+ if (patterns.some((pattern) => candidates.some((candidate) => pattern.test(candidate)))) issues.push(`ExperienceBundle contains ${label}`);
508
+ }
509
+ if (nonMetadata.some((text) => OPAQUE_BLOB_RE.test(text))) issues.push("ExperienceBundle contains a long opaque encoded blob");
510
+ }
511
+
512
+ /**
513
+ * Value-free privacy classification used before a successful run can become a
514
+ * local Operational Experience candidate. It deliberately returns only codes;
515
+ * unsafe source text is never copied into an intake receipt or bundle.
516
+ */
517
+ function portableExperienceSafetyIssues(text) {
518
+ const variants = [String(text || "")];
519
+ let current = variants[0];
520
+ for (let index = 0; index < 3; index += 1) {
521
+ try {
522
+ const decoded = decodeURIComponent(current);
523
+ if (decoded === current) break;
524
+ variants.push(decoded);
525
+ current = decoded;
526
+ } catch { break; }
527
+ }
528
+ const codes = [];
529
+ const hit = (patterns, code) => {
530
+ if (patterns.some((pattern) => variants.some((value) => pattern.test(value)))) codes.push(code);
531
+ };
532
+ hit(SECRET_PATTERNS, "secret-or-credential");
533
+ hit(PII_PATTERNS, "personal-or-customer-identifier");
534
+ hit(LOCAL_PATH_PATTERNS, "local-path");
535
+ hit(RAW_INTERACTION_PATTERNS, "raw-prompt-or-transcript");
536
+ hit(PROMPT_INJECTION_PATTERNS, "prompt-injection-material");
537
+ hit(BASE_PACKAGE_PATTERNS, "base-package-material");
538
+ if (variants.some((value) => PUBLIC_URL_RE.test(value))) codes.push("url");
539
+ if (variants.some((value) => CUSTOMER_DATA_RE.test(value))) codes.push("customer-data");
540
+ if (variants.some((value) => OPAQUE_BLOB_RE.test(value))) codes.push("opaque-blob");
541
+ return [...new Set(codes)].sort(compareCodePoints);
542
+ }
543
+
544
+ function validateExperienceBundle(payload) {
545
+ const value = normalizeExperienceBundle(payload);
546
+ const issues = [];
547
+ const required = new Set(["schemaVersion", "kind", "bundleId", "bundleHash", "requestedVisibility", "pack", "items", "sourceAttestations", "privacy"]);
548
+ strictObject(value, required, required, "ExperienceBundle", issues);
549
+ if (value.schemaVersion !== BUNDLE_SCHEMA) issues.push(`schemaVersion must equal ${BUNDLE_SCHEMA}`);
550
+ if (value.kind !== "agentlas-experience-bundle") issues.push("kind must equal agentlas-experience-bundle");
551
+ if (!["private", "unlisted", "public"].includes(value.requestedVisibility)) issues.push("requestedVisibility is invalid");
552
+ validatePack(value.pack, issues);
553
+
554
+ let items = value.items;
555
+ if (!Array.isArray(items) || items.length < 1 || items.length > MAX_STORED_ITEMS) {
556
+ issues.push(`items must contain 1..${MAX_STORED_ITEMS} items`);
557
+ items = [];
558
+ }
559
+ const itemIds = [];
560
+ const evidenceIds = [];
561
+ items.forEach((item, index) => {
562
+ validateItem(item, index, issues);
563
+ if (typeof item.experienceItemId === "string") itemIds.push(item.experienceItemId);
564
+ if (item.experiencePackId !== value.pack?.experiencePackId) issues.push(`items[${index}].experiencePackId does not match pack`);
565
+ if (item.experiencePackReleaseId !== value.pack?.releaseId) issues.push(`items[${index}].experiencePackReleaseId does not match pack release`);
566
+ if (Array.isArray(item.evidenceReceiptIds)) evidenceIds.push(...item.evidenceReceiptIds.filter((entry) => typeof entry === "string"));
567
+ });
568
+ if (new Set(itemIds).size !== itemIds.length) issues.push("items must have unique experienceItemId values");
569
+ if (canonicalJson(value.pack?.itemIds || []) !== canonicalJson(sortedUnique(itemIds))) issues.push("pack.itemIds must exactly equal submitted item ids");
570
+ if (canonicalJson(value.pack?.evidenceReceiptIds || []) !== canonicalJson(sortedUnique(evidenceIds))) issues.push("pack.evidenceReceiptIds must exactly equal item evidence ids");
571
+
572
+ let attestations = value.sourceAttestations;
573
+ if (!Array.isArray(attestations) || attestations.length > MAX_SOURCE_ATTESTATIONS) {
574
+ issues.push(`sourceAttestations must contain at most ${MAX_SOURCE_ATTESTATIONS} entries`);
575
+ attestations = [];
576
+ }
577
+ const attestationFields = new Set(["kind", "experienceItemId", "evidenceHash"]);
578
+ attestations.forEach((row, index) => {
579
+ const data = strictObject(row, attestationFields, attestationFields, `sourceAttestations[${index}]`, issues);
580
+ if (data.kind !== "user-attested") issues.push(`sourceAttestations[${index}].kind must be user-attested`);
581
+ if (!itemIds.includes(data.experienceItemId)) issues.push(`sourceAttestations[${index}] references missing item`);
582
+ checkHash(data.evidenceHash, `sourceAttestations[${index}].evidenceHash`, issues);
583
+ });
584
+
585
+ const privacyFields = new Set(["basePackageMaterialIncluded", "rawPromptIncluded", "rawTranscriptIncluded", "rawLocalPathsIncluded", "credentialValuesIncluded"]);
586
+ const privacy = strictObject(value.privacy, privacyFields, privacyFields, "privacy", issues);
587
+ for (const flag of privacyFields) if (privacy[flag] !== false) issues.push(`privacy.${flag} must be false`);
588
+ validateBundleSecurity(value, issues);
589
+
590
+ const canonicalBytes = Buffer.byteLength(canonicalJson(value), "utf8");
591
+ if (canonicalBytes > MAX_BUNDLE_CANONICAL_BYTES) issues.push(`canonical ExperienceBundle exceeds ${MAX_BUNDLE_CANONICAL_BYTES} bytes`);
592
+ let expectedPackHash = null;
593
+ let expectedBundleHash = null;
594
+ try {
595
+ expectedPackHash = experiencePackContentHash(value);
596
+ expectedBundleHash = experienceBundleHash(value);
597
+ if (value.pack?.contentHash !== expectedPackHash) issues.push("pack.contentHash does not match canonical Experience content");
598
+ if (value.bundleHash !== expectedBundleHash) issues.push("bundleHash does not match canonical bundle content");
599
+ const expectedId = `exb_${expectedBundleHash.slice(7, 55)}`;
600
+ if (value.bundleId !== expectedId || !BUNDLE_ID_RE.test(String(value.bundleId || ""))) issues.push("bundleId must be derived from bundleHash");
601
+ } catch (error) {
602
+ if (error instanceof ExperienceBundleValidationError) issues.push(...error.issues);
603
+ else throw error;
604
+ }
605
+ if (issues.length) throw new ExperienceBundleValidationError(issues);
606
+ return { bundle: value, canonicalJson: canonicalJson(value), canonicalBytes, expectedPackHash, expectedBundleHash, expectedBundleId: experienceBundleId(value) };
607
+ }
608
+
609
+ function readBundleFile(filePath, cwd = process.cwd()) {
610
+ const absolute = path.resolve(cwd, filePath);
611
+ const stat = fs.lstatSync(absolute);
612
+ if (!stat.isFile() || stat.isSymbolicLink()) throw new Error("Experience bundle must be a regular file; symlinks are forbidden");
613
+ if (stat.size < 1 || stat.size > MAX_BUNDLE_FILE_BYTES) throw new Error(`Experience bundle file must be 1..${MAX_BUNDLE_FILE_BYTES} bytes`);
614
+ let payload;
615
+ try { payload = JSON.parse(fs.readFileSync(absolute, "utf8")); } catch (error) { throw new Error(`Experience bundle is invalid JSON: ${error.message}`); }
616
+ return { absolute, ...validateExperienceBundle(payload) };
617
+ }
618
+
619
+ function recoverPrivateAtomicTarget(filePath, options = {}) {
620
+ const fsImpl = options.fs || fs;
621
+ const backup = `${filePath}.previous`;
622
+ if (!fsImpl.existsSync(backup)) return;
623
+ const backupStat = fsImpl.lstatSync(backup);
624
+ if (!backupStat.isFile() || backupStat.isSymbolicLink()) throw new Error("private atomic backup is unsafe");
625
+ if (fsImpl.existsSync(filePath)) {
626
+ const targetStat = fsImpl.lstatSync(filePath);
627
+ if (!targetStat.isFile() || targetStat.isSymbolicLink()) throw new Error("private atomic target is unsafe");
628
+ fsImpl.rmSync(backup, { force: true });
629
+ return;
630
+ }
631
+ fsImpl.renameSync(backup, filePath);
632
+ }
633
+
634
+ function replacePrivateFileAtomic(temp, filePath, options = {}) {
635
+ const fsImpl = options.fs || fs;
636
+ const platform = options.platform || process.platform;
637
+ recoverPrivateAtomicTarget(filePath, { fs: fsImpl });
638
+ try {
639
+ fsImpl.renameSync(temp, filePath);
640
+ return;
641
+ } catch (error) {
642
+ if (platform !== "win32" || !["EEXIST", "EPERM", "EACCES"].includes(error?.code) || !fsImpl.existsSync(filePath)) throw error;
643
+ }
644
+ const backup = `${filePath}.previous`;
645
+ fsImpl.renameSync(filePath, backup);
646
+ try {
647
+ fsImpl.renameSync(temp, filePath);
648
+ } catch (error) {
649
+ try {
650
+ if (!fsImpl.existsSync(filePath) && fsImpl.existsSync(backup)) fsImpl.renameSync(backup, filePath);
651
+ } catch (rollbackError) {
652
+ error.rollbackError = rollbackError;
653
+ }
654
+ throw error;
655
+ }
656
+ try { fsImpl.rmSync(backup, { force: true }); } catch { /* target is committed; recover/cleanup on the next access */ }
657
+ }
658
+
659
+ function writePrivateJsonAtomic(filePath, value) {
660
+ const dir = path.dirname(filePath);
661
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
662
+ try { fs.chmodSync(dir, 0o700); } catch { /* best effort on Windows */ }
663
+ const temp = path.join(dir, `.${path.basename(filePath)}.${process.pid}.${crypto.randomBytes(6).toString("hex")}.tmp`);
664
+ try {
665
+ fs.writeFileSync(temp, JSON.stringify(value, null, 2) + "\n", { encoding: "utf8", mode: 0o600, flag: "wx" });
666
+ replacePrivateFileAtomic(temp, filePath);
667
+ try { fs.chmodSync(filePath, 0o600); } catch { /* best effort on Windows */ }
668
+ } finally {
669
+ try { fs.rmSync(temp, { force: true }); } catch { /* noop */ }
670
+ }
671
+ }
672
+
673
+ function writePrivateTextAtomic(filePath, text) {
674
+ const dir = path.dirname(filePath);
675
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
676
+ try { fs.chmodSync(dir, 0o700); } catch { /* best effort */ }
677
+ const temp = path.join(dir, `.${path.basename(filePath)}.${process.pid}.${crypto.randomBytes(6).toString("hex")}.tmp`);
678
+ try {
679
+ fs.writeFileSync(temp, text.endsWith("\n") ? text : `${text}\n`, { encoding: "utf8", mode: 0o600, flag: "wx" });
680
+ replacePrivateFileAtomic(temp, filePath);
681
+ try { fs.chmodSync(filePath, 0o600); } catch { /* best effort */ }
682
+ } finally {
683
+ try { fs.rmSync(temp, { force: true }); } catch { /* noop */ }
684
+ }
685
+ }
686
+
687
+ function readPrivateFileSnapshot(filePath) {
688
+ recoverPrivateAtomicTarget(filePath);
689
+ if (!fs.existsSync(filePath)) return { exists: false, bytes: null };
690
+ const stat = fs.lstatSync(filePath);
691
+ if (!stat.isFile() || stat.isSymbolicLink()) throw new Error("private transaction target is unsafe");
692
+ return { exists: true, bytes: fs.readFileSync(filePath) };
693
+ }
694
+
695
+ function writePrivateBufferAtomic(filePath, bytes) {
696
+ const dir = path.dirname(filePath);
697
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
698
+ try { fs.chmodSync(dir, 0o700); } catch { /* best effort */ }
699
+ const temp = path.join(dir, `.${path.basename(filePath)}.${process.pid}.${crypto.randomBytes(6).toString("hex")}.rollback.tmp`);
700
+ try {
701
+ fs.writeFileSync(temp, bytes, { mode: 0o600, flag: "wx" });
702
+ replacePrivateFileAtomic(temp, filePath);
703
+ try { fs.chmodSync(filePath, 0o600); } catch { /* best effort */ }
704
+ } finally {
705
+ try { fs.rmSync(temp, { force: true }); } catch { /* noop */ }
706
+ }
707
+ }
708
+
709
+ function restorePrivateFileSnapshot(filePath, snapshot) {
710
+ recoverPrivateAtomicTarget(filePath);
711
+ if (snapshot.exists) {
712
+ writePrivateBufferAtomic(filePath, snapshot.bytes);
713
+ return;
714
+ }
715
+ if (fs.existsSync(filePath)) {
716
+ const stat = fs.lstatSync(filePath);
717
+ if (!stat.isFile() || stat.isSymbolicLink()) throw new Error("private rollback target is unsafe");
718
+ fs.rmSync(filePath, { force: true });
719
+ }
720
+ try { fs.rmSync(`${filePath}.previous`, { force: true }); } catch { /* noop */ }
721
+ }
722
+
723
+ function exchangeStatePath(userDataDir) {
724
+ return path.join(userDataDir, "terminal", "experience-exchange-v1.json");
725
+ }
726
+
727
+ function bundleStorePath(userDataDir, bundleId) {
728
+ if (!BUNDLE_ID_RE.test(String(bundleId || ""))) throw new Error("invalid bundle id");
729
+ return path.join(userDataDir, "terminal", "experience-bundles-v1", `${bundleId}.agentlas-experience.json`);
730
+ }
731
+
732
+ function emptyState() {
733
+ return { schemaVersion: STATE_SCHEMA, updatedAt: null, bundles: [] };
734
+ }
735
+
736
+ function waitSync(milliseconds) {
737
+ const signal = new Int32Array(new SharedArrayBuffer(4));
738
+ Atomics.wait(signal, 0, 0, milliseconds);
739
+ }
740
+
741
+ function withExchangeStateLock(userDataDir, action) {
742
+ const stateFile = exchangeStatePath(userDataDir);
743
+ const dir = path.dirname(stateFile);
744
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
745
+ try { fs.chmodSync(dir, 0o700); } catch { /* best effort */ }
746
+ const lockFile = `${stateFile}.lock`;
747
+ const deadline = Date.now() + EXCHANGE_LOCK_WAIT_MS;
748
+ let descriptor = null;
749
+ while (descriptor == null) {
750
+ try {
751
+ descriptor = fs.openSync(lockFile, "wx", 0o600);
752
+ fs.writeFileSync(descriptor, `${process.pid}\n${new Date().toISOString()}\n`, "utf8");
753
+ } catch (error) {
754
+ if (!error || error.code !== "EEXIST") throw error;
755
+ try {
756
+ const stat = fs.lstatSync(lockFile);
757
+ if (!stat.isFile() || stat.isSymbolicLink()) throw new Error("Terminal Experience exchange lock is unsafe");
758
+ if (Date.now() - stat.mtimeMs > EXCHANGE_LOCK_STALE_MS) {
759
+ fs.unlinkSync(lockFile);
760
+ continue;
761
+ }
762
+ } catch (statError) {
763
+ if (statError?.code === "ENOENT") continue;
764
+ throw statError;
765
+ }
766
+ if (Date.now() >= deadline) throw new Error("Terminal Experience exchange state is busy; retry the command");
767
+ waitSync(25);
768
+ }
769
+ }
770
+ try {
771
+ return action();
772
+ } finally {
773
+ try { fs.closeSync(descriptor); } catch { /* noop */ }
774
+ try { fs.unlinkSync(lockFile); } catch { /* stale recovery handles leftovers */ }
775
+ }
776
+ }
777
+
778
+ function loadExchangeState(userDataDir) {
779
+ const file = exchangeStatePath(userDataDir);
780
+ recoverPrivateAtomicTarget(file);
781
+ if (!fs.existsSync(file)) return emptyState();
782
+ const stat = fs.lstatSync(file);
783
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_STATE_BYTES) throw new Error("Terminal Experience exchange state is unsafe or too large");
784
+ const state = JSON.parse(fs.readFileSync(file, "utf8"));
785
+ const allowed = new Set(["schemaVersion", "updatedAt", "bundles"]);
786
+ const issues = [];
787
+ strictObject(state, allowed, allowed, "exchange state", issues);
788
+ if (state.schemaVersion !== STATE_SCHEMA || !Array.isArray(state.bundles)) issues.push("exchange state schema is invalid");
789
+ if (state.updatedAt != null) checkIso(state.updatedAt, "exchange state.updatedAt", issues, true);
790
+ if (state.bundles?.length > 2048) issues.push("exchange state has too many bundle records");
791
+ for (const [index, row] of (state.bundles || []).entries()) {
792
+ const required = new Set(["bundleId", "bundleHash", "experiencePackId", "experiencePackReleaseId", "agentDefinitionId", "compatibleBaseReleaseIds", "projectScopeHash", "storedAt", "remote"]);
793
+ strictObject(row, required, required, `exchange state.bundles[${index}]`, issues);
794
+ if (!BUNDLE_ID_RE.test(String(row.bundleId || ""))) issues.push(`exchange state.bundles[${index}].bundleId is invalid`);
795
+ checkHash(row.bundleHash, `exchange state.bundles[${index}].bundleHash`, issues);
796
+ for (const key of ["experiencePackId", "experiencePackReleaseId", "agentDefinitionId"]) checkId(row[key], `exchange state.bundles[${index}].${key}`, issues);
797
+ checkStringList(row.compatibleBaseReleaseIds, `exchange state.bundles[${index}].compatibleBaseReleaseIds`, 1, 64, issues, { ids: true });
798
+ checkHash(row.projectScopeHash, `exchange state.bundles[${index}].projectScopeHash`, issues);
799
+ checkIso(row.storedAt, `exchange state.bundles[${index}].storedAt`, issues);
800
+ if (row.remote != null) {
801
+ const remoteLabel = `exchange state.bundles[${index}].remote`;
802
+ if (typeof row.remote !== "object" || Array.isArray(row.remote)) issues.push(`${remoteLabel} is invalid`);
803
+ else {
804
+ const allowedRemote = new Set(["uploadId", "status", "requestedVisibility", "revision", "serverCheckedAt", "receipt", "baseResolution"]);
805
+ strictObject(row.remote, new Set(["uploadId", "status", "requestedVisibility", "revision", "serverCheckedAt", "receipt"]), allowedRemote, remoteLabel, issues);
806
+ if (!UPLOAD_ID_RE.test(String(row.remote.uploadId || ""))) issues.push(`${remoteLabel}.uploadId is invalid`);
807
+ if (!/^rev_[0-9a-f]{32}$/.test(String(row.remote.revision || ""))) issues.push(`${remoteLabel}.revision is invalid`);
808
+ checkIso(row.remote.serverCheckedAt, `${remoteLabel}.serverCheckedAt`, issues);
809
+ try {
810
+ const receipt = validateUploadReceipt(row.remote.receipt, null);
811
+ if (receipt.uploadId !== row.remote.uploadId || receipt.revision !== row.remote.revision || receipt.status !== row.remote.status || receipt.requestedVisibility !== row.remote.requestedVisibility) {
812
+ issues.push(`${remoteLabel} projection does not match its receipt`);
813
+ }
814
+ } catch (error) {
815
+ issues.push(...(error.issues || [`${remoteLabel}.receipt is invalid`]));
816
+ }
817
+ if (row.remote.baseResolution != null) {
818
+ const base = row.remote.baseResolution;
819
+ const requiredBase = new Set(["schema", "cloudId", "slug", "agentDefinitionId", "agentReleaseId", "packageHash", "packageHashVersion"]);
820
+ strictObject(base, requiredBase, requiredBase, `${remoteLabel}.baseResolution`, issues);
821
+ if (base.schema !== BASE_RESOLUTION_SCHEMA) issues.push(`${remoteLabel}.baseResolution schema is invalid`);
822
+ for (const key of ["cloudId", "agentDefinitionId", "agentReleaseId"]) checkId(base[key], `${remoteLabel}.baseResolution.${key}`, issues);
823
+ if (typeof base.slug !== "string" || !/^[a-z0-9][a-z0-9._-]{0,95}$/.test(base.slug)) issues.push(`${remoteLabel}.baseResolution.slug is invalid`);
824
+ checkHash(base.packageHash, `${remoteLabel}.baseResolution.packageHash`, issues);
825
+ if (!["path-sha256-v1", "path-sha256-executable-v2"].includes(base.packageHashVersion)) issues.push(`${remoteLabel}.baseResolution.packageHashVersion is invalid`);
826
+ }
827
+ }
828
+ }
829
+ }
830
+ if (issues.length) throw new ExperienceBundleValidationError(issues);
831
+ return state;
832
+ }
833
+
834
+ function saveExchangeState(userDataDir, state) {
835
+ state.updatedAt = new Date().toISOString();
836
+ writePrivateJsonAtomic(exchangeStatePath(userDataDir), state);
837
+ }
838
+
839
+ function projectScopeHash(cwd) {
840
+ let resolved = path.resolve(cwd || process.cwd());
841
+ try { resolved = fs.realpathSync.native(resolved); } catch { /* resolved path is still deterministic locally */ }
842
+ return canonicalHash({ kind: "terminal-project-scope", path: resolved.normalize("NFC") });
843
+ }
844
+
845
+ function commitLocalBundleRecord(userDataDir, validation, options = {}) {
846
+ const bundle = validation.bundle;
847
+ return withExchangeStateLock(userDataDir, () => {
848
+ const storedPath = bundleStorePath(userDataDir, bundle.bundleId);
849
+ const statePath = exchangeStatePath(userDataDir);
850
+ const storedSnapshot = readPrivateFileSnapshot(storedPath);
851
+ const stateSnapshot = readPrivateFileSnapshot(statePath);
852
+ const state = loadExchangeState(userDataDir);
853
+ const now = new Date().toISOString();
854
+ const previous = state.bundles.find((row) => row.bundleId === bundle.bundleId);
855
+ const row = {
856
+ bundleId: bundle.bundleId,
857
+ bundleHash: bundle.bundleHash,
858
+ experiencePackId: bundle.pack.experiencePackId,
859
+ experiencePackReleaseId: bundle.pack.releaseId,
860
+ agentDefinitionId: bundle.pack.baseCompatibility.agentDefinitionId,
861
+ compatibleBaseReleaseIds: [...bundle.pack.baseCompatibility.compatibleBaseReleaseIds],
862
+ projectScopeHash: projectScopeHash(options.cwd),
863
+ storedAt: now,
864
+ remote: Object.prototype.hasOwnProperty.call(options, "remote") ? options.remote : previous?.remote || null,
865
+ };
866
+ const index = state.bundles.findIndex((item) => item.bundleId === row.bundleId);
867
+ if (index >= 0) state.bundles[index] = row;
868
+ else state.bundles.push(row);
869
+ try {
870
+ writePrivateTextAtomic(storedPath, validation.canonicalJson);
871
+ saveExchangeState(userDataDir, state);
872
+ return row;
873
+ } catch (error) {
874
+ const rollbackErrors = [];
875
+ for (const [filePath, snapshot] of [[storedPath, storedSnapshot], [statePath, stateSnapshot]]) {
876
+ try { restorePrivateFileSnapshot(filePath, snapshot); }
877
+ catch (rollbackError) { rollbackErrors.push(rollbackError); }
878
+ }
879
+ if (rollbackErrors.length) error.rollbackErrors = rollbackErrors;
880
+ throw error;
881
+ }
882
+ });
883
+ }
884
+
885
+ function saveLocalBundle(userDataDir, validation, options = {}) {
886
+ return commitLocalBundleRecord(userDataDir, validation, options);
887
+ }
888
+
889
+ function readStoredBundle(userDataDir, bundleId) {
890
+ const file = bundleStorePath(userDataDir, bundleId);
891
+ recoverPrivateAtomicTarget(file);
892
+ return readBundleFile(file, "/");
893
+ }
894
+
895
+ function resolveBundleInput(userDataDir, sourceOrRef, cwd) {
896
+ if (!sourceOrRef) throw new Error("an Experience bundle file or saved bundle id is required");
897
+ if (BUNDLE_ID_RE.test(sourceOrRef)) return readStoredBundle(userDataDir, sourceOrRef);
898
+ return readBundleFile(sourceOrRef, cwd);
899
+ }
900
+
901
+ function parseFlags(args) {
902
+ const flags = { _: [] };
903
+ for (let index = 0; index < args.length; index += 1) {
904
+ const token = String(args[index]);
905
+ if (!token.startsWith("--")) { flags._.push(token); continue; }
906
+ const equal = token.indexOf("=");
907
+ if (equal > 2) { flags[token.slice(2, equal)] = token.slice(equal + 1); continue; }
908
+ const key = token.slice(2);
909
+ if (index + 1 < args.length && !String(args[index + 1]).startsWith("--")) flags[key] = String(args[++index]);
910
+ else flags[key] = true;
911
+ }
912
+ return flags;
913
+ }
914
+
915
+ function idempotencyKeyForBundle(bundle, explicit, operation = "save") {
916
+ const defaultDigest = crypto.createHash("sha256")
917
+ .update(canonicalJson({ bundleHash: bundle.bundleHash, operation, requestedVisibility: bundle.requestedVisibility }), "utf8")
918
+ .digest("hex");
919
+ const key = explicit || `exb-${defaultDigest}`;
920
+ if (!SAFE_IDEMPOTENCY_RE.test(key)) throw new Error("Idempotency-Key must be 8..200 safe ASCII characters");
921
+ return key;
922
+ }
923
+
924
+ function idempotencyKeyHash(key) {
925
+ return `sha256:${crypto.createHash("sha256").update(key, "utf8").digest("hex")}`;
926
+ }
927
+
928
+ function trustedExperienceOrigin(rawValue, options = {}) {
929
+ let parsed;
930
+ try { parsed = new URL(String(rawValue || "https://agentlas.cloud")); }
931
+ catch { throw new Error("Agentlas Experience Web origin is invalid"); }
932
+ if (parsed.username || parsed.password || parsed.search || parsed.hash || (parsed.pathname !== "/" && parsed.pathname !== "")) {
933
+ throw new Error("Agentlas Experience Web origin must not contain userinfo, path, query, or fragment");
934
+ }
935
+ const hostname = parsed.hostname.toLowerCase();
936
+ const loopback = hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
937
+ const loopbackOptIn = options.allowLoopback === true || options.env?.AGENTLAS_EXPERIENCE_ALLOW_LOOPBACK === "1";
938
+ if (loopback) {
939
+ if (!loopbackOptIn || !["http:", "https:"].includes(parsed.protocol)) {
940
+ throw new Error("Loopback Experience Web origin requires explicit AGENTLAS_EXPERIENCE_ALLOW_LOOPBACK=1 opt-in");
941
+ }
942
+ } else {
943
+ if (parsed.protocol !== "https:" || !OFFICIAL_EXPERIENCE_CLOUD_HOSTS.has(hostname) || (parsed.port && parsed.port !== "443")) {
944
+ throw new Error("Authenticated Experience exchange is restricted to an explicitly approved HTTPS Agentlas origin");
945
+ }
946
+ }
947
+ return parsed.origin;
948
+ }
949
+
950
+ function parseResponseJson(response, label) {
951
+ try { return JSON.parse(response.text || "null"); } catch { throw new Error(`${label} returned invalid JSON`); }
952
+ }
953
+
954
+ function responseError(response, label) {
955
+ let data = null;
956
+ try { data = JSON.parse(response.text || "null"); } catch { /* generic below */ }
957
+ const serverCode = [data?.errorCode, data?.code, data?.error]
958
+ .find((value) => typeof value === "string" && /^[a-z0-9][a-z0-9._-]{0,95}$/.test(value));
959
+ const detail = typeof data?.message === "string"
960
+ ? data.message
961
+ : typeof data?.error === "string"
962
+ ? data.error
963
+ : "";
964
+ const error = new Error(`${label} failed (${response.status})${detail ? `: ${detail.slice(0, 300)}` : ""}`);
965
+ error.status = response.status;
966
+ error.code = serverCode || (response.status === 401 || response.status === 403 ? "authentication_refused" : "experience_exchange_failed");
967
+ error.details = data;
968
+ return error;
969
+ }
970
+
971
+ async function authenticatedContext(options, dryRun) {
972
+ if (dryRun) return null;
973
+ const configured = options.baseUrl || options.env?.AGENTLAS_WEB_BASE_URL || process.env.AGENTLAS_WEB_BASE_URL || "https://agentlas.cloud";
974
+ const root = trustedExperienceOrigin(configured, options);
975
+ const getSessionCookie = options.getSessionCookie;
976
+ const cookie = typeof getSessionCookie === "function" ? await getSessionCookie() : options.sessionCookie;
977
+ if (!cookie) {
978
+ const error = new Error("Agentlas Cloud login is required; no Experience request was sent");
979
+ error.code = "authentication_required";
980
+ throw error;
981
+ }
982
+ if (typeof cookie !== "string" || cookie.length > 4096 || !/^agentlas_session=[A-Za-z0-9._~+/=%-]{1,4070}$/.test(cookie)) {
983
+ const error = new Error("Agentlas Cloud session cookie is malformed; no Experience request was sent");
984
+ error.code = "invalid_session_cookie";
985
+ throw error;
986
+ }
987
+ if (typeof options.fetchHub !== "function") throw new Error("authenticated Hub fetch boundary is unavailable");
988
+ return { cookie, origin: root, base: `${root}/api/experience/v1` };
989
+ }
990
+
991
+ function validateBaseResolution(value, bundle, requestedDescriptor) {
992
+ const required = new Set(["schema", "cloudId", "slug", "agentDefinitionId", "agentReleaseId", "packageHash", "packageHashVersion"]);
993
+ const issues = [];
994
+ const data = requiredObject(value, required, "ExperienceBaseResolution", issues);
995
+ if (data.schema !== BASE_RESOLUTION_SCHEMA) issues.push("base resolution schema is invalid");
996
+ for (const key of ["cloudId", "agentDefinitionId", "agentReleaseId"]) checkId(data[key], `base resolution.${key}`, issues);
997
+ if (typeof data.slug !== "string" || !/^[a-z0-9][a-z0-9._-]{0,95}$/.test(data.slug)) issues.push("base resolution.slug is invalid");
998
+ checkHash(data.packageHash, "base resolution.packageHash", issues);
999
+ if (!["path-sha256-v1", "path-sha256-executable-v2"].includes(data.packageHashVersion)) issues.push("base resolution.packageHashVersion is invalid");
1000
+ if (data.agentDefinitionId !== bundle.pack.baseCompatibility.agentDefinitionId || !bundle.pack.baseCompatibility.compatibleBaseReleaseIds.includes(data.agentReleaseId)) {
1001
+ issues.push("base resolution does not match the exact bundle base ids");
1002
+ }
1003
+ if (requestedDescriptor.cloudId && data.cloudId !== requestedDescriptor.cloudId) issues.push("base resolution.cloudId mismatches request");
1004
+ if (requestedDescriptor.slug && data.slug !== requestedDescriptor.slug) issues.push("base resolution.slug mismatches request");
1005
+ if (data.packageHash !== requestedDescriptor.packageHash) issues.push("base resolution.packageHash mismatches request");
1006
+ if (requestedDescriptor.packageHashVersion && data.packageHashVersion !== requestedDescriptor.packageHashVersion) issues.push("base resolution.packageHashVersion mismatches request");
1007
+ if (issues.length) throw new ExperienceBundleValidationError(issues);
1008
+ return {
1009
+ schema: data.schema,
1010
+ cloudId: data.cloudId,
1011
+ slug: data.slug,
1012
+ agentDefinitionId: data.agentDefinitionId,
1013
+ agentReleaseId: data.agentReleaseId,
1014
+ packageHash: data.packageHash,
1015
+ packageHashVersion: data.packageHashVersion,
1016
+ };
1017
+ }
1018
+
1019
+ function normalizeBaseDescriptor(options, existing) {
1020
+ const descriptor = {
1021
+ ...(existing || {}),
1022
+ ...(options.baseDescriptor || {}),
1023
+ };
1024
+ const request = {
1025
+ ...(descriptor.slug ? { slug: String(descriptor.slug) } : {}),
1026
+ ...(descriptor.cloudId ? { cloudId: String(descriptor.cloudId) } : {}),
1027
+ packageHash: String(descriptor.packageHash || ""),
1028
+ ...(descriptor.packageHashVersion ? { packageHashVersion: String(descriptor.packageHashVersion) } : {}),
1029
+ };
1030
+ if (!request.slug && !request.cloudId) throw new Error("exact base preflight requires --base-slug or --base-cloud-id");
1031
+ if (!HASH_RE.test(request.packageHash)) throw new Error("exact base preflight requires --base-package-hash sha256:<64 hex>");
1032
+ if (request.packageHashVersion && !["path-sha256-v1", "path-sha256-executable-v2"].includes(request.packageHashVersion)) {
1033
+ throw new Error("--base-package-hash-version is invalid");
1034
+ }
1035
+ return request;
1036
+ }
1037
+
1038
+ async function resolveBaseRelease(bundle, options, auth, existingDescriptor) {
1039
+ const descriptor = normalizeBaseDescriptor(options, existingDescriptor);
1040
+ const response = await options.fetchHub(`${auth.base}/base-releases/resolve`, {
1041
+ method: "POST",
1042
+ headers: { "content-type": "application/json", cookie: auth.cookie, origin: auth.origin },
1043
+ body: JSON.stringify(descriptor),
1044
+ });
1045
+ if (!response.ok) throw responseError(response, "exact base release preflight");
1046
+ const body = parseResponseJson(response, "exact base release preflight");
1047
+ return validateBaseResolution(body.baseResolution || body, bundle, descriptor);
1048
+ }
1049
+
1050
+ function validateUploadReceipt(receipt, bundle) {
1051
+ const required = new Set(["schema", "uploadId", "bundleId", "bundleHash", "experiencePackId", "experienceReleaseId", "ownerWorkspaceRef", "status", "requestedVisibility", "revision", "createdAt", "updatedAt"]);
1052
+ const issues = [];
1053
+ const data = requiredObject(receipt, required, "ExperienceUploadReceipt", issues);
1054
+ if (data.schema !== RECEIPT_SCHEMA) issues.push("ExperienceUploadReceipt schema is invalid");
1055
+ if (!UPLOAD_ID_RE.test(String(data.uploadId || ""))) issues.push("uploadId is invalid");
1056
+ if (!BUNDLE_ID_RE.test(String(data.bundleId || ""))) issues.push("receipt.bundleId is invalid");
1057
+ checkHash(data.bundleHash, "receipt.bundleHash", issues);
1058
+ for (const key of ["experiencePackId", "experienceReleaseId", "ownerWorkspaceRef"]) checkId(data[key], `receipt.${key}`, issues);
1059
+ if (!["draft-saved", "verification-requested", "verification-pending", "verified-private", "public-active", "conflict", "withdrawn", "rejected"].includes(data.status)) issues.push("receipt.status is invalid");
1060
+ if (!["private", "unlisted", "public"].includes(data.requestedVisibility)) issues.push("receipt.requestedVisibility is invalid");
1061
+ if (typeof data.revision !== "string" || !/^rev_[0-9a-f]{32}$/.test(data.revision)) issues.push("receipt.revision is invalid");
1062
+ checkIso(data.createdAt, "receipt.createdAt", issues);
1063
+ checkIso(data.updatedAt, "receipt.updatedAt", issues);
1064
+ if (data.errorCode != null && !/^[a-z0-9][a-z0-9._-]{0,95}$/.test(data.errorCode)) issues.push("receipt.errorCode is invalid");
1065
+ if (bundle) {
1066
+ if (data.bundleId !== bundle.bundleId || data.bundleHash !== bundle.bundleHash || data.experiencePackId !== bundle.pack.experiencePackId || data.experienceReleaseId !== bundle.pack.releaseId || data.requestedVisibility !== bundle.requestedVisibility) {
1067
+ issues.push("server receipt does not match the submitted bundle");
1068
+ }
1069
+ }
1070
+ if (issues.length) throw new ExperienceBundleValidationError(issues);
1071
+ return {
1072
+ schema: data.schema,
1073
+ uploadId: data.uploadId,
1074
+ bundleId: data.bundleId,
1075
+ bundleHash: data.bundleHash,
1076
+ experiencePackId: data.experiencePackId,
1077
+ experienceReleaseId: data.experienceReleaseId,
1078
+ ownerWorkspaceRef: data.ownerWorkspaceRef,
1079
+ status: data.status,
1080
+ requestedVisibility: data.requestedVisibility,
1081
+ revision: data.revision,
1082
+ createdAt: data.createdAt,
1083
+ updatedAt: data.updatedAt,
1084
+ ...(data.errorCode != null ? { errorCode: data.errorCode } : {}),
1085
+ };
1086
+ }
1087
+
1088
+ function remoteProjection(receipt, baseResolution = null, previousRemote = null) {
1089
+ return {
1090
+ uploadId: receipt.uploadId,
1091
+ status: receipt.status,
1092
+ requestedVisibility: receipt.requestedVisibility,
1093
+ revision: receipt.revision,
1094
+ serverCheckedAt: new Date().toISOString(),
1095
+ receipt,
1096
+ ...(baseResolution ? { baseResolution } : previousRemote?.baseResolution ? { baseResolution: previousRemote.baseResolution } : {}),
1097
+ };
1098
+ }
1099
+
1100
+ function commitServerAcceptedBundle(userDataDir, validation, receipt, baseResolution, options = {}) {
1101
+ const bundle = validation.bundle;
1102
+ try {
1103
+ const state = loadExchangeState(userDataDir);
1104
+ const previous = state.bundles.find((row) => row.bundleId === bundle.bundleId);
1105
+ return commitLocalBundleRecord(userDataDir, validation, {
1106
+ cwd: options.cwd,
1107
+ remote: remoteProjection(receipt, baseResolution, previous?.remote || null),
1108
+ });
1109
+ } catch (error) {
1110
+ const stateError = new Error(
1111
+ `Experience was accepted by the server as ${receipt.uploadId}, but Terminal could not atomically commit the canonical bundle and authoritative receipt. ` +
1112
+ "The prior local bundle/state were restored; rerun the same command and Idempotency-Key to reconcile the same receipt.",
1113
+ );
1114
+ stateError.code = "AGENTLAS_EXPERIENCE_LOCAL_STATE_COMMIT_FAILED";
1115
+ stateError.receipt = receipt;
1116
+ stateError.bundleId = bundle.bundleId;
1117
+ stateError.cause = error;
1118
+ throw stateError;
1119
+ }
1120
+ }
1121
+
1122
+ function persistRemoteReceipt(userDataDir, bundle, receipt, baseResolution = null) {
1123
+ try {
1124
+ return withExchangeStateLock(userDataDir, () => {
1125
+ const state = loadExchangeState(userDataDir);
1126
+ const row = state.bundles.find((item) => item.bundleId === bundle.bundleId);
1127
+ if (!row) throw new Error("local bundle record disappeared before receipt persistence");
1128
+ row.remote = remoteProjection(receipt, baseResolution, row.remote);
1129
+ saveExchangeState(userDataDir, state);
1130
+ return row;
1131
+ });
1132
+ } catch (error) {
1133
+ const stateError = new Error(
1134
+ `Experience was accepted by the server as ${receipt.uploadId}, but Terminal could not persist the authoritative receipt. ` +
1135
+ "Do not change the bundle or Idempotency-Key; rerun the same save/publish command to reconcile the same receipt.",
1136
+ );
1137
+ stateError.code = "AGENTLAS_EXPERIENCE_LOCAL_STATE_COMMIT_FAILED";
1138
+ stateError.receipt = receipt;
1139
+ stateError.bundleId = bundle.bundleId;
1140
+ stateError.cause = error;
1141
+ throw stateError;
1142
+ }
1143
+ }
1144
+
1145
+ async function recoverLostUpload(bundle, idempotencyKey, options, auth, originalError) {
1146
+ const query = new URLSearchParams({ bundleId: bundle.bundleId });
1147
+ let response;
1148
+ try {
1149
+ response = await options.fetchHub(`${auth.base}/uploads?${query.toString()}`, {
1150
+ method: "GET",
1151
+ headers: { accept: "application/json", cookie: auth.cookie, origin: auth.origin, "Idempotency-Key": idempotencyKey },
1152
+ });
1153
+ } catch (recoveryError) {
1154
+ originalError.recoveryError = recoveryError;
1155
+ throw originalError;
1156
+ }
1157
+ if (!response.ok) {
1158
+ originalError.recoveryStatus = response.status;
1159
+ throw originalError;
1160
+ }
1161
+ const body = parseResponseJson(response, "lost upload recovery");
1162
+ const receipt = validateUploadReceipt(body.receipt, bundle);
1163
+ const etag = response.headers && typeof response.headers.get === "function" ? response.headers.get("etag") : null;
1164
+ if (etag !== `"${receipt.revision}"`) throw new Error("lost upload recovery ETag does not match the receipt revision");
1165
+ return { receipt, replayed: true, recovered: true };
1166
+ }
1167
+
1168
+ async function publishBundle(validation, options = {}) {
1169
+ const originalBundle = validation.bundle;
1170
+ const requestedVisibility = options.operation === "save"
1171
+ ? "private"
1172
+ : String(options.requestedVisibility || originalBundle.requestedVisibility);
1173
+ if (options.operation === "publish" && !["unlisted", "public"].includes(requestedVisibility)) {
1174
+ throw new Error("experience publish requires requested visibility unlisted or public; use experience save for a private draft");
1175
+ }
1176
+ const bundle = normalizeExperienceBundle({ ...originalBundle, requestedVisibility });
1177
+ const normalizedValidation = validateExperienceBundle(bundle);
1178
+ const dryRun = options.dryRun === true;
1179
+ const operation = options.operation === "publish" ? "publish" : "save";
1180
+ const key = idempotencyKeyForBundle(bundle, options.idempotencyKey, operation);
1181
+ if (dryRun) {
1182
+ return { dryRun: true, networkUsed: false, bundleId: bundle.bundleId, bundleHash: bundle.bundleHash, requestedVisibility: bundle.requestedVisibility, publicActivation: false, evaluatorAuthority: false };
1183
+ }
1184
+ const auth = await authenticatedContext(options, false);
1185
+ const existingState = loadExchangeState(options.userDataDir);
1186
+ const existingRow = findStateRecord(existingState, bundle.bundleId);
1187
+ const exactBaseDescriptor = normalizeBaseDescriptor(options, existingRow?.remote?.baseResolution);
1188
+ // Preflight and server acceptance happen before the canonical local envelope
1189
+ // is changed. A failed promotion must leave the prior private file/state
1190
+ // byte-identical instead of pairing a public envelope with an old receipt.
1191
+ const baseRelease = await resolveBaseRelease(bundle, { ...options, baseDescriptor: exactBaseDescriptor }, auth, existingRow?.remote?.baseResolution);
1192
+ let response;
1193
+ try {
1194
+ response = await options.fetchHub(`${auth.base}/uploads`, {
1195
+ method: "POST",
1196
+ headers: { "content-type": "application/json", cookie: auth.cookie, origin: auth.origin, "Idempotency-Key": key, "If-None-Match": "*" },
1197
+ body: JSON.stringify({ bundle }),
1198
+ });
1199
+ } catch (error) {
1200
+ const recovered = await recoverLostUpload(bundle, key, options, auth, error);
1201
+ commitServerAcceptedBundle(options.userDataDir, normalizedValidation, recovered.receipt, baseRelease, { cwd: options.cwd });
1202
+ return { ...recovered, dryRun: false, networkUsed: true, baseRelease, publicActivation: false, evaluatorAuthority: false };
1203
+ }
1204
+ if (!response.ok) throw responseError(response, "Experience draft upload");
1205
+ const body = parseResponseJson(response, "Experience draft upload");
1206
+ if (typeof body.replayed !== "boolean" || (response.status === 201 && body.replayed !== false) || (response.status === 200 && body.replayed !== true)) {
1207
+ throw new Error("Experience upload returned an invalid replay marker");
1208
+ }
1209
+ const receipt = validateUploadReceipt(body.receipt, bundle);
1210
+ const expectedStatus = operation === "publish" ? "verification-requested" : "draft-saved";
1211
+ if (receipt.status !== expectedStatus) throw new Error(`Experience ${operation} receipt must be ${expectedStatus}, never ${receipt.status}`);
1212
+ const etag = response.headers && typeof response.headers.get === "function" ? response.headers.get("etag") : null;
1213
+ if (etag !== `"${receipt.revision}"`) throw new Error("Experience upload ETag does not match the exact receipt revision");
1214
+ commitServerAcceptedBundle(options.userDataDir, normalizedValidation, receipt, baseRelease, { cwd: options.cwd });
1215
+ return { receipt, replayed: body.replayed, recovered: false, dryRun: false, networkUsed: true, baseRelease, publicActivation: false, evaluatorAuthority: false };
1216
+ }
1217
+
1218
+ function findStateRecord(state, ref) {
1219
+ const matches = state.bundles.filter((row) => [row.bundleId, row.bundleHash, row.experiencePackId, row.experiencePackReleaseId, row.remote?.uploadId].includes(ref));
1220
+ return matches.sort((a, b) => String(b.storedAt).localeCompare(String(a.storedAt)))[0] || null;
1221
+ }
1222
+
1223
+ function verifyStoredBundleRow(userDataDir, row) {
1224
+ const validation = readStoredBundle(userDataDir, row.bundleId);
1225
+ const bundle = validation.bundle;
1226
+ const sameIdentity =
1227
+ bundle.bundleId === row.bundleId &&
1228
+ bundle.bundleHash === row.bundleHash &&
1229
+ bundle.pack.experiencePackId === row.experiencePackId &&
1230
+ bundle.pack.releaseId === row.experiencePackReleaseId &&
1231
+ bundle.pack.baseCompatibility.agentDefinitionId === row.agentDefinitionId &&
1232
+ canonicalJson(bundle.pack.baseCompatibility.compatibleBaseReleaseIds) === canonicalJson(row.compatibleBaseReleaseIds);
1233
+ if (!sameIdentity) throw new Error("stored Experience bundle identity does not match its private index");
1234
+ if (row.remote) {
1235
+ const receipt = validateUploadReceipt(row.remote.receipt, bundle);
1236
+ if (
1237
+ receipt.uploadId !== row.remote.uploadId ||
1238
+ receipt.status !== row.remote.status ||
1239
+ receipt.requestedVisibility !== row.remote.requestedVisibility ||
1240
+ receipt.revision !== row.remote.revision
1241
+ ) throw new Error("stored Experience server receipt projection drifted from its private index");
1242
+ if (row.remote.baseResolution) {
1243
+ const base = row.remote.baseResolution;
1244
+ if (
1245
+ base.agentDefinitionId !== row.agentDefinitionId ||
1246
+ !row.compatibleBaseReleaseIds.includes(base.agentReleaseId)
1247
+ ) throw new Error("stored Experience exact base resolution drifted from its private index");
1248
+ }
1249
+ }
1250
+ return validation;
1251
+ }
1252
+
1253
+ function scopedStateRows(userDataDir, cwd) {
1254
+ const scope = projectScopeHash(cwd);
1255
+ return loadExchangeState(userDataDir).bundles
1256
+ .filter((row) => row.projectScopeHash === scope)
1257
+ .sort((a, b) => compareCodePoints(a.experiencePackReleaseId, b.experiencePackReleaseId) || compareCodePoints(a.bundleId, b.bundleId));
1258
+ }
1259
+
1260
+ function resolveScopedStoredRecord(userDataDir, ref, cwd) {
1261
+ if (!ref) throw new Error("an exact Experience bundle, pack release, or upload reference is required");
1262
+ const matches = scopedStateRows(userDataDir, cwd)
1263
+ .filter((row) => [row.bundleId, row.bundleHash, row.experiencePackId, row.experiencePackReleaseId, row.remote?.uploadId].includes(ref));
1264
+ if (!matches.length) throw new Error(`no exact local Experience record exists for this project: ${ref}`);
1265
+ if (matches.length > 1) throw new Error(`Experience reference is ambiguous; use an exact release, bundle, or upload id: ${ref}`);
1266
+ return { row: matches[0], validation: verifyStoredBundleRow(userDataDir, matches[0]) };
1267
+ }
1268
+
1269
+ function publicStoredBundleView(row, validation) {
1270
+ const bundle = validation.bundle;
1271
+ const itemStatusCounts = bundle.items.reduce((counts, item) => {
1272
+ counts[item.status] = (counts[item.status] || 0) + 1;
1273
+ return counts;
1274
+ }, { candidate: 0, promoted: 0, deprecated: 0, rejected: 0 });
1275
+ const remote = row.remote
1276
+ ? {
1277
+ uploadId: row.remote.uploadId,
1278
+ status: row.remote.status,
1279
+ requestedVisibility: row.remote.requestedVisibility,
1280
+ revision: row.remote.revision,
1281
+ serverCheckedAt: row.remote.serverCheckedAt,
1282
+ receiptPresent: true,
1283
+ receiptVerified: true,
1284
+ ...(row.remote.baseResolution ? { exactBaseAgentReleaseId: row.remote.baseResolution.agentReleaseId } : {}),
1285
+ }
1286
+ : null;
1287
+ return {
1288
+ schemaVersion: "agentlas.terminal-experience-local-view.v1",
1289
+ bundleId: row.bundleId,
1290
+ bundleHash: row.bundleHash,
1291
+ experiencePackId: row.experiencePackId,
1292
+ experiencePackReleaseId: row.experiencePackReleaseId,
1293
+ agentDefinitionId: row.agentDefinitionId,
1294
+ compatibleBaseReleaseIds: [...row.compatibleBaseReleaseIds],
1295
+ requestedVisibility: bundle.requestedVisibility,
1296
+ itemCount: bundle.items.length,
1297
+ itemStatusCounts,
1298
+ reviewState: itemStatusCounts.candidate > 0 && itemStatusCounts.promoted === 0 ? "candidate-review" : "curated",
1299
+ storedAt: row.storedAt,
1300
+ currentProjectOnly: true,
1301
+ localBundleVerified: true,
1302
+ remote,
1303
+ publicActivationClaimed: false,
1304
+ evaluatorAuthority: false,
1305
+ };
1306
+ }
1307
+
1308
+ function listStoredExperienceBundles(userDataDir, cwd) {
1309
+ return scopedStateRows(userDataDir, cwd)
1310
+ .map((row) => publicStoredBundleView(row, verifyStoredBundleRow(userDataDir, row)));
1311
+ }
1312
+
1313
+ function inspectStoredExperienceBundle(userDataDir, ref, cwd) {
1314
+ const { row, validation } = resolveScopedStoredRecord(userDataDir, ref, cwd);
1315
+ return publicStoredBundleView(row, validation);
1316
+ }
1317
+
1318
+ function previewWithdrawUpload(ref, options = {}) {
1319
+ const { row, validation } = resolveScopedStoredRecord(options.userDataDir, ref, options.cwd);
1320
+ if (!row.remote?.uploadId || !row.remote?.revision || !row.remote?.receipt) {
1321
+ throw new Error("unpublish requires an exact locally observed server receipt; publish or run experience status first");
1322
+ }
1323
+ const receipt = validateUploadReceipt(row.remote.receipt, validation.bundle);
1324
+ if (receipt.status === "withdrawn") throw new Error("Experience upload is already withdrawn");
1325
+ return {
1326
+ schemaVersion: "agentlas.terminal-experience-unpublish-preview.v1",
1327
+ dryRun: true,
1328
+ action: "unpublish",
1329
+ bundleId: row.bundleId,
1330
+ experiencePackReleaseId: row.experiencePackReleaseId,
1331
+ uploadId: receipt.uploadId,
1332
+ currentStatus: receipt.status,
1333
+ ifMatchRevision: receipt.revision,
1334
+ networkUsed: false,
1335
+ localWriteUsed: false,
1336
+ serverReceiptPresent: true,
1337
+ authority: "local-observed-server-receipt",
1338
+ publicActivationClaimed: false,
1339
+ };
1340
+ }
1341
+
1342
+ async function fetchUploadStatus(ref, options = {}) {
1343
+ const { row, validation } = resolveScopedStoredRecord(options.userDataDir, ref, options.cwd);
1344
+ const uploadId = row.remote?.uploadId;
1345
+ if (!uploadId) throw new Error("no exact server upload receipt is known for this local project bundle");
1346
+ const auth = await authenticatedContext(options, false);
1347
+ const response = await options.fetchHub(`${auth.base}/uploads/${encodeURIComponent(uploadId)}`, {
1348
+ method: "GET",
1349
+ headers: { accept: "application/json", cookie: auth.cookie, origin: auth.origin },
1350
+ });
1351
+ if (!response.ok) throw responseError(response, "Experience upload status");
1352
+ const body = parseResponseJson(response, "Experience upload status");
1353
+ const bundle = validation.bundle;
1354
+ const receipt = validateUploadReceipt(body.receipt, bundle);
1355
+ if (receipt.uploadId !== uploadId) throw new Error("status receipt id mismatch");
1356
+ const etag = response.headers && typeof response.headers.get === "function" ? response.headers.get("etag") : null;
1357
+ if (etag !== `"${receipt.revision}"`) throw new Error("status ETag does not match the exact receipt revision");
1358
+ persistRemoteReceipt(options.userDataDir, bundle, receipt);
1359
+ return { receipt, authoritative: "server", publicActivation: false, evaluatorAuthority: false };
1360
+ }
1361
+
1362
+ function assertSafeExportTarget(filePath, overwrite) {
1363
+ if (!fs.existsSync(filePath)) return;
1364
+ const stat = fs.lstatSync(filePath);
1365
+ if (stat.isSymbolicLink()) throw new Error("Experience export output must not be a symbolic link");
1366
+ if (!stat.isFile()) throw new Error("Experience export output must be an ordinary file path");
1367
+ if (!overwrite) throw new Error("Experience export output already exists; pass --overwrite to replace that exact regular file");
1368
+ }
1369
+
1370
+ function writePrivateExportAtomic(filePath, text, overwrite) {
1371
+ const dir = path.dirname(filePath);
1372
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
1373
+ try { fs.chmodSync(dir, 0o700); } catch { /* best effort */ }
1374
+ assertSafeExportTarget(filePath, overwrite);
1375
+ const temp = path.join(dir, `.${path.basename(filePath)}.${process.pid}.${crypto.randomBytes(6).toString("hex")}.export.tmp`);
1376
+ try {
1377
+ fs.writeFileSync(temp, text.endsWith("\n") ? text : `${text}\n`, { encoding: "utf8", mode: 0o600, flag: "wx" });
1378
+ if (overwrite) {
1379
+ replacePrivateFileAtomic(temp, filePath);
1380
+ } else {
1381
+ // Same-directory hard-link publication is atomic and no-clobber: an
1382
+ // output created after the preflight causes EEXIST instead of overwrite.
1383
+ fs.linkSync(temp, filePath);
1384
+ fs.unlinkSync(temp);
1385
+ }
1386
+ try { fs.chmodSync(filePath, 0o600); } catch { /* best effort on Windows */ }
1387
+ } finally {
1388
+ try { fs.rmSync(temp, { force: true }); } catch { /* noop */ }
1389
+ }
1390
+ }
1391
+
1392
+ async function fetchUploadExport(ref, options = {}) {
1393
+ const { row } = resolveScopedStoredRecord(options.userDataDir, ref, options.cwd);
1394
+ const uploadId = row.remote?.uploadId;
1395
+ if (!uploadId) throw new Error("export requires an exact locally observed server upload receipt");
1396
+ const requestedOutput = options.outputPath
1397
+ ? path.resolve(options.cwd || process.cwd(), options.outputPath)
1398
+ : row
1399
+ ? path.resolve(options.cwd || process.cwd(), `${row.bundleId}.agentlas-experience.json`)
1400
+ : null;
1401
+ if (requestedOutput) assertSafeExportTarget(requestedOutput, options.overwrite === true);
1402
+ const auth = await authenticatedContext(options, false);
1403
+ const response = await options.fetchHub(`${auth.base}/uploads/${encodeURIComponent(uploadId)}/export`, {
1404
+ method: "GET",
1405
+ headers: { accept: "application/json", cookie: auth.cookie, origin: auth.origin },
1406
+ });
1407
+ if (!response.ok) throw responseError(response, "Experience export");
1408
+ const body = parseResponseJson(response, "Experience export");
1409
+ const validation = validateExperienceBundle(body.bundle);
1410
+ const receipt = validateUploadReceipt(body.receipt, validation.bundle);
1411
+ if (receipt.uploadId !== uploadId) throw new Error("export receipt id mismatch");
1412
+ if (validation.bundle.pack.ownerRef !== receipt.ownerWorkspaceRef) throw new Error("export bundle ownerRef does not match the authenticated receipt owner");
1413
+ if (row && (validation.bundle.bundleId !== row.bundleId || validation.bundle.bundleHash !== row.bundleHash)) {
1414
+ throw new Error("exported Experience semantics do not match the exact local bundle identity");
1415
+ }
1416
+ const etag = response.headers && typeof response.headers.get === "function" ? response.headers.get("etag") : null;
1417
+ if (etag !== `"${receipt.revision}"`) throw new Error("export ETag does not match the exact receipt revision");
1418
+ const outputPath = requestedOutput || path.resolve(options.cwd || process.cwd(), `${validation.bundle.bundleId}.agentlas-experience.json`);
1419
+ assertSafeExportTarget(outputPath, options.overwrite === true);
1420
+ writePrivateExportAtomic(outputPath, validation.canonicalJson, options.overwrite === true);
1421
+ if (row) persistRemoteReceipt(options.userDataDir, validation.bundle, receipt);
1422
+ return {
1423
+ outputPath,
1424
+ bundleId: validation.bundle.bundleId,
1425
+ bundleHash: validation.bundle.bundleHash,
1426
+ canonicalBytes: validation.canonicalBytes,
1427
+ uploadId: receipt.uploadId,
1428
+ status: receipt.status,
1429
+ revision: receipt.revision,
1430
+ authoritative: "server",
1431
+ };
1432
+ }
1433
+
1434
+ async function withdrawUpload(ref, options = {}) {
1435
+ const { row, validation } = resolveScopedStoredRecord(options.userDataDir, ref, options.cwd);
1436
+ const uploadId = row.remote?.uploadId;
1437
+ if (!uploadId) throw new Error("unpublish requires an exact locally observed server upload receipt");
1438
+ if (!row?.remote?.revision) throw new Error("withdraw requires the exact locally observed server revision; run experience status first");
1439
+ if (row.remote.status === "withdrawn") throw new Error("Experience upload is already withdrawn");
1440
+ const auth = await authenticatedContext(options, false);
1441
+ const response = await options.fetchHub(`${auth.base}/uploads/${encodeURIComponent(uploadId)}`, {
1442
+ method: "DELETE",
1443
+ headers: { accept: "application/json", cookie: auth.cookie, origin: auth.origin, "If-Match": `"${row.remote.revision}"` },
1444
+ });
1445
+ if (!response.ok) {
1446
+ if (response.status === 412) {
1447
+ const body = parseResponseJson(response, "Experience withdrawal conflict");
1448
+ const current = body.current?.receipt || body.current || body.receipt;
1449
+ if (current) {
1450
+ const bundle = validation.bundle;
1451
+ const receipt = validateUploadReceipt(current, bundle);
1452
+ persistRemoteReceipt(options.userDataDir, bundle, receipt);
1453
+ const error = new Error("Experience withdrawal revision is stale; current server receipt was reconciled locally. Review status and retry.");
1454
+ error.code = "experience_revision_conflict";
1455
+ error.status = 412;
1456
+ error.current = receipt;
1457
+ throw error;
1458
+ }
1459
+ }
1460
+ throw responseError(response, "Experience withdrawal (server support may be unavailable)");
1461
+ }
1462
+ const body = parseResponseJson(response, "Experience withdrawal");
1463
+ const bundle = validation.bundle;
1464
+ const receipt = validateUploadReceipt(body.receipt || body, bundle);
1465
+ if (receipt.uploadId !== uploadId || receipt.status !== "withdrawn") throw new Error("withdrawal did not return the exact withdrawn server receipt");
1466
+ const etag = response.headers && typeof response.headers.get === "function" ? response.headers.get("etag") : null;
1467
+ if (etag !== `"${receipt.revision}"`) throw new Error("withdrawal ETag does not match the new server revision");
1468
+ if (row) {
1469
+ persistRemoteReceipt(options.userDataDir, bundle, receipt);
1470
+ }
1471
+ return { receipt, authoritative: "server", publicActivation: false };
1472
+ }
1473
+
1474
+ const TASK_CLASS_KEYWORDS = Object.freeze({
1475
+ research: ["research", "investigate", "literature review", "market research", "리서치", "연구", "자료 조사"],
1476
+ writing: ["writing", "write article", "write copy", "copywriting", "blog post", "essay", "글쓰기", "글 작성", "카피 작성", "원고 작성"],
1477
+ coding: ["coding", "code implementation", "implement code", "write code", "source code", "코딩", "코드 구현", "코드 작성", "프로그래밍"],
1478
+ debugging: ["debug", "debugging", "bug fix", "fix bug", "troubleshoot", "error", "exception", "failure", "failed", "디버깅", "버그 수정", "오류", "오류 수정", "실패"],
1479
+ design: ["design", "ui design", "ux design", "wireframe", "디자인", "와이어프레임", "화면 설계"],
1480
+ "image-generation": ["image generation", "generate image", "create image", "text to image", "이미지 생성", "그림 생성"],
1481
+ "video-production": ["video production", "create video", "video editing", "영상 제작", "비디오 제작", "영상 편집"],
1482
+ presentation: ["presentation", "slide deck", "powerpoint", "ppt", "프레젠테이션", "발표 자료", "슬라이드", "피피티"],
1483
+ document: ["document", "docx", "pdf document", "document editing", "문서", "문서 작성", "문서 편집"],
1484
+ "data-analysis": ["data analysis", "analyze data", "analytics", "데이터 분석", "통계 분석"],
1485
+ "browser-automation": ["browser automation", "automate browser", "playwright", "브라우저 자동화", "웹 자동화"],
1486
+ "social-publishing": ["social publishing", "publish social", "post to instagram", "post to tiktok", "sns 게시", "소셜 게시", "인스타 업로드", "틱톡 업로드"],
1487
+ marketing: ["marketing", "campaign", "seo", "마케팅", "캠페인"],
1488
+ sales: ["sales", "lead generation", "sales outreach", "영업", "리드 발굴"],
1489
+ "customer-support": ["customer support", "customer service", "support ticket", "고객 지원", "고객 문의", "cs 응대"],
1490
+ ecommerce: ["ecommerce", "e commerce", "online store", "shopify", "이커머스", "온라인 쇼핑몰", "스마트스토어"],
1491
+ "legal-review": ["legal review", "contract review", "legal analysis", "법률 검토", "계약 검토", "법무 검토"],
1492
+ finance: ["finance", "financial analysis", "investment analysis", "재무", "금융", "투자 분석"],
1493
+ "project-planning": ["project planning", "project plan", "roadmap", "프로젝트 계획", "로드맵", "일정 계획"],
1494
+ "agent-building": ["agent building", "build agent", "create agent", "에이전트 빌드", "에이전트 만들", "에이전트 생성"],
1495
+ "workflow-automation": ["workflow automation", "automate workflow", "automation workflow", "워크플로 자동화", "업무 자동화"],
1496
+ "file-operations": ["file operations", "move files", "rename files", "organize files", "파일 작업", "파일 이동", "파일 이름 변경", "파일 정리"],
1497
+ translation: ["translation", "translate", "localization", "번역", "현지화"],
1498
+ });
1499
+
1500
+ function normalizeClassificationText(value) {
1501
+ return String(value || "")
1502
+ .normalize("NFKC")
1503
+ .toLowerCase()
1504
+ .replace(/[^a-z0-9가-힣]+/gi, " ")
1505
+ .replace(/\s+/g, " ")
1506
+ .trim();
1507
+ }
1508
+
1509
+ function normalizedTaxonomyAtom(value) {
1510
+ return typeof value === "string" ? value.normalize("NFKC").trim().toLowerCase() : "";
1511
+ }
1512
+
1513
+ function canonicalSourceTaskId(value) {
1514
+ const normalized = normalizedTaxonomyAtom(value);
1515
+ return normalized.startsWith(CANONICAL_TASK_PREFIX) && CANONICAL_TASK_ID_SET.has(normalized) ? normalized : null;
1516
+ }
1517
+
1518
+ function canonicalTaskId(value) {
1519
+ const normalized = normalizedTaxonomyAtom(value);
1520
+ const source = canonicalSourceTaskId(normalized);
1521
+ if (source) return source;
1522
+ const id = `${CANONICAL_TASK_PREFIX}${normalized}`;
1523
+ return CANONICAL_TASK_ID_SET.has(id) ? id : null;
1524
+ }
1525
+
1526
+ function isCanonicalTaskId(value) {
1527
+ return typeof value === "string" && CANONICAL_TASK_ID_SET.has(value);
1528
+ }
1529
+
1530
+ function keywordOccurs(normalizedPrompt, rawKeyword) {
1531
+ const keyword = normalizeClassificationText(rawKeyword);
1532
+ if (!keyword) return false;
1533
+ if (/[가-힣]/.test(keyword)) return normalizedPrompt.includes(keyword);
1534
+ return ` ${normalizedPrompt} `.includes(` ${keyword} `);
1535
+ }
1536
+
1537
+ function deriveCanonicalTaskClasses(prompt, options = {}) {
1538
+ const declaredRaw = options.declaredTaskClasses ?? options.declaredTaskClass;
1539
+ if (declaredRaw != null && (Array.isArray(declaredRaw) ? declaredRaw.length : String(declaredRaw).trim())) {
1540
+ const declared = (Array.isArray(declaredRaw) ? declaredRaw : [declaredRaw]).map(String);
1541
+ const taskIds = [...new Set(declared.map(canonicalTaskId).filter(Boolean))];
1542
+ const invalidDeclared = declared.filter((value) => !canonicalTaskId(value));
1543
+ return {
1544
+ taskIds: CANONICAL_TASK_IDS.filter((id) => taskIds.includes(id)),
1545
+ source: "declared-task-class",
1546
+ matchedTaskClasses: CANONICAL_TASK_IDS.filter((id) => taskIds.includes(id)),
1547
+ invalidDeclaredCount: invalidDeclared.length,
1548
+ };
1549
+ }
1550
+ const normalizedPrompt = normalizeClassificationText(prompt);
1551
+ const matches = [];
1552
+ for (const slug of CANONICAL_TASK_SLUGS) {
1553
+ if ((TASK_CLASS_KEYWORDS[slug] || []).some((keyword) => keywordOccurs(normalizedPrompt, keyword))) {
1554
+ matches.push(`${CANONICAL_TASK_PREFIX}${slug}`);
1555
+ }
1556
+ }
1557
+ return { taskIds: matches, source: "deterministic-keyword-map", matchedTaskClasses: matches, invalidDeclaredCount: 0 };
1558
+ }
1559
+
1560
+ function parseEnvironmentConstraint(value) {
1561
+ const normalized = normalizedTaxonomyAtom(value);
1562
+ const contract = EXPERIENCE_TAXONOMY_V1.environment;
1563
+ if (normalized.startsWith(contract.osPrefix)) {
1564
+ const selected = normalized.slice(contract.osPrefix.length);
1565
+ return CANONICAL_OS_VALUES.has(selected) ? { dimension: "os", value: selected } : null;
1566
+ }
1567
+ if (normalized.startsWith(contract.archPrefix)) {
1568
+ const selected = normalized.slice(contract.archPrefix.length);
1569
+ return CANONICAL_ARCH_VALUES.has(selected) ? { dimension: "arch", value: selected } : null;
1570
+ }
1571
+ if (normalized.startsWith(contract.runtimePrefix)) {
1572
+ const selected = normalized.slice(contract.runtimePrefix.length);
1573
+ return CANONICAL_RUNTIME_RE.test(selected) ? { dimension: "runtime", value: selected } : null;
1574
+ }
1575
+ return null;
1576
+ }
1577
+
1578
+ function isCanonicalEnvironmentTag(value) {
1579
+ return Boolean(parseEnvironmentConstraint(value));
1580
+ }
1581
+
1582
+ function defaultEnvironmentTags(options = {}) {
1583
+ const platform = options.platform || process.platform;
1584
+ const arch = options.arch || process.arch;
1585
+ const platformCandidate = normalizedTaxonomyAtom(platform === "darwin" ? "macos" : platform === "win32" ? "windows" : platform);
1586
+ const archCandidate = normalizedTaxonomyAtom(arch === "x86_64" ? "x64" : arch === "aarch64" ? "arm64" : arch);
1587
+ const runtimeCandidate = normalizedTaxonomyAtom(typeof options.runtime === "string" ? options.runtime : typeof options.runtimeTag === "string" ? options.runtimeTag : "terminal");
1588
+ const platformName = CANONICAL_OS_VALUES.has(platformCandidate) ? platformCandidate : "unknown";
1589
+ const archName = CANONICAL_ARCH_VALUES.has(archCandidate) ? archCandidate : "unknown";
1590
+ const runtimeName = CANONICAL_RUNTIME_RE.test(runtimeCandidate) ? runtimeCandidate : "unknown";
1591
+ return [
1592
+ `${EXPERIENCE_TAXONOMY_V1.environment.osPrefix}${platformName}`,
1593
+ `${EXPERIENCE_TAXONOMY_V1.environment.archPrefix}${archName}`,
1594
+ `${EXPERIENCE_TAXONOMY_V1.environment.runtimePrefix}${runtimeName}`,
1595
+ ];
1596
+ }
1597
+
1598
+ function environmentConstraintsMatch(constraints, environment) {
1599
+ const actual = {
1600
+ os: normalizedTaxonomyAtom(environment?.os),
1601
+ arch: normalizedTaxonomyAtom(environment?.arch),
1602
+ runtime: normalizedTaxonomyAtom(environment?.runtime),
1603
+ };
1604
+ if (!CANONICAL_OS_VALUES.has(actual.os) || !CANONICAL_ARCH_VALUES.has(actual.arch) || !CANONICAL_RUNTIME_RE.test(actual.runtime)) return false;
1605
+ if (actual.os === "unknown" || actual.arch === "unknown" || actual.runtime === "unknown") return false;
1606
+ return (constraints || []).every((raw) => {
1607
+ const parsed = parseEnvironmentConstraint(raw);
1608
+ return Boolean(parsed && actual[parsed.dimension] === parsed.value);
1609
+ });
1610
+ }
1611
+
1612
+ function selectApplicablePortableItems(input = {}) {
1613
+ const profile = new Set([input.taskClass, ...(input.capabilityTags || [])].map(canonicalTaskId).filter(Boolean));
1614
+ if (!profile.size) return [];
1615
+ const eligible = (input.items || []).filter((item) => {
1616
+ if (!item || ["deprecated", "rejected"].includes(item.status)) return false;
1617
+ if (!(item.taskSignatures || []).map(canonicalSourceTaskId).filter(Boolean).some((task) => profile.has(task))) return false;
1618
+ return environmentConstraintsMatch(item.environmentConstraints || [], input.environment || {});
1619
+ });
1620
+ const superseded = new Set(eligible.flatMap((item) => item.supersedesItemIds || []));
1621
+ return eligible
1622
+ .filter((item) => typeof item.experienceItemId === "string" && !superseded.has(item.experienceItemId))
1623
+ .map((item) => item.experienceItemId);
1624
+ }
1625
+
1626
+ function readExactLocalBaseMarker(agentRoot, expectedSlug = null) {
1627
+ if (!agentRoot) return { marker: null, reason: "exact-local-base-marker-unavailable" };
1628
+ const file = path.join(path.resolve(agentRoot), ".agentlas-cloud-package.json");
1629
+ try {
1630
+ const stat = fs.lstatSync(file);
1631
+ if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 256 * 1024) {
1632
+ return { marker: null, reason: "exact-local-base-marker-unsafe" };
1633
+ }
1634
+ const raw = JSON.parse(fs.readFileSync(file, "utf8"));
1635
+ const slug = String(raw.slug || expectedSlug || "").trim();
1636
+ const packageHashRaw = String(raw.packageHash || "").replace(/^sha256:/i, "").toLowerCase();
1637
+ const packageHashVersion = String(raw.packageHashVersion || "");
1638
+ const cloudId = raw.cloudId == null ? null : String(raw.cloudId);
1639
+ if (
1640
+ !/^[a-z0-9][a-z0-9._-]{0,95}$/.test(slug) ||
1641
+ (expectedSlug && slug !== expectedSlug) ||
1642
+ !/^[a-f0-9]{64}$/.test(packageHashRaw) ||
1643
+ !["path-sha256-v1", "path-sha256-executable-v2"].includes(packageHashVersion) ||
1644
+ (cloudId && !ID_RE.test(cloudId))
1645
+ ) return { marker: null, reason: "exact-local-base-marker-invalid" };
1646
+ return {
1647
+ marker: {
1648
+ slug,
1649
+ cloudId,
1650
+ packageHash: `sha256:${packageHashRaw}`,
1651
+ packageHashVersion,
1652
+ },
1653
+ reason: null,
1654
+ };
1655
+ } catch (error) {
1656
+ return {
1657
+ marker: null,
1658
+ reason: error?.code === "ENOENT" ? "exact-local-base-marker-unavailable" : "exact-local-base-marker-invalid",
1659
+ };
1660
+ }
1661
+ }
1662
+
1663
+ function exactTaskSignatureInPrompt(signature, prompt, options = {}) {
1664
+ if (!isCanonicalTaskId(signature)) return false;
1665
+ return deriveCanonicalTaskClasses(prompt, options).taskIds.includes(signature);
1666
+ }
1667
+
1668
+ /**
1669
+ * Resolve normal Terminal runs without fuzzy identity or semantic guessing.
1670
+ * Automatic retrieval additionally needs one exact Experience release selected
1671
+ * by an authoritative loadout. Merely saving/uploading a compatible bundle is
1672
+ * never attachment consent.
1673
+ */
1674
+ function resolveRuntimeExperienceForAgent(options = {}) {
1675
+ const requested = options.requested || {};
1676
+ if (requested.disabled === true) {
1677
+ return { disabled: true, observableReason: "disabled-by-user", resolution: "skipped" };
1678
+ }
1679
+ const environmentTags = defaultEnvironmentTags(options);
1680
+ if (environmentTags.some((tag) => tag.endsWith("/unknown"))) {
1681
+ return { disabled: true, observableReason: "runtime-environment-unknown", resolution: "skipped" };
1682
+ }
1683
+ if (Array.isArray(requested.environmentTags) && requested.environmentTags.length) {
1684
+ const declaredEnvironment = [...new Set(requested.environmentTags.map(String).filter(Boolean))];
1685
+ const exactDefault = declaredEnvironment.length === environmentTags.length && declaredEnvironment.every((tag) => environmentTags.includes(tag));
1686
+ if (!declaredEnvironment.every(isCanonicalEnvironmentTag)) {
1687
+ return { disabled: true, observableReason: "legacy-environment-constraint-not-runtime-activatable", resolution: "skipped" };
1688
+ }
1689
+ if (!exactDefault) {
1690
+ return { disabled: true, observableReason: "declared-environment-does-not-match-runtime", resolution: "skipped" };
1691
+ }
1692
+ }
1693
+ const explicitBase = String(requested.baseAgentReleaseId || "");
1694
+ const explicitSignatures = [...new Set((requested.taskSignatures || []).map(String).filter(Boolean))];
1695
+ const explicitPackReleases = [...new Set((requested.experiencePackReleaseIds || []).map(String).filter(Boolean))];
1696
+ if (explicitBase || explicitSignatures.length || requested.agentDefinitionId || explicitPackReleases.length) {
1697
+ if (!ID_RE.test(explicitBase) || !explicitSignatures.length || explicitPackReleases.length !== 1 || !ID_RE.test(explicitPackReleases[0])) {
1698
+ return { disabled: true, observableReason: "incomplete-explicit-experience-binding", resolution: "skipped" };
1699
+ }
1700
+ if (explicitSignatures.some((item) => !isCanonicalTaskId(item))) {
1701
+ return { disabled: true, observableReason: "legacy-task-signature-not-runtime-activatable", resolution: "skipped" };
1702
+ }
1703
+ return {
1704
+ disabled: false,
1705
+ baseAgentReleaseId: explicitBase,
1706
+ ...(requested.agentDefinitionId && ID_RE.test(String(requested.agentDefinitionId)) ? { agentDefinitionId: String(requested.agentDefinitionId) } : {}),
1707
+ experiencePackReleaseIds: explicitPackReleases,
1708
+ taskSignatures: explicitSignatures,
1709
+ environmentTags,
1710
+ resolution: "explicit-exact",
1711
+ };
1712
+ }
1713
+ const agent = options.agent;
1714
+ if (!agent || agent.builtin || !agent.slug) {
1715
+ return { disabled: true, observableReason: agent?.builtin ? "builtin-agent-has-no-owned-experience-base" : "no-exact-agent-base", resolution: "skipped" };
1716
+ }
1717
+ const local = readExactLocalBaseMarker(options.agentRoot, agent.slug);
1718
+ if (!local.marker) return { disabled: true, observableReason: local.reason, resolution: "skipped" };
1719
+ const attachedPackReleases = [...new Set(
1720
+ (requested.attachedExperiencePackReleaseIds || []).map(String).filter(Boolean),
1721
+ )];
1722
+ if (attachedPackReleases.length !== 1 || !ID_RE.test(attachedPackReleases[0])) {
1723
+ return { disabled: true, observableReason: "explicit-experience-attachment-required", resolution: "skipped" };
1724
+ }
1725
+ let state;
1726
+ try { state = loadExchangeState(options.userDataDir); }
1727
+ catch { return { disabled: true, observableReason: "local-experience-state-invalid", resolution: "skipped" }; }
1728
+ const scopeHash = projectScopeHash(options.cwd);
1729
+ const matchingRows = state.bundles.filter((row) => {
1730
+ const base = row.remote?.baseResolution;
1731
+ return attachedPackReleases.includes(row.experiencePackReleaseId) &&
1732
+ row.projectScopeHash === scopeHash && base &&
1733
+ base.slug === local.marker.slug &&
1734
+ (!local.marker.cloudId || base.cloudId === local.marker.cloudId) &&
1735
+ base.packageHash === local.marker.packageHash &&
1736
+ base.packageHashVersion === local.marker.packageHashVersion &&
1737
+ base.agentDefinitionId === row.agentDefinitionId &&
1738
+ row.compatibleBaseReleaseIds.includes(base.agentReleaseId);
1739
+ });
1740
+ if (!matchingRows.length) {
1741
+ return { disabled: true, observableReason: "exact-local-base-release-unavailable", resolution: "skipped" };
1742
+ }
1743
+ const baseKeys = new Set(matchingRows.map((row) => {
1744
+ const base = row.remote.baseResolution;
1745
+ return `${base.agentDefinitionId}\0${base.agentReleaseId}\0${base.packageHash}`;
1746
+ }));
1747
+ if (baseKeys.size !== 1) {
1748
+ return { disabled: true, observableReason: "ambiguous-exact-base-release", resolution: "skipped" };
1749
+ }
1750
+ const base = matchingRows[0].remote.baseResolution;
1751
+ const taskClassResolution = deriveCanonicalTaskClasses(options.prompt, {
1752
+ declaredTaskClasses: requested.declaredTaskClasses ?? options.declaredTaskClasses ?? options.declaredTaskClass,
1753
+ });
1754
+ if (taskClassResolution.invalidDeclaredCount) {
1755
+ return { disabled: true, observableReason: "invalid-declared-task-class", resolution: "skipped" };
1756
+ }
1757
+ if (!taskClassResolution.taskIds.length) {
1758
+ return { disabled: true, observableReason: "canonical-task-class-unresolved", resolution: "skipped" };
1759
+ }
1760
+ const environment = new Set(environmentTags);
1761
+ const classifiedTasks = new Set(taskClassResolution.taskIds);
1762
+ const taskSignatures = new Set();
1763
+ let sawPromotedItem = false;
1764
+ let sawCanonicalSignature = false;
1765
+ let sawMatchingCanonicalTask = false;
1766
+ let sawLegacyEnvironmentForMatch = false;
1767
+ let sawCanonicalEnvironmentMismatch = false;
1768
+ for (const row of matchingRows) {
1769
+ let bundle;
1770
+ try { bundle = readStoredBundle(options.userDataDir, row.bundleId).bundle; }
1771
+ catch { continue; }
1772
+ for (const item of bundle.items) {
1773
+ if (item.status !== "promoted") continue;
1774
+ sawPromotedItem = true;
1775
+ const canonicalSignatures = item.taskSignatures.filter(isCanonicalTaskId);
1776
+ if (canonicalSignatures.length) sawCanonicalSignature = true;
1777
+ const matchedSignatures = canonicalSignatures.filter((signature) => classifiedTasks.has(signature));
1778
+ if (!matchedSignatures.length) continue;
1779
+ sawMatchingCanonicalTask = true;
1780
+ if (!item.environmentConstraints.every(isCanonicalEnvironmentTag)) {
1781
+ sawLegacyEnvironmentForMatch = true;
1782
+ continue;
1783
+ }
1784
+ if (!item.environmentConstraints.every((constraint) => environment.has(constraint))) {
1785
+ sawCanonicalEnvironmentMismatch = true;
1786
+ continue;
1787
+ }
1788
+ for (const signature of matchedSignatures) taskSignatures.add(signature);
1789
+ }
1790
+ }
1791
+ if (!taskSignatures.size) {
1792
+ const observableReason = sawPromotedItem && !sawCanonicalSignature
1793
+ ? "legacy-task-signature-not-auto-activatable"
1794
+ : sawMatchingCanonicalTask && sawLegacyEnvironmentForMatch
1795
+ ? "legacy-environment-constraint-not-auto-activatable"
1796
+ : sawMatchingCanonicalTask && sawCanonicalEnvironmentMismatch
1797
+ ? "canonical-environment-constraint-mismatch"
1798
+ : "canonical-task-signature-unavailable";
1799
+ return {
1800
+ disabled: true,
1801
+ observableReason,
1802
+ resolution: "skipped",
1803
+ taskClassResolution,
1804
+ };
1805
+ }
1806
+ return {
1807
+ disabled: false,
1808
+ baseAgentReleaseId: base.agentReleaseId,
1809
+ agentDefinitionId: base.agentDefinitionId,
1810
+ experiencePackReleaseIds: attachedPackReleases,
1811
+ taskSignatures: [...taskSignatures].sort(compareCodePoints),
1812
+ environmentTags,
1813
+ resolution: "automatic-exact",
1814
+ taskClassResolution,
1815
+ };
1816
+ }
1817
+
1818
+ function estimateTokens(text) {
1819
+ return Math.ceil(Buffer.byteLength(String(text || ""), "utf8") / 3);
1820
+ }
1821
+
1822
+ function buildLocalExperienceAdvisory(options = {}) {
1823
+ const empty = { text: "", itemIds: [], estimatedTokens: 0, authority: "local-advisory", serverRentalResolutionReceiptPresent: false };
1824
+ if (!options.userDataDir || !options.cwd || !ID_RE.test(String(options.baseAgentReleaseId || ""))) return empty;
1825
+ const experiencePackReleaseIds = new Set(
1826
+ (options.experiencePackReleaseIds || []).map(String).filter((value) => ID_RE.test(value)),
1827
+ );
1828
+ if (experiencePackReleaseIds.size !== 1) return empty;
1829
+ const taskSignatures = new Set((options.taskSignatures || []).map(String).filter(isCanonicalTaskId));
1830
+ if (!taskSignatures.size) return empty;
1831
+ const resolvedEnvironmentTags = (options.environmentTags || defaultEnvironmentTags(options)).map(String).filter(isCanonicalEnvironmentTag);
1832
+ if (resolvedEnvironmentTags.some((tag) => tag.endsWith("/unknown"))) return empty;
1833
+ const environmentTags = new Set(resolvedEnvironmentTags);
1834
+ const state = loadExchangeState(options.userDataDir);
1835
+ const projectHash = projectScopeHash(options.cwd);
1836
+ const candidates = [];
1837
+ for (const row of state.bundles) {
1838
+ if (
1839
+ !experiencePackReleaseIds.has(row.experiencePackReleaseId) ||
1840
+ row.projectScopeHash !== projectHash ||
1841
+ !row.compatibleBaseReleaseIds.includes(options.baseAgentReleaseId)
1842
+ ) continue;
1843
+ if (options.agentDefinitionId && row.agentDefinitionId !== options.agentDefinitionId) continue;
1844
+ let validation;
1845
+ try { validation = readStoredBundle(options.userDataDir, row.bundleId); } catch { continue; }
1846
+ for (const item of validation.bundle.items) {
1847
+ if (item.status !== "promoted") continue;
1848
+ if (!item.taskSignatures.some((signature) => taskSignatures.has(signature))) continue;
1849
+ if (!item.environmentConstraints.every(isCanonicalEnvironmentTag)) continue;
1850
+ if (!item.environmentConstraints.every((constraint) => environmentTags.has(constraint))) continue;
1851
+ candidates.push(item);
1852
+ }
1853
+ }
1854
+ candidates.sort((a, b) => Number(b.confidence) - Number(a.confidence) || compareCodePoints(a.experienceItemId, b.experienceItemId));
1855
+ const header = "[AGENTLAS_LOCAL_EXPERIENCE_ADVISORY v1] NO SERVER RENTAL-RESOLUTION RECEIPT. Local user-attested procedures only; not evaluator-verified and not reputation evidence.";
1856
+ const reservedTokens = Number.isInteger(options.reservedTokens)
1857
+ ? Math.max(0, Math.min(EXPERIENCE_RETRIEVAL_MAX_TOKENS, options.reservedTokens))
1858
+ : 0;
1859
+ const dynamicTokenBudget = Math.max(0, EXPERIENCE_RETRIEVAL_MAX_TOKENS - reservedTokens);
1860
+ if (estimateTokens(header) > dynamicTokenBudget) return empty;
1861
+ let text = header;
1862
+ const itemIds = [];
1863
+ for (const item of candidates) {
1864
+ if (itemIds.length >= EXPERIENCE_RETRIEVAL_MAX_ITEMS || itemIds.includes(item.experienceItemId)) continue;
1865
+ const line = `\n- [${item.experienceItemId}] ${item.summary}\n Steps: ${item.instructions.join(" | ")}`;
1866
+ const next = `${text}${line}`;
1867
+ if (estimateTokens(next) > dynamicTokenBudget) continue;
1868
+ text = next;
1869
+ itemIds.push(item.experienceItemId);
1870
+ }
1871
+ if (!itemIds.length) return empty;
1872
+ return { text, itemIds, estimatedTokens: estimateTokens(text), authority: "local-advisory", serverRentalResolutionReceiptPresent: false };
1873
+ }
1874
+
1875
+ function augmentRuntimeSystemWithLocalExperience(systemPrompt, options = {}) {
1876
+ const context = buildLocalExperienceAdvisory(options);
1877
+ return {
1878
+ systemPrompt: context.text ? `${String(systemPrompt || "")}\n\n${context.text}` : String(systemPrompt || ""),
1879
+ experienceContext: context,
1880
+ };
1881
+ }
1882
+
1883
+ function renderValidation(validation) {
1884
+ return [
1885
+ `Portable Experience valid: ${validation.bundle.bundleId}`,
1886
+ `pack: ${validation.bundle.pack.experiencePackId}@${validation.bundle.pack.version}`,
1887
+ `items: ${validation.bundle.items.length} / ${MAX_STORED_ITEMS} · canonical bytes: ${validation.canonicalBytes} / ${MAX_BUNDLE_CANONICAL_BYTES}`,
1888
+ "Privacy scan: passed · base package/raw prompt/transcript/path/credential material: absent",
1889
+ "Authority: local deterministic validation only; no server receipt, evaluator, reputation, Variant, or public activation.",
1890
+ ].join("\n");
1891
+ }
1892
+
1893
+ function renderPublish(result) {
1894
+ if (result.dryRun) return `DRY RUN · ${result.bundleId} validated · network used: no · server state unchanged · public activation: not performed`;
1895
+ return [
1896
+ `Server-authoritative Experience upload receipt: ${result.receipt.uploadId}`,
1897
+ `state: ${result.receipt.status} · requested visibility: ${result.receipt.requestedVisibility}`,
1898
+ `idempotency: ${result.replayed ? "same receipt replayed" : "first accepted receipt"}${result.recovered ? " · recovered after lost response" : ""}`,
1899
+ "Public activation/evaluator verification/reputation: NOT performed or claimed.",
1900
+ ].join("\n");
1901
+ }
1902
+
1903
+ function publicUploadReceipt(receipt) {
1904
+ return {
1905
+ schema: receipt.schema,
1906
+ uploadId: receipt.uploadId,
1907
+ bundleId: receipt.bundleId,
1908
+ bundleHash: receipt.bundleHash,
1909
+ experiencePackId: receipt.experiencePackId,
1910
+ experienceReleaseId: receipt.experienceReleaseId,
1911
+ status: receipt.status,
1912
+ requestedVisibility: receipt.requestedVisibility,
1913
+ revision: receipt.revision,
1914
+ createdAt: receipt.createdAt,
1915
+ updatedAt: receipt.updatedAt,
1916
+ ...(receipt.errorCode ? { errorCode: receipt.errorCode } : {}),
1917
+ };
1918
+ }
1919
+
1920
+ function publicCommandExchangeResult(result) {
1921
+ return {
1922
+ ...(result.receipt ? { receipt: publicUploadReceipt(result.receipt) } : {}),
1923
+ ...(BUNDLE_ID_RE.test(String(result.bundleId || "")) ? { bundleId: result.bundleId } : {}),
1924
+ ...(HASH_RE.test(String(result.bundleHash || "")) ? { bundleHash: result.bundleHash } : {}),
1925
+ ...(["private", "unlisted", "public"].includes(result.requestedVisibility) ? { requestedVisibility: result.requestedVisibility } : {}),
1926
+ ...(typeof result.replayed === "boolean" ? { replayed: result.replayed } : {}),
1927
+ ...(typeof result.recovered === "boolean" ? { recovered: result.recovered } : {}),
1928
+ ...(typeof result.dryRun === "boolean" ? { dryRun: result.dryRun } : {}),
1929
+ ...(typeof result.networkUsed === "boolean" ? { networkUsed: result.networkUsed } : {}),
1930
+ ...(typeof result.authoritative === "string" ? { authoritative: result.authoritative } : {}),
1931
+ publicActivation: false,
1932
+ evaluatorAuthority: false,
1933
+ };
1934
+ }
1935
+
1936
+ function baseDescriptorFromFlags(flags) {
1937
+ return {
1938
+ ...(flags["base-slug"] ? { slug: flags["base-slug"] } : {}),
1939
+ ...(flags["base-cloud-id"] ? { cloudId: flags["base-cloud-id"] } : {}),
1940
+ ...(flags["base-package-hash"] ? { packageHash: flags["base-package-hash"] } : {}),
1941
+ ...(flags["base-package-hash-version"] ? { packageHashVersion: flags["base-package-hash-version"] } : {}),
1942
+ };
1943
+ }
1944
+
1945
+ async function cmdExperienceExchange(options = {}) {
1946
+ const args = options.args || [];
1947
+ const sub = args[0] || "list";
1948
+ const flags = parseFlags(args.slice(1));
1949
+ const emit = options.out || console.log;
1950
+ if (!options.userDataDir) throw new Error("Terminal userData path is required");
1951
+
1952
+ if (sub === "help" || sub === "--help" || sub === "-h") {
1953
+ const help = [
1954
+ "agentlas experience list",
1955
+ "agentlas experience inspect <exact-release-id|bundle-id|upload-id>",
1956
+ "agentlas experience validate <bundle.agentlas-experience.json>",
1957
+ "agentlas experience save <bundle> --base-cloud-id <id>|--base-slug <slug> --base-package-hash sha256:<hash>",
1958
+ "agentlas experience publish <bundle> --visibility unlisted|public --base-cloud-id <id>|--base-slug <slug> --base-package-hash sha256:<hash>",
1959
+ "agentlas experience status <bundle-id|upload-id>",
1960
+ "agentlas experience unpublish <exact-release-id|bundle-id|upload-id> [--dry-run]",
1961
+ "agentlas experience withdraw <bundle-id|upload-id>",
1962
+ "agentlas experience export <bundle-id|upload-id> [--out file] [--overwrite]",
1963
+ "Options: --dry-run (zero network/write), --idempotency-key <safe-key>, save --local-only",
1964
+ "Legacy pack-only local intents: legacy-list|legacy-inspect|legacy-publish|legacy-unpublish",
1965
+ "publish requests verification only; Terminal never claims evaluator verification or public activation.",
1966
+ ].join("\n");
1967
+ emit(help);
1968
+ return { help: true };
1969
+ }
1970
+
1971
+ if (["legacy-list", "legacy-inspect", "legacy-publish", "legacy-unpublish"].includes(sub)) {
1972
+ if (typeof options.legacyCommand !== "function") throw new Error("legacy local-intent Experience handler is unavailable");
1973
+ const mapped = sub.slice("legacy-".length);
1974
+ return options.legacyCommand({ ...options, args: [mapped, ...args.slice(1)] });
1975
+ }
1976
+ if (sub === "list" || sub === "ls") {
1977
+ if (flags._.length) throw new Error("usage: agentlas experience list [--json]");
1978
+ const bundles = listStoredExperienceBundles(options.userDataDir, options.cwd);
1979
+ const result = {
1980
+ schemaVersion: "agentlas.terminal-experience-local-list.v1",
1981
+ currentProjectOnly: true,
1982
+ networkUsed: false,
1983
+ bundles,
1984
+ };
1985
+ const lines = bundles.length
1986
+ ? ["LOCAL PORTABLE EXPERIENCE BUNDLES · current project only · no network", ...bundles.map((bundle) =>
1987
+ `- ${bundle.experiencePackId}@${bundle.experiencePackReleaseId} · ${bundle.itemCount} item(s) · ${bundle.reviewState} · Hub: ${bundle.remote ? `${bundle.remote.status} (${bundle.remote.uploadId})` : "not submitted"}`)]
1988
+ : ["No Portable Experience Bundles are stored for this project.", "Hub was not contacted."];
1989
+ emit(flags.json ? JSON.stringify(result, null, 2) : lines.join("\n"));
1990
+ return result;
1991
+ }
1992
+ if (sub === "inspect" || sub === "show") {
1993
+ const ref = flags._[0];
1994
+ if (!ref || flags._.length !== 1) throw new Error("usage: agentlas experience inspect <exact-release-id|bundle-id|upload-id>");
1995
+ const bundle = inspectStoredExperienceBundle(options.userDataDir, ref, options.cwd);
1996
+ const result = { ...bundle, networkUsed: false };
1997
+ emit(flags.json ? JSON.stringify(result, null, 2) : [
1998
+ `${bundle.experiencePackId}@${bundle.experiencePackReleaseId}`,
1999
+ `bundle: ${bundle.bundleId} · ${bundle.itemCount} item(s) · local integrity: verified`,
2000
+ `review state: ${bundle.reviewState} · candidates ${bundle.itemStatusCounts.candidate} · promoted ${bundle.itemStatusCounts.promoted}`,
2001
+ `compatible base releases: ${bundle.compatibleBaseReleaseIds.join(", ")}`,
2002
+ bundle.remote
2003
+ ? `Hub receipt: ${bundle.remote.status} · ${bundle.remote.uploadId} · exact revision ${bundle.remote.revision}`
2004
+ : "Hub receipt: none · not submitted",
2005
+ "Owner/account, local path, raw content, prompt, transcript, and credentials are intentionally omitted.",
2006
+ "Public activation/evaluator authority: not claimed.",
2007
+ ].join("\n"));
2008
+ return result;
2009
+ }
2010
+ if (sub === "validate") {
2011
+ const validation = readBundleFile(flags._[0], options.cwd);
2012
+ const result = { valid: true, bundleId: validation.bundle.bundleId, bundleHash: validation.bundle.bundleHash, packContentHash: validation.bundle.pack.contentHash, items: validation.bundle.items.length, canonicalBytes: validation.canonicalBytes, networkUsed: false, authority: "local-validation" };
2013
+ emit(flags.json ? JSON.stringify(result, null, 2) : renderValidation(validation));
2014
+ return result;
2015
+ }
2016
+ if (sub === "save") {
2017
+ const validation = readBundleFile(flags._[0], options.cwd);
2018
+ if (flags["local-only"] === true) {
2019
+ if (flags["dry-run"] === true) {
2020
+ const result = { dryRun: true, saved: false, networkUsed: false, bundleId: validation.bundle.bundleId };
2021
+ emit(flags.json ? JSON.stringify(result, null, 2) : `DRY RUN · ${validation.bundle.bundleId} validated · no file saved · network used: no`);
2022
+ return result;
2023
+ }
2024
+ const row = saveLocalBundle(options.userDataDir, validation, { cwd: options.cwd });
2025
+ const result = { saved: true, localOnly: true, networkUsed: false, bundleId: row.bundleId, projectScopeHash: row.projectScopeHash, serverReceiptPresent: false };
2026
+ emit(flags.json ? JSON.stringify(result, null, 2) : `Local 0600 Experience bundle saved: ${row.bundleId}\nHub: not contacted · server receipt: none · public activation: none`);
2027
+ return result;
2028
+ }
2029
+ const result = await publishBundle(validation, {
2030
+ ...options,
2031
+ operation: "save",
2032
+ dryRun: flags["dry-run"] === true,
2033
+ idempotencyKey: flags["idempotency-key"] || null,
2034
+ baseDescriptor: baseDescriptorFromFlags(flags),
2035
+ });
2036
+ emit(flags.json ? JSON.stringify(publicCommandExchangeResult(result), null, 2) : renderPublish(result));
2037
+ return result;
2038
+ }
2039
+ if (sub === "publish") {
2040
+ const source = flags._[0];
2041
+ const validation = resolveBundleInput(options.userDataDir, source, options.cwd);
2042
+ const result = await publishBundle(validation, {
2043
+ ...options,
2044
+ operation: "publish",
2045
+ requestedVisibility: flags.visibility || validation.bundle.requestedVisibility,
2046
+ dryRun: flags["dry-run"] === true,
2047
+ idempotencyKey: flags["idempotency-key"] || null,
2048
+ baseDescriptor: baseDescriptorFromFlags(flags),
2049
+ });
2050
+ emit(flags.json ? JSON.stringify(publicCommandExchangeResult(result), null, 2) : renderPublish(result));
2051
+ return result;
2052
+ }
2053
+ if (sub === "status") {
2054
+ const result = await fetchUploadStatus(flags._[0], options);
2055
+ emit(flags.json ? JSON.stringify(publicCommandExchangeResult(result), null, 2) : `Server-authoritative status: ${result.receipt.status} · ${result.receipt.uploadId}\nrequested visibility: ${result.receipt.requestedVisibility} · Terminal did not assert public activation/evaluator reputation`);
2056
+ return result;
2057
+ }
2058
+ if (sub === "export") {
2059
+ const result = await fetchUploadExport(flags._[0], {
2060
+ ...options,
2061
+ outputPath: typeof flags.out === "string" ? flags.out : null,
2062
+ overwrite: flags.overwrite === true,
2063
+ });
2064
+ // Intentionally omit owner/account fields and bundle content from stdout.
2065
+ emit(flags.json ? JSON.stringify(result, null, 2) : `Experience exported: ${result.outputPath}\nbundle hash: ${result.bundleHash}`);
2066
+ return result;
2067
+ }
2068
+ if (sub === "withdraw" || sub === "unpublish") {
2069
+ if (!flags._[0] || flags._.length !== 1) throw new Error("usage: agentlas experience unpublish <exact-release-id|bundle-id|upload-id> [--dry-run]");
2070
+ if (flags["dry-run"] === true) {
2071
+ const result = previewWithdrawUpload(flags._[0], options);
2072
+ emit(flags.json ? JSON.stringify(result, null, 2) : `DRY RUN · exact upload ${result.uploadId} at ${result.ifMatchRevision}\nnetwork/write used: no · server state unchanged · no new receipt`);
2073
+ return result;
2074
+ }
2075
+ const result = await withdrawUpload(flags._[0], options);
2076
+ emit(flags.json ? JSON.stringify(publicCommandExchangeResult(result), null, 2) : `Server-authoritative unpublication: ${result.receipt.uploadId} · withdrawn\nExisting receipts/history remain; no public activation claim.`);
2077
+ return result;
2078
+ }
2079
+ throw new Error("unknown experience subcommand (list|inspect|validate|save|publish|status|export|unpublish|withdraw; legacy: legacy-list|legacy-inspect|legacy-publish|legacy-unpublish)");
2080
+ }
2081
+
2082
+ module.exports = {
2083
+ BUNDLE_SCHEMA,
2084
+ RECEIPT_SCHEMA,
2085
+ MAX_BUNDLE_CANONICAL_BYTES,
2086
+ MAX_STORED_ITEMS,
2087
+ EXPERIENCE_RETRIEVAL_MAX_ITEMS,
2088
+ EXPERIENCE_RETRIEVAL_MAX_TOKENS,
2089
+ EXPERIENCE_TAXONOMY_V1,
2090
+ EXPERIENCE_TAXONOMY_CHECKSUM,
2091
+ CANONICAL_TASK_PREFIX,
2092
+ CANONICAL_ENV_PREFIX,
2093
+ CANONICAL_TASK_SLUGS,
2094
+ CANONICAL_TASK_IDS,
2095
+ ExperienceBundleValidationError,
2096
+ canonicalJson,
2097
+ canonicalHash,
2098
+ normalizeExperienceBundle,
2099
+ experiencePackContentPayload,
2100
+ experiencePackContentHash,
2101
+ experienceBundleHashPayload,
2102
+ experienceBundleHash,
2103
+ experienceBundleId,
2104
+ validateExperienceBundle,
2105
+ validateUploadReceipt,
2106
+ readBundleFile,
2107
+ recoverPrivateAtomicTarget,
2108
+ replacePrivateFileAtomic,
2109
+ exchangeStatePath,
2110
+ bundleStorePath,
2111
+ loadExchangeState,
2112
+ withExchangeStateLock,
2113
+ saveLocalBundle,
2114
+ commitServerAcceptedBundle,
2115
+ readStoredBundle,
2116
+ verifyStoredBundleRow,
2117
+ scopedStateRows,
2118
+ resolveScopedStoredRecord,
2119
+ listStoredExperienceBundles,
2120
+ inspectStoredExperienceBundle,
2121
+ previewWithdrawUpload,
2122
+ publicUploadReceipt,
2123
+ publicCommandExchangeResult,
2124
+ projectScopeHash,
2125
+ idempotencyKeyForBundle,
2126
+ idempotencyKeyHash,
2127
+ trustedExperienceOrigin,
2128
+ publishBundle,
2129
+ fetchUploadStatus,
2130
+ fetchUploadExport,
2131
+ withdrawUpload,
2132
+ defaultEnvironmentTags,
2133
+ loadExperienceTaxonomyContract,
2134
+ validateExperienceTaxonomyContract,
2135
+ canonicalSourceTaskId,
2136
+ canonicalTaskId,
2137
+ isCanonicalTaskId,
2138
+ parseEnvironmentConstraint,
2139
+ isCanonicalEnvironmentTag,
2140
+ environmentConstraintsMatch,
2141
+ selectApplicablePortableItems,
2142
+ deriveCanonicalTaskClasses,
2143
+ readExactLocalBaseMarker,
2144
+ exactTaskSignatureInPrompt,
2145
+ resolveRuntimeExperienceForAgent,
2146
+ estimateTokens,
2147
+ buildLocalExperienceAdvisory,
2148
+ augmentRuntimeSystemWithLocalExperience,
2149
+ portableExperienceSafetyIssues,
2150
+ cmdExperienceExchange,
2151
+ };