agentlas 0.9.10 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +106 -0
- package/README.md +319 -341
- package/bin/agentlas.cjs +32 -9
- package/engine/agentlas-banner.cjs +24 -3
- package/engine/agentlas-composer.cjs +239 -31
- package/engine/agentlas-config.cjs +103 -7
- package/engine/agentlas-core-harness.cjs +48 -4
- package/engine/agentlas-evolution.cjs +3 -4
- package/engine/agentlas-i18n.cjs +88 -32
- package/engine/agentlas-input.cjs +234 -18
- package/engine/agentlas-memory-governance.cjs +3 -5
- package/engine/agentlas-memory-import.cjs +3 -3
- package/engine/agentlas-native-host.cjs +200 -9
- package/engine/agentlas-onboard.cjs +112 -20
- package/engine/agentlas-permissions.cjs +14 -7
- package/engine/agentlas-sqlite-policy.cjs +34 -0
- package/engine/agentlas-ui.cjs +93 -42
- package/engine/agentlas-workforce.cjs +472 -58
- package/engine/agentlas-workload-routing.cjs +8 -3
- package/engine/agentlas.cjs +140 -12367
- package/engine/agents/files.cjs +61 -0
- package/engine/agents/import-local.cjs +237 -0
- package/engine/agents/registry.cjs +158 -0
- package/engine/agents/router.cjs +565 -0
- package/engine/agents/routes.cjs +43 -0
- package/engine/architecture.data.json +2 -2
- package/engine/automation/daemon.cjs +335 -0
- package/engine/automation/schedule.cjs +181 -0
- package/engine/automation/store.cjs +209 -0
- package/engine/cloud/auth.cjs +279 -0
- package/engine/cloud/hub-client.cjs +239 -0
- package/engine/cloud-assets/cargo.cjs +49 -0
- package/engine/cloud-assets/cas.cjs +235 -0
- package/engine/cloud-assets/commands.cjs +273 -0
- package/engine/cloud-assets/package.cjs +936 -0
- package/engine/cloud-assets/restore.cjs +172 -0
- package/engine/cloud-assets/state.cjs +268 -0
- package/engine/commands/automation.cjs +195 -0
- package/engine/commands/billing.cjs +96 -0
- package/engine/commands/browser.cjs +20 -0
- package/engine/commands/build.cjs +33 -0
- package/engine/commands/call.cjs +24 -0
- package/engine/commands/career-graph.cjs +51 -0
- package/engine/commands/cd.cjs +22 -0
- package/engine/commands/chat.cjs +12 -0
- package/engine/commands/chats.cjs +28 -0
- package/engine/commands/cloud.cjs +17 -0
- package/engine/commands/connect.cjs +20 -0
- package/engine/commands/context.cjs +66 -0
- package/engine/commands/creds.cjs +203 -0
- package/engine/commands/doctor.cjs +68 -0
- package/engine/commands/env.cjs +33 -0
- package/engine/commands/evolve.cjs +23 -0
- package/engine/commands/experience.cjs +34 -0
- package/engine/commands/film.cjs +8 -0
- package/engine/commands/firm.cjs +115 -0
- package/engine/commands/help.cjs +65 -0
- package/engine/commands/hep.cjs +23 -0
- package/engine/commands/import.cjs +35 -0
- package/engine/commands/index.cjs +136 -0
- package/engine/commands/install.cjs +27 -0
- package/engine/commands/journal.cjs +31 -0
- package/engine/commands/legacy-network.cjs +29 -0
- package/engine/commands/list.cjs +49 -0
- package/engine/commands/login.cjs +68 -0
- package/engine/commands/logout.cjs +28 -0
- package/engine/commands/mcp.cjs +91 -0
- package/engine/commands/memory.cjs +21 -0
- package/engine/commands/multimodal.cjs +91 -0
- package/engine/commands/native.cjs +34 -0
- package/engine/commands/netadmin.cjs +31 -0
- package/engine/commands/oberon.cjs +70 -0
- package/engine/commands/ontology.cjs +24 -0
- package/engine/commands/open.cjs +48 -0
- package/engine/commands/plugin.cjs +101 -0
- package/engine/commands/project.cjs +47 -0
- package/engine/commands/research.cjs +36 -0
- package/engine/commands/route.cjs +37 -0
- package/engine/commands/run.cjs +149 -0
- package/engine/commands/search.cjs +55 -0
- package/engine/commands/setup.cjs +45 -0
- package/engine/commands/storm.cjs +75 -0
- package/engine/commands/swarm.cjs +75 -0
- package/engine/commands/telegram.cjs +32 -0
- package/engine/commands/uninstall.cjs +68 -0
- package/engine/commands/update.cjs +54 -0
- package/engine/commands/upload.cjs +18 -0
- package/engine/commands/usage.cjs +35 -0
- package/engine/commands/variant.cjs +25 -0
- package/engine/commands/version.cjs +9 -0
- package/engine/commands/whoami.cjs +38 -0
- package/engine/commands/workforce.cjs +103 -0
- package/engine/core/db.cjs +160 -0
- package/engine/core/paths.cjs +35 -0
- package/engine/experience/build.cjs +181 -0
- package/engine/experience/intents.cjs +492 -0
- package/engine/experience/runtime.cjs +242 -0
- package/engine/experience/variant.cjs +196 -0
- package/engine/firms/orchestrate.cjs +333 -0
- package/engine/hephaestus/runtime.cjs +697 -0
- package/engine/hub/install.cjs +872 -0
- package/engine/hub/plugins.cjs +213 -0
- package/engine/mcp/consent.cjs +289 -0
- package/engine/mcp/contract.cjs +202 -0
- package/engine/mcp/index.cjs +43 -0
- package/engine/mcp/inventory.cjs +322 -0
- package/engine/mcp/plan.cjs +286 -0
- package/engine/mcp/probe.cjs +151 -0
- package/engine/memory-cli/curate.cjs +163 -0
- package/engine/oberon/common.cjs +69 -0
- package/engine/oberon/manifest.cjs +164 -0
- package/engine/oberon/outputs.cjs +70 -0
- package/engine/oberon/render.cjs +164 -0
- package/engine/project/career-graph.cjs +249 -0
- package/engine/project/credentials.cjs +262 -0
- package/engine/project/env-file.cjs +46 -0
- package/engine/project/index.cjs +27 -0
- package/engine/project/memory-context.cjs +453 -0
- package/engine/project/ontology.cjs +467 -0
- package/engine/project/paths.cjs +39 -0
- package/engine/project/seed.cjs +200 -0
- package/engine/project/state.cjs +403 -0
- package/engine/project/super-ontology-seed.json +3288 -0
- package/engine/runtimes/detect.cjs +54 -0
- package/engine/runtimes/overrides.cjs +139 -0
- package/engine/runtimes/resolve.cjs +64 -0
- package/engine/sessions/apply-fences.cjs +188 -0
- package/engine/sessions/fences.cjs +362 -0
- package/engine/sessions/orchestrator.cjs +170 -0
- package/engine/sessions/prompt.cjs +212 -0
- package/engine/sessions/session.cjs +245 -0
- package/engine/sessions/sink.cjs +54 -0
- package/engine/sessions/store.cjs +79 -0
- package/engine/storm/deps.cjs +88 -0
- package/engine/storm/storm.cjs +218 -0
- package/engine/storm/swarm.cjs +422 -0
- package/engine/ui/palette.cjs +105 -0
- package/engine/ui/renderer.cjs +85 -0
- package/engine/ui/repl.cjs +444 -0
- package/engine/workforce/capture.cjs +701 -0
- package/engine/workforce/deps.cjs +472 -0
- package/package.json +2 -6
- package/engine/agentlas-experience-mcp.cjs +0 -1709
- package/engine/agentlas-parity.cjs +0 -1499
- package/engine/agentlas-repl.cjs +0 -1780
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
* experience/intents — 경험 팩 publish/unpublish "로컬 의도" 저장소.
|
|
5
|
+
*
|
|
6
|
+
* v1 engine/agentlas-experience-mcp.cjs의 Experience 의도 저장 슬라이스를 토씨
|
|
7
|
+
* 그대로 이식했다(MCP 서브시스템은 engine/mcp/*로 분리 — 이 파일은 그 스코프가
|
|
8
|
+
* 아니다). 절대 계약(약화 금지):
|
|
9
|
+
* - publish/unpublish 명령은 로컬 의도만 영속화한다. Hub를 호출하지 않고,
|
|
10
|
+
* 서버 영수증을 위조하지 않는다(hubReceipt는 항상 null로만 저장 가능).
|
|
11
|
+
* - 자격증명 값, MCP command/args/URL, base 패키지 바이트는 공개 투영에
|
|
12
|
+
* 절대 들어가지 않는다. contentVerified는 선언만으로 true가 될 수 없다.
|
|
13
|
+
* - 상태 파일은 <userData>/terminal/experience-intents-v1.json — v1과 동일
|
|
14
|
+
* 포맷/경로. 쓰기는 0600 원자적 쓰기(임시파일+rename)만 사용한다.
|
|
15
|
+
*
|
|
16
|
+
* 검증 프리미티브(assert 계열, UNSAFE_TEXT_PATTERNS)는 engine/mcp/contract.cjs에도
|
|
17
|
+
* 존재하지만, 의도 저장소는 mcp 서브시스템 변경과 독립적으로 보안 경계를
|
|
18
|
+
* 유지해야 하므로 v1 원문을 여기 동결한다(우연한 규칙 완화 전파 방지).
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const crypto = require("node:crypto");
|
|
22
|
+
const fs = require("node:fs");
|
|
23
|
+
const path = require("node:path");
|
|
24
|
+
|
|
25
|
+
const ID_RE = /^[A-Za-z0-9][A-Za-z0-9._:/@-]{2,255}$/;
|
|
26
|
+
const HASH_RE = /^sha256:[0-9a-f]{64}$/;
|
|
27
|
+
const ENV_RE = /^[A-Z][A-Z0-9_]*$/;
|
|
28
|
+
const EXPERIENCE_STATE_SCHEMA = "agentlas.terminal-experience-intents.v1";
|
|
29
|
+
const EXPERIENCE_INTENT_SCHEMA = "agentlas.terminal-experience-intent.v1";
|
|
30
|
+
const MAX_JSON_BYTES = 2 * 1024 * 1024;
|
|
31
|
+
const EXPERIENCE_LOCK_STALE_MS = 30_000;
|
|
32
|
+
const EXPERIENCE_LOCK_WAIT_MS = 2_000;
|
|
33
|
+
|
|
34
|
+
const EXPERIENCE_PACK_REQUIRED = [
|
|
35
|
+
"schemaVersion", "kind", "experiencePackId", "releaseId", "ownerRef", "version",
|
|
36
|
+
"baseCompatibility", "itemIds", "evidenceReceiptIds", "mcpRequirements",
|
|
37
|
+
"containsBasePackageMaterial", "contentHash", "visibility", "status",
|
|
38
|
+
];
|
|
39
|
+
const EXPERIENCE_PACK_ALLOWED = new Set([...EXPERIENCE_PACK_REQUIRED, "createdAt", "releasedAt", "withdrawnAt"]);
|
|
40
|
+
const MCP_REQUIREMENT_REQUIRED = [
|
|
41
|
+
"schemaVersion", "kind", "requirementId", "catalogId", "reason", "capabilities",
|
|
42
|
+
"required", "requiresKey", "priority", "permissions", "alternatives", "unavailablePolicy",
|
|
43
|
+
];
|
|
44
|
+
const MCP_REQUIREMENT_ALLOWED = new Set([...MCP_REQUIREMENT_REQUIRED, "credentialMetadata"]);
|
|
45
|
+
|
|
46
|
+
// Public contract text must be compact, value-free, and instruction-safe.
|
|
47
|
+
const UNSAFE_TEXT_PATTERNS = [
|
|
48
|
+
{ code: "openai-secret", re: /\bsk-[A-Za-z0-9_-]{20,}\b/ },
|
|
49
|
+
{ code: "github-secret", re: /\bgh[pousr]_[A-Za-z0-9_]{20,}\b/ },
|
|
50
|
+
{ code: "aws-secret", re: /\bAKIA[0-9A-Z]{16}\b/ },
|
|
51
|
+
{ code: "private-key", re: /-----BEGIN (?:RSA |EC |OPENSSH |DSA )?PRIVATE KEY-----/i },
|
|
52
|
+
{ code: "credential", re: /(?:api[_ -]?key|access[_ -]?token|refresh[_ -]?token|password|private[_ -]?key|authorization)\s*[:=]\s*\S+/i },
|
|
53
|
+
{ code: "bearer", re: /\bbearer\s+[A-Za-z0-9._~+/=-]{8,}/i },
|
|
54
|
+
{ code: "private-path", re: /(?:file:\/\/|(?:^|[\s"'`()\[\]{}=:,;])(?:\.\.[/\\]|~[/\\]|\/(?!\/|\s)(?:[^/\s"'`<>]+\/)*[^/\s"'`<>]+|[A-Za-z]:[/\\]\S+|\\\\[^\\/\s]+[\\/][^\\/\s]+))/i },
|
|
55
|
+
{ code: "email", re: /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i },
|
|
56
|
+
{ code: "phone", re: /(?:\+?\d[\d .()-]{8,}\d)/ },
|
|
57
|
+
{ code: "account-id", re: /\b(?:account|customer|client|user)[ _-]?(?:id|number|no)\s*[:=#]?\s*[A-Za-z0-9_-]{4,}\b|(?:계정|고객|사용자)[ _-]?(?:id|아이디|번호)\s*[:=#]?\s*[A-Za-z0-9_-]{4,}/i },
|
|
58
|
+
{ code: "raw-prompt", re: /(?:raw[_ -]?prompt|full[_ -]?transcript|conversation[_ -]?dump|system[_ -]?prompt|(?:^|\n)\s*(?:system|assistant|user|tool)\s*:|(?:AGENTS|CLAUDE|GEMINI)\.md|\.agentlas[\\/])/i },
|
|
59
|
+
{ code: "prompt-injection", re: /(?:ignore|disregard|override)[\s_-]+(?:all[\s_-]+)?(?:previous|prior|system|developer|hidden)[\s_-]+(?:instructions?|prompts?|rules?|directives?)/i },
|
|
60
|
+
{ code: "exfiltration", re: /(?:reveal|show|print|dump|expose|leak|send|upload|exfiltrate|steal)[\s_-]+(?:(?:the|all)[\s_-]+)?(?:secret|credential|token|cookie|password|private[\s_-]?key|api[\s_-]?key)/i },
|
|
61
|
+
{ code: "safety-bypass", re: /(?:disable|bypass|skip|remove|turn[\s_-]+off)[\s_-]+(?:(?:the|all)[\s_-]+)?(?:safety|guardrails?|approval|consent|permission[\s_-]?checks?|security[\s_-]?checks?)/i },
|
|
62
|
+
{ code: "opaque-blob", re: /\b(?:[A-Fa-f0-9]{128,}|[A-Za-z0-9+/]{124,}={0,2})\b/ },
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
function assertObject(value, label) {
|
|
66
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) throw new Error(`${label} must be an object`);
|
|
67
|
+
return value;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function assertExactKeys(value, allowed, required, label) {
|
|
71
|
+
assertObject(value, label);
|
|
72
|
+
for (const key of Object.keys(value)) {
|
|
73
|
+
if (!allowed.has(key)) throw new Error(`${label} has an unsupported field: ${key}`);
|
|
74
|
+
}
|
|
75
|
+
for (const key of required) {
|
|
76
|
+
if (!Object.prototype.hasOwnProperty.call(value, key)) throw new Error(`${label} is missing: ${key}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function assertId(value, label) {
|
|
81
|
+
if (!ID_RE.test(String(value || ""))) throw new Error(`${label} is not a valid Agentlas id`);
|
|
82
|
+
return String(value);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function assertUniqueIds(value, label, options = {}) {
|
|
86
|
+
if (!Array.isArray(value)) throw new Error(`${label} must be an array`);
|
|
87
|
+
if (options.min && value.length < options.min) throw new Error(`${label} must have at least ${options.min} item(s)`);
|
|
88
|
+
if (options.max && value.length > options.max) throw new Error(`${label} has too many items`);
|
|
89
|
+
const items = value.map((item, index) => assertId(item, `${label}[${index}]`));
|
|
90
|
+
if (new Set(items).size !== items.length) throw new Error(`${label} must be unique`);
|
|
91
|
+
return items;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function assertSafeText(value, label, max = 300) {
|
|
95
|
+
const text = String(value || "").trim();
|
|
96
|
+
if (!text || text.length > max || /[\u0000-\u001f\u007f\u202a-\u202e\u2066-\u2069]/i.test(text)) throw new Error(`${label} must be compact single-line text`);
|
|
97
|
+
const unsafe = UNSAFE_TEXT_PATTERNS.find((pattern) => pattern.re.test(text));
|
|
98
|
+
if (unsafe) throw new Error(`${label} is not public-safe (${unsafe.code})`);
|
|
99
|
+
return text;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function assertIsoDateOrNull(value, label) {
|
|
103
|
+
if (value == null) return null;
|
|
104
|
+
if (typeof value !== "string" || !value || !Number.isFinite(Date.parse(value))) throw new Error(`${label} must be an ISO date-time or null`);
|
|
105
|
+
return value;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function validateCredentialMetadata(value, label) {
|
|
109
|
+
const allowed = new Set(["provider", "env", "allowedHosts", "scopes", "setupUrl", "brokerMode"]);
|
|
110
|
+
assertExactKeys(value, allowed, ["provider", "env"], label);
|
|
111
|
+
assertId(value.provider, `${label}.provider`);
|
|
112
|
+
if (!Array.isArray(value.env) || !value.env.length || new Set(value.env).size !== value.env.length || value.env.some((key) => !ENV_RE.test(String(key)))) {
|
|
113
|
+
throw new Error(`${label}.env must contain unique uppercase environment names`);
|
|
114
|
+
}
|
|
115
|
+
assertSafeText(value.provider, `${label}.provider`, 255);
|
|
116
|
+
value.env.forEach((key, index) => assertSafeText(key, `${label}.env[${index}]`, 255));
|
|
117
|
+
if (value.allowedHosts != null) {
|
|
118
|
+
if (!Array.isArray(value.allowedHosts) || !value.allowedHosts.length || new Set(value.allowedHosts).size !== value.allowedHosts.length) {
|
|
119
|
+
throw new Error(`${label}.allowedHosts must be a non-empty unique list`);
|
|
120
|
+
}
|
|
121
|
+
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])?)*$/;
|
|
122
|
+
for (const host of value.allowedHosts) {
|
|
123
|
+
if (typeof host !== "string" || host.length > 255 || !hostRe.test(host)) throw new Error(`${label}.allowedHosts contains an invalid host`);
|
|
124
|
+
assertSafeText(host, `${label}.allowedHosts`, 255);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
if (value.scopes != null) {
|
|
128
|
+
if (!Array.isArray(value.scopes) || !value.scopes.length || new Set(value.scopes).size !== value.scopes.length) {
|
|
129
|
+
throw new Error(`${label}.scopes must be a non-empty unique list`);
|
|
130
|
+
}
|
|
131
|
+
for (const scope of value.scopes) {
|
|
132
|
+
if (!/^[A-Za-z0-9][A-Za-z0-9._:/@-]{0,127}$/.test(String(scope))) throw new Error(`${label}.scopes contains an invalid scope`);
|
|
133
|
+
assertSafeText(scope, `${label}.scopes`, 128);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
if (value.setupUrl != null) {
|
|
137
|
+
let parsed;
|
|
138
|
+
try { parsed = new URL(value.setupUrl); } catch { throw new Error(`${label}.setupUrl must be a safe HTTPS provider page`); }
|
|
139
|
+
if (parsed.protocol !== "https:" || parsed.username || parsed.password || parsed.port || parsed.search || parsed.hash) {
|
|
140
|
+
throw new Error(`${label}.setupUrl must be HTTPS without userinfo, custom port, query, or fragment`);
|
|
141
|
+
}
|
|
142
|
+
if (!/^[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])?)*$/.test(parsed.hostname)) {
|
|
143
|
+
throw new Error(`${label}.setupUrl hostname is invalid`);
|
|
144
|
+
}
|
|
145
|
+
assertSafeText(value.setupUrl, `${label}.setupUrl`, 2048);
|
|
146
|
+
}
|
|
147
|
+
if (value.brokerMode != null && !["host-bound-broker", "runtime-env-injection", "provider-managed-oauth", "manual-provider-page"].includes(value.brokerMode)) {
|
|
148
|
+
throw new Error(`${label}.brokerMode is invalid`);
|
|
149
|
+
}
|
|
150
|
+
return value;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function validateMcpRequirement(value, label = "mcpRequirement") {
|
|
154
|
+
assertExactKeys(value, MCP_REQUIREMENT_ALLOWED, MCP_REQUIREMENT_REQUIRED, label);
|
|
155
|
+
if (value.schemaVersion !== "agentlas.mcp-requirement.v1" || value.kind !== "agentlas-mcp-requirement") {
|
|
156
|
+
throw new Error(`${label} has an unsupported schema`);
|
|
157
|
+
}
|
|
158
|
+
assertId(value.requirementId, `${label}.requirementId`);
|
|
159
|
+
assertId(value.catalogId, `${label}.catalogId`);
|
|
160
|
+
assertSafeText(value.reason, `${label}.reason`, 300);
|
|
161
|
+
const capabilities = assertUniqueIds(value.capabilities, `${label}.capabilities`, { min: 1 });
|
|
162
|
+
if (typeof value.required !== "boolean" || typeof value.requiresKey !== "boolean") throw new Error(`${label} required/requiresKey must be boolean`);
|
|
163
|
+
if (!Number.isInteger(value.priority) || value.priority < 1 || value.priority > 1000) throw new Error(`${label}.priority is invalid`);
|
|
164
|
+
const permissions = assertUniqueIds(value.permissions, `${label}.permissions`);
|
|
165
|
+
const alternatives = assertUniqueIds(value.alternatives, `${label}.alternatives`);
|
|
166
|
+
if (alternatives.includes(value.catalogId)) throw new Error(`${label}.alternatives must not contain the primary catalogId`);
|
|
167
|
+
[...capabilities, ...permissions, ...alternatives].forEach((text, index) => assertSafeText(text, `${label}.publicText[${index}]`, 255));
|
|
168
|
+
const unavailable = assertObject(value.unavailablePolicy, `${label}.unavailablePolicy`);
|
|
169
|
+
assertExactKeys(unavailable, new Set(["build", "rental", "execution"]), ["build", "rental", "execution"], `${label}.unavailablePolicy`);
|
|
170
|
+
if (unavailable.build !== "degrade") throw new Error(`${label} must degrade rather than abort a build`);
|
|
171
|
+
const expectedRental = value.required ? "exclude-variant" : "continue-degraded";
|
|
172
|
+
if (unavailable.rental !== expectedRental) throw new Error(`${label} rental policy must be ${expectedRental}`);
|
|
173
|
+
if (!["use-alternative", "disable-capability", "continue-degraded"].includes(unavailable.execution)) throw new Error(`${label} execution policy is invalid`);
|
|
174
|
+
if (value.credentialMetadata != null) validateCredentialMetadata(value.credentialMetadata, `${label}.credentialMetadata`);
|
|
175
|
+
if (value.requiresKey && value.credentialMetadata == null) throw new Error(`${label} requires credential metadata`);
|
|
176
|
+
return value;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function validateExperiencePack(value) {
|
|
180
|
+
assertExactKeys(value, EXPERIENCE_PACK_ALLOWED, EXPERIENCE_PACK_REQUIRED, "experience pack");
|
|
181
|
+
if (value.schemaVersion !== "agentlas.experience-pack.v1" || value.kind !== "agentlas-experience-pack") {
|
|
182
|
+
throw new Error("experience pack has an unsupported schema");
|
|
183
|
+
}
|
|
184
|
+
for (const key of ["experiencePackId", "releaseId", "ownerRef", "version"]) assertId(value[key], `experience pack.${key}`);
|
|
185
|
+
const base = assertObject(value.baseCompatibility, "experience pack.baseCompatibility");
|
|
186
|
+
assertExactKeys(base, new Set(["agentDefinitionId", "compatibleBaseReleaseIds"]), ["agentDefinitionId", "compatibleBaseReleaseIds"], "experience pack.baseCompatibility");
|
|
187
|
+
assertId(base.agentDefinitionId, "experience pack.baseCompatibility.agentDefinitionId");
|
|
188
|
+
assertUniqueIds(base.compatibleBaseReleaseIds, "experience pack.baseCompatibility.compatibleBaseReleaseIds", { min: 1 });
|
|
189
|
+
assertUniqueIds(value.itemIds, "experience pack.itemIds", { min: value.status === "active" ? 1 : 0 });
|
|
190
|
+
assertUniqueIds(value.evidenceReceiptIds, "experience pack.evidenceReceiptIds");
|
|
191
|
+
if (!Array.isArray(value.mcpRequirements) || value.mcpRequirements.length > 64) throw new Error("experience pack.mcpRequirements is invalid");
|
|
192
|
+
value.mcpRequirements.forEach((requirement, index) => validateMcpRequirement(requirement, `experience pack.mcpRequirements[${index}]`));
|
|
193
|
+
// base 패키지 복사 반입 금지 — 참조만 허용(오너 결정, 완화 불가).
|
|
194
|
+
if (value.containsBasePackageMaterial !== false) throw new Error("experience pack must reference the base release; copied base material is forbidden");
|
|
195
|
+
if (!HASH_RE.test(String(value.contentHash || ""))) throw new Error("experience pack.contentHash is invalid");
|
|
196
|
+
if (!["private", "unlisted", "public"].includes(value.visibility)) throw new Error("experience pack.visibility is invalid");
|
|
197
|
+
if (!["draft", "active", "suspended", "withdrawn", "deleted"].includes(value.status)) throw new Error("experience pack.status is invalid");
|
|
198
|
+
for (const key of ["createdAt", "releasedAt", "withdrawnAt"]) if (Object.prototype.hasOwnProperty.call(value, key)) assertIsoDateOrNull(value[key], `experience pack.${key}`);
|
|
199
|
+
return value;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function readJsonFile(filePath, label) {
|
|
203
|
+
const absolute = path.resolve(filePath);
|
|
204
|
+
const stat = fs.lstatSync(absolute);
|
|
205
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error(`${label} must be a regular file (symlinks are not accepted)`);
|
|
206
|
+
if (stat.size <= 0 || stat.size > MAX_JSON_BYTES) throw new Error(`${label} has an invalid size`);
|
|
207
|
+
let value;
|
|
208
|
+
try { value = JSON.parse(fs.readFileSync(absolute, "utf8")); } catch (error) { throw new Error(`${label} is not valid JSON: ${error.message}`); }
|
|
209
|
+
return { absolute, value };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function writePrivateJsonAtomic(filePath, value) {
|
|
213
|
+
const dir = path.dirname(filePath);
|
|
214
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
215
|
+
try { fs.chmodSync(dir, 0o700); } catch { /* best effort */ }
|
|
216
|
+
const temp = path.join(dir, `.${path.basename(filePath)}.${process.pid}.${crypto.randomBytes(6).toString("hex")}.tmp`);
|
|
217
|
+
try {
|
|
218
|
+
fs.writeFileSync(temp, JSON.stringify(value, null, 2) + "\n", { encoding: "utf8", mode: 0o600, flag: "wx" });
|
|
219
|
+
fs.renameSync(temp, filePath);
|
|
220
|
+
try { fs.chmodSync(filePath, 0o600); } catch { /* best effort */ }
|
|
221
|
+
} finally {
|
|
222
|
+
try { fs.rmSync(temp, { force: true }); } catch { /* noop */ }
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function experienceStatePath(userDataDir) {
|
|
227
|
+
return path.join(userDataDir, "terminal", "experience-intents-v1.json");
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function waitSync(milliseconds) {
|
|
231
|
+
// Atomics.wait은 지원 Node 20+에서 스핀 없이 대기하는 유일한 동기 sleep이다.
|
|
232
|
+
const signal = new Int32Array(new SharedArrayBuffer(4));
|
|
233
|
+
Atomics.wait(signal, 0, 0, milliseconds);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function withExperienceStateLock(userDataDir, action) {
|
|
237
|
+
const stateFile = experienceStatePath(userDataDir);
|
|
238
|
+
const dir = path.dirname(stateFile);
|
|
239
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
240
|
+
try { fs.chmodSync(dir, 0o700); } catch { /* best effort */ }
|
|
241
|
+
const lockFile = `${stateFile}.lock`;
|
|
242
|
+
const deadline = Date.now() + EXPERIENCE_LOCK_WAIT_MS;
|
|
243
|
+
let descriptor = null;
|
|
244
|
+
while (descriptor == null) {
|
|
245
|
+
try {
|
|
246
|
+
descriptor = fs.openSync(lockFile, "wx", 0o600);
|
|
247
|
+
fs.writeFileSync(descriptor, `${process.pid}\n${new Date().toISOString()}\n`, "utf8");
|
|
248
|
+
} catch (error) {
|
|
249
|
+
if (!error || error.code !== "EEXIST") throw error;
|
|
250
|
+
try {
|
|
251
|
+
const stat = fs.lstatSync(lockFile);
|
|
252
|
+
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error("Terminal experience lock is unsafe");
|
|
253
|
+
if (Date.now() - stat.mtimeMs > EXPERIENCE_LOCK_STALE_MS) {
|
|
254
|
+
fs.unlinkSync(lockFile);
|
|
255
|
+
continue;
|
|
256
|
+
}
|
|
257
|
+
} catch (statError) {
|
|
258
|
+
if (statError && statError.code === "ENOENT") continue;
|
|
259
|
+
throw statError;
|
|
260
|
+
}
|
|
261
|
+
if (Date.now() >= deadline) throw new Error("Terminal experience state is busy; retry the command");
|
|
262
|
+
waitSync(25);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
try {
|
|
266
|
+
return action();
|
|
267
|
+
} finally {
|
|
268
|
+
try { fs.closeSync(descriptor); } catch { /* noop */ }
|
|
269
|
+
try { fs.unlinkSync(lockFile); } catch { /* crash recovery handles leftovers */ }
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
function emptyExperienceState() {
|
|
274
|
+
return { schemaVersion: EXPERIENCE_STATE_SCHEMA, updatedAt: null, intents: [] };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function loadExperienceState(userDataDir) {
|
|
278
|
+
const file = experienceStatePath(userDataDir);
|
|
279
|
+
if (!fs.existsSync(file)) return emptyExperienceState();
|
|
280
|
+
const stat = fs.lstatSync(file);
|
|
281
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > MAX_JSON_BYTES) throw new Error("Terminal experience state is unsafe or too large");
|
|
282
|
+
const state = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
283
|
+
assertExactKeys(state, new Set(["schemaVersion", "updatedAt", "intents"]), ["schemaVersion", "updatedAt", "intents"], "Terminal experience state");
|
|
284
|
+
if (state.schemaVersion !== EXPERIENCE_STATE_SCHEMA || !Array.isArray(state.intents)) throw new Error("Terminal experience state schema is invalid");
|
|
285
|
+
assertIsoDateOrNull(state.updatedAt, "Terminal experience state.updatedAt");
|
|
286
|
+
state.intents.forEach((intent, index) => validateStoredExperienceIntent(intent, index));
|
|
287
|
+
return state;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function validateStoredExperienceIntent(intent, index) {
|
|
291
|
+
const label = `Terminal experience state.intents[${index}]`;
|
|
292
|
+
const required = [
|
|
293
|
+
"schemaVersion", "intentId", "experiencePackId", "releaseId", "ownerRef", "version", "contentHash",
|
|
294
|
+
"compatibleBaseReleaseIds", "mcpRequirementIds", "sourcePath", "desiredAction", "localState", "hubReceipt",
|
|
295
|
+
"contractValidatedAt", "contentVerified", "updatedAt",
|
|
296
|
+
];
|
|
297
|
+
assertExactKeys(intent, new Set(required), required, label);
|
|
298
|
+
if (intent.schemaVersion !== EXPERIENCE_INTENT_SCHEMA) throw new Error(`${label}.schemaVersion is invalid`);
|
|
299
|
+
for (const key of ["intentId", "experiencePackId", "releaseId", "ownerRef", "version"]) assertId(intent[key], `${label}.${key}`);
|
|
300
|
+
if (!HASH_RE.test(String(intent.contentHash || ""))) throw new Error(`${label}.contentHash is invalid`);
|
|
301
|
+
assertUniqueIds(intent.compatibleBaseReleaseIds, `${label}.compatibleBaseReleaseIds`, { min: 1 });
|
|
302
|
+
assertUniqueIds(intent.mcpRequirementIds, `${label}.mcpRequirementIds`);
|
|
303
|
+
if (typeof intent.sourcePath !== "string" || !path.isAbsolute(intent.sourcePath) || intent.sourcePath.length > 4096 || /[\0\r\n]/.test(intent.sourcePath)) throw new Error(`${label}.sourcePath is invalid`);
|
|
304
|
+
if (!["publish", "unpublish"].includes(intent.desiredAction)) throw new Error(`${label}.desiredAction is invalid`);
|
|
305
|
+
const expectedState = intent.desiredAction === "publish" ? "publish-requested" : "unpublish-requested";
|
|
306
|
+
if (intent.localState !== expectedState) throw new Error(`${label}.localState is inconsistent`);
|
|
307
|
+
// 서버 영수증은 로컬에서 합성될 수 없다 — 위조 발행 주장 차단의 핵심 게이트.
|
|
308
|
+
if (intent.hubReceipt !== null) throw new Error(`${label}.hubReceipt cannot be synthesized locally`);
|
|
309
|
+
if (intent.contentVerified !== false) throw new Error(`${label}.contentVerified cannot be asserted from a declaration alone`);
|
|
310
|
+
assertIsoDateOrNull(intent.contractValidatedAt, `${label}.contractValidatedAt`);
|
|
311
|
+
assertIsoDateOrNull(intent.updatedAt, `${label}.updatedAt`);
|
|
312
|
+
return intent;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function saveExperienceState(userDataDir, state) {
|
|
316
|
+
state.updatedAt = new Date().toISOString();
|
|
317
|
+
writePrivateJsonAtomic(experienceStatePath(userDataDir), state);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function publicExperienceIntent(intent) {
|
|
321
|
+
return {
|
|
322
|
+
schemaVersion: intent.schemaVersion,
|
|
323
|
+
intentId: intent.intentId,
|
|
324
|
+
experiencePackId: intent.experiencePackId,
|
|
325
|
+
releaseId: intent.releaseId,
|
|
326
|
+
ownerRef: intent.ownerRef,
|
|
327
|
+
version: intent.version,
|
|
328
|
+
contentHash: intent.contentHash,
|
|
329
|
+
compatibleBaseReleaseIds: intent.compatibleBaseReleaseIds,
|
|
330
|
+
desiredAction: intent.desiredAction,
|
|
331
|
+
localState: intent.localState,
|
|
332
|
+
hubPublication: { status: "not-submitted", receiptPresent: false },
|
|
333
|
+
updatedAt: intent.updatedAt,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function parseSimpleFlags(args) {
|
|
338
|
+
const flags = { _: [] };
|
|
339
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
340
|
+
const token = String(args[index]);
|
|
341
|
+
if (token === "--json") flags.json = true;
|
|
342
|
+
else if (token.startsWith("--") && token.includes("=")) {
|
|
343
|
+
const at = token.indexOf("=");
|
|
344
|
+
flags[token.slice(2, at)] = token.slice(at + 1);
|
|
345
|
+
} else if (token.startsWith("--")) {
|
|
346
|
+
const key = token.slice(2);
|
|
347
|
+
if (index + 1 < args.length && !String(args[index + 1]).startsWith("--")) flags[key] = String(args[++index]);
|
|
348
|
+
else flags[key] = true;
|
|
349
|
+
} else flags._.push(token);
|
|
350
|
+
}
|
|
351
|
+
return flags;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function renderExperienceList(intents) {
|
|
355
|
+
if (!intents.length) return "No local Experience Pack publication intents.\nHub publication: not attempted.";
|
|
356
|
+
const lines = ["LOCAL EXPERIENCE INTENTS (Terminal-owned; not Hub publication)"];
|
|
357
|
+
for (const intent of intents) lines.push(`- ${intent.experiencePackId}@${intent.version} · ${intent.localState} · Hub receipt: none`);
|
|
358
|
+
return lines.join("\n");
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
function findIntent(state, ref) {
|
|
362
|
+
const matches = state.intents.filter((intent) => [intent.intentId, intent.experiencePackId, intent.releaseId].includes(ref));
|
|
363
|
+
if (!matches.length) return null;
|
|
364
|
+
return matches.sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt)))[0];
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
function publishExperienceIntent(userDataDir, sourcePath, cwd) {
|
|
368
|
+
if (!sourcePath) throw new Error("usage: agentlas experience publish <experience-pack.json>");
|
|
369
|
+
const source = path.resolve(cwd || process.cwd(), sourcePath);
|
|
370
|
+
const { absolute, value } = readJsonFile(source, "experience pack");
|
|
371
|
+
const pack = validateExperiencePack(value);
|
|
372
|
+
const now = new Date().toISOString();
|
|
373
|
+
const intentId = `experience-intent:${crypto.createHash("sha256").update(`${pack.releaseId}\0${pack.contentHash}`).digest("hex").slice(0, 32)}`;
|
|
374
|
+
const intent = {
|
|
375
|
+
schemaVersion: EXPERIENCE_INTENT_SCHEMA,
|
|
376
|
+
intentId,
|
|
377
|
+
experiencePackId: pack.experiencePackId,
|
|
378
|
+
releaseId: pack.releaseId,
|
|
379
|
+
ownerRef: pack.ownerRef,
|
|
380
|
+
version: pack.version,
|
|
381
|
+
contentHash: pack.contentHash,
|
|
382
|
+
compatibleBaseReleaseIds: [...pack.baseCompatibility.compatibleBaseReleaseIds],
|
|
383
|
+
mcpRequirementIds: pack.mcpRequirements.map((requirement) => requirement.requirementId),
|
|
384
|
+
sourcePath: absolute,
|
|
385
|
+
desiredAction: "publish",
|
|
386
|
+
localState: "publish-requested",
|
|
387
|
+
hubReceipt: null,
|
|
388
|
+
contractValidatedAt: now,
|
|
389
|
+
contentVerified: false,
|
|
390
|
+
updatedAt: now,
|
|
391
|
+
};
|
|
392
|
+
withExperienceStateLock(userDataDir, () => {
|
|
393
|
+
const state = loadExperienceState(userDataDir);
|
|
394
|
+
const existing = state.intents.findIndex((row) => row.intentId === intentId);
|
|
395
|
+
if (existing >= 0) state.intents[existing] = intent;
|
|
396
|
+
else state.intents.push(intent);
|
|
397
|
+
saveExperienceState(userDataDir, state);
|
|
398
|
+
});
|
|
399
|
+
return publicExperienceIntent(intent);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function unpublishExperienceIntent(userDataDir, ref) {
|
|
403
|
+
if (!ref) throw new Error("usage: agentlas experience unpublish <pack-id|release-id|intent-id>");
|
|
404
|
+
let intent;
|
|
405
|
+
withExperienceStateLock(userDataDir, () => {
|
|
406
|
+
const state = loadExperienceState(userDataDir);
|
|
407
|
+
intent = findIntent(state, ref);
|
|
408
|
+
if (!intent) throw new Error(`local Experience Pack intent not found: ${ref}`);
|
|
409
|
+
intent.desiredAction = "unpublish";
|
|
410
|
+
intent.localState = "unpublish-requested";
|
|
411
|
+
intent.hubReceipt = null;
|
|
412
|
+
intent.updatedAt = new Date().toISOString();
|
|
413
|
+
saveExperienceState(userDataDir, state);
|
|
414
|
+
});
|
|
415
|
+
return publicExperienceIntent(intent);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* cmdExperience — 레거시 pack-only 로컬 의도 표면.
|
|
420
|
+
* v2에서는 `agentlas experience legacy-*` 로만 도달한다(모던 명령은
|
|
421
|
+
* agentlas-experience-exchange.cjs의 cmdExperienceExchange가 처리).
|
|
422
|
+
*/
|
|
423
|
+
function cmdExperience(options) {
|
|
424
|
+
const args = options.args || [];
|
|
425
|
+
const sub = args[0] || "list";
|
|
426
|
+
const flags = parseSimpleFlags(args.slice(1));
|
|
427
|
+
const emit = options.out || console.log;
|
|
428
|
+
const userData = options.userDataDir;
|
|
429
|
+
if (!userData) throw new Error("Terminal userData path is required");
|
|
430
|
+
if (sub === "list" || sub === "ls") {
|
|
431
|
+
const list = loadExperienceState(userData).intents.map(publicExperienceIntent);
|
|
432
|
+
emit(flags.json ? JSON.stringify({ localOnly: true, hubPublicationAttempted: false, intents: list }, null, 2) : renderExperienceList(list));
|
|
433
|
+
return list;
|
|
434
|
+
}
|
|
435
|
+
if (sub === "inspect" || sub === "show") {
|
|
436
|
+
const ref = flags._[0];
|
|
437
|
+
if (!ref) throw new Error("usage: agentlas experience inspect <pack-id|release-id|intent-id>");
|
|
438
|
+
const intent = findIntent(loadExperienceState(userData), ref);
|
|
439
|
+
if (!intent) throw new Error(`local Experience Pack intent not found: ${ref}`);
|
|
440
|
+
const projected = publicExperienceIntent(intent);
|
|
441
|
+
emit(flags.json ? JSON.stringify(projected, null, 2) : [
|
|
442
|
+
`${projected.experiencePackId}@${projected.version}`,
|
|
443
|
+
`release: ${projected.releaseId}`,
|
|
444
|
+
`local intent: ${projected.desiredAction} (${projected.localState})`,
|
|
445
|
+
"Hub publication: not submitted · server receipt: none",
|
|
446
|
+
"base package: referenced only (not copied)",
|
|
447
|
+
].join("\n"));
|
|
448
|
+
return projected;
|
|
449
|
+
}
|
|
450
|
+
if (sub === "publish") {
|
|
451
|
+
const intent = publishExperienceIntent(userData, flags._[0], options.cwd);
|
|
452
|
+
emit(flags.json ? JSON.stringify(intent, null, 2) : [
|
|
453
|
+
`Local publish intent saved: ${intent.experiencePackId}@${intent.version}`,
|
|
454
|
+
"Hub publication: NOT performed · server receipt: none",
|
|
455
|
+
"Use the Hub API/UI later; this command does not claim remote publication.",
|
|
456
|
+
].join("\n"));
|
|
457
|
+
return intent;
|
|
458
|
+
}
|
|
459
|
+
if (sub === "unpublish") {
|
|
460
|
+
const intent = unpublishExperienceIntent(userData, flags._[0]);
|
|
461
|
+
emit(flags.json ? JSON.stringify(intent, null, 2) : [
|
|
462
|
+
`Local unpublish intent saved: ${intent.experiencePackId}@${intent.version}`,
|
|
463
|
+
"Hub state: unchanged · no server request or receipt was created.",
|
|
464
|
+
].join("\n"));
|
|
465
|
+
return intent;
|
|
466
|
+
}
|
|
467
|
+
throw new Error(`unknown experience subcommand: ${sub} (list|inspect|publish|unpublish)`);
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
module.exports = {
|
|
471
|
+
EXPERIENCE_STATE_SCHEMA,
|
|
472
|
+
EXPERIENCE_INTENT_SCHEMA,
|
|
473
|
+
UNSAFE_TEXT_PATTERNS,
|
|
474
|
+
assertObject,
|
|
475
|
+
assertExactKeys,
|
|
476
|
+
assertId,
|
|
477
|
+
assertUniqueIds,
|
|
478
|
+
assertSafeText,
|
|
479
|
+
assertIsoDateOrNull,
|
|
480
|
+
validateCredentialMetadata,
|
|
481
|
+
validateMcpRequirement,
|
|
482
|
+
validateExperiencePack,
|
|
483
|
+
readJsonFile,
|
|
484
|
+
writePrivateJsonAtomic,
|
|
485
|
+
parseSimpleFlags,
|
|
486
|
+
experienceStatePath,
|
|
487
|
+
withExperienceStateLock,
|
|
488
|
+
loadExperienceState,
|
|
489
|
+
publishExperienceIntent,
|
|
490
|
+
unpublishExperienceIntent,
|
|
491
|
+
cmdExperience,
|
|
492
|
+
};
|