@limecloud/cli 0.11.0 → 0.13.0
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/README.md +27 -2
- package/dist/identity.json +69 -11
- package/dist/knowledge.json +1 -1
- package/dist/management.json +5648 -4192
- package/dist/skills.json +1 -1
- package/package.json +1 -1
- package/src/cli.mjs +19 -9
- package/src/compatibility.mjs +16 -8
- package/src/distribution.mjs +72 -0
- package/src/group-task.mjs +14 -9
- package/src/install.mjs +26 -5
- package/src/locales/en.json +32 -6
- package/src/locales/zh-CN.json +32 -6
- package/src/product-skill-access.mjs +68 -0
- package/src/product-skill-contracts.mjs +57 -0
- package/src/product-skill-files.mjs +105 -0
- package/src/product-skills.mjs +35 -15
- package/src/release-version.mjs +13 -0
- package/src/update.mjs +57 -0
package/src/group-task.mjs
CHANGED
|
@@ -58,6 +58,7 @@ function present(value) {
|
|
|
58
58
|
|
|
59
59
|
// 本地只保存恢复定位和发送标记,不缓存命令、影响、组状态、回执或任何认证凭据。
|
|
60
60
|
// 每次执行先重新读取原准备;发送前持久标记,一次尝试后只允许查询/终结,不自动重发。
|
|
61
|
+
// 已有有效引用的查询/终结直接访问原动作,避免额外的准备读取阻断未知结果恢复。
|
|
61
62
|
export async function executeGroupTask(plan, { contract, context, call, referenceDirectory,
|
|
62
63
|
referenceStore = id => new PrivateJSONStore(join(referenceDirectory, `${id}.json`), { durable: true }), now = Date.now } = {}) {
|
|
63
64
|
if (!context.apiOrigin || !context.clientID) throw new CLIError("group_context_required", message("groupTask.contextRequired"));
|
|
@@ -96,7 +97,8 @@ export async function executeGroupTask(plan, { contract, context, call, referenc
|
|
|
96
97
|
if (ref && (Object.keys(ref).some(key => !referenceKeys.has(key)) || ["client_id", "application_id", "actor_id", "space_id", "preparation_id", "target_id"].some(key => !validID(ref[key])) ||
|
|
97
98
|
!digest(ref.confirmation_digest) || !/^sha256:[a-f0-9]{64}$/.test(ref.execution_contract_revision) || !Number.isFinite(Date.parse(ref.expires_at)) ||
|
|
98
99
|
ref.execute_attempted_at !== undefined && !Number.isFinite(Date.parse(ref.execute_attempted_at)) ||
|
|
99
|
-
ref.schema_version !== 1 || ref.issuer !== context.issuer || ref.api_origin !== context.apiOrigin || ref.
|
|
100
|
+
ref.schema_version !== 1 || ref.issuer !== context.issuer || ref.api_origin !== context.apiOrigin || ref.audience !== context.apiOrigin ||
|
|
101
|
+
!["sandbox", "production"].includes(ref.environment) || ref.client_id !== context.clientID ||
|
|
100
102
|
ref.space_id !== plan.space || ref.preparation_id !== plan.id)) throw new CLIError("group_reference_conflict", message("groupTask.referenceConflict"));
|
|
101
103
|
// 只用已核对的原引用生成恢复提示;读取失败或新响应不兼容不能使原操作失去定位。
|
|
102
104
|
try {
|
|
@@ -104,13 +106,15 @@ export async function executeGroupTask(plan, { contract, context, call, referenc
|
|
|
104
106
|
if (plan.action === "execute" && ref.execute_attempted_at) {
|
|
105
107
|
throw new CLIError("group_execution_already_attempted", message("groupTask.alreadyAttempted"));
|
|
106
108
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
109
|
+
if (plan.action === "get" || plan.action === "execute" || !ref) {
|
|
110
|
+
const readOperation = catalogue.find(item => item.command === "open.group_change.get");
|
|
111
|
+
if (!readOperation) throw new CLIError("invalid_contract", message("groupTask.contractUnavailable"));
|
|
112
|
+
const prepared = await invoke(readOperation, { path: readOperation.path.replace("{spaceID}", plan.space).replace("{preparationID}", plan.id), query: new URLSearchParams() });
|
|
113
|
+
const current = verifyPrepared(prepared, plan.id);
|
|
114
|
+
if (ref && Object.keys(current).some(key => current[key] !== ref[key])) throw new CLIError("group_reference_conflict", message("groupTask.referenceConflict"));
|
|
115
|
+
if (!ref) { ref = current; await store.write(ref); }
|
|
116
|
+
if (plan.action === "get") return result(ref, "preparation", prepared);
|
|
117
|
+
}
|
|
114
118
|
if (plan.action === "execute" && (plan.confirmation !== ref.confirmation_digest ||
|
|
115
119
|
ref.execution_contract_revision !== actionContract(contract, plan.operation).contract_revision)) {
|
|
116
120
|
throw new CLIError("group_confirmation_changed", message("groupTask.confirmationChanged"));
|
|
@@ -120,7 +124,8 @@ export async function executeGroupTask(plan, { contract, context, call, referenc
|
|
|
120
124
|
await store.write(ref);
|
|
121
125
|
} : undefined);
|
|
122
126
|
const receipt = plan.action === "resolve" ? value.receipt : value;
|
|
123
|
-
if (receipt && (receipt.operation_key !== plan.id || receipt.preview.preparation_id !== plan.id || receipt.preview.token !== ref.confirmation_digest ||
|
|
127
|
+
if (receipt && (receipt.operation_key !== plan.id || receipt.preview.preparation_id !== plan.id || receipt.preview.token !== ref.confirmation_digest ||
|
|
128
|
+
receipt.preview.kind !== "group" || receipt.preview.target_id !== ref.target_id || receipt.result.id !== ref.target_id || receipt.result.space_id !== ref.space_id) ||
|
|
124
129
|
plan.action === "resolve" && value.cancellation && value.cancellation.operation_key !== plan.id) throw new CLIError("invalid_response", message("groupTask.invalidResponse"));
|
|
125
130
|
return result(ref, plan.action === "resolve" ? "resolution" : "receipt", value);
|
|
126
131
|
} catch (error) {
|
package/src/install.mjs
CHANGED
|
@@ -1,26 +1,42 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { access,
|
|
2
|
+
import { access, realpath } from "node:fs/promises";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { join, resolve } from "node:path";
|
|
5
5
|
import { createInterface } from "node:readline/promises";
|
|
6
|
+
import { fileURLToPath } from "node:url";
|
|
6
7
|
import { CLIError } from "./protocol.mjs";
|
|
7
8
|
import { message, translate } from "./i18n.mjs";
|
|
8
9
|
import { validateSettings } from "./storage.mjs";
|
|
10
|
+
import { cliDistributionSnapshot, cliRegistry as registry, requireMatchingDistribution } from "./distribution.mjs";
|
|
11
|
+
import { releaseVersion } from "./release-version.mjs";
|
|
12
|
+
|
|
13
|
+
const sourceRoot = fileURLToPath(new URL("../", import.meta.url));
|
|
9
14
|
|
|
10
15
|
export function installationPlan(version, prefix, env = process.env) {
|
|
11
16
|
const destination = prefix ?? env.LIMECLOUD_INSTALL_PREFIX ?? join(homedir(), ".local");
|
|
17
|
+
if (!releaseVersion(version)) {
|
|
18
|
+
throw new CLIError("invalid_arguments", message("install.invalidVersion"));
|
|
19
|
+
}
|
|
12
20
|
if (typeof destination !== "string" || !destination.trim() || destination.length > 2048 || /[\x00-\x1f\x7f]/.test(destination)) {
|
|
13
21
|
throw new CLIError("invalid_arguments", message("install.invalidPrefix"));
|
|
14
22
|
}
|
|
15
23
|
const absolute = resolve(destination);
|
|
16
24
|
const bin = process.platform === "win32" ? absolute : join(absolute, "bin");
|
|
17
|
-
return { package: "@limecloud/cli", version, prefix: absolute, bin_directory: bin,
|
|
25
|
+
return { package: "@limecloud/cli", version, prefix: absolute, bin_directory: bin, registry, lifecycle_scripts: "disabled",
|
|
26
|
+
verification: { status: "planned", source: "running-cli-package", provenance: "not-evaluated", dependency_contents: "not-evaluated" },
|
|
18
27
|
executable: join(bin, process.platform === "win32" ? "limecloud-cli.cmd" : "limecloud-cli"),
|
|
19
|
-
command: ["npm", "install", "--global", "--prefix", absolute, "--
|
|
28
|
+
command: ["npm", "install", "--global", "--prefix", absolute, "--ignore-scripts", "--registry", registry,
|
|
29
|
+
`--@limecloud:registry=${registry}`, "--no-audit", "--no-fund", `@limecloud/cli@${version}`] };
|
|
20
30
|
}
|
|
21
31
|
|
|
22
32
|
// 参数通过 argv 传递,不经 shell;npm 输出走 stderr,保持 stdout 的结构化合同。
|
|
23
33
|
export async function installRuntime(plan, { env = process.env, io = process, signal } = {}) {
|
|
34
|
+
let expected;
|
|
35
|
+
try {
|
|
36
|
+
if (JSON.stringify(plan) !== JSON.stringify(installationPlan(plan.version, plan.prefix))) throw new Error("Changed installation plan");
|
|
37
|
+
expected = await cliDistributionSnapshot(sourceRoot, plan.version);
|
|
38
|
+
} catch { throw new CLIError("installation_source_invalid", message("install.sourceInvalid")); }
|
|
39
|
+
signal?.throwIfAborted();
|
|
24
40
|
const npmScript = env.npm_execpath;
|
|
25
41
|
const fromNpm = typeof npmScript === "string" && /(?:^|[\\/])npm-cli\.js$/.test(npmScript);
|
|
26
42
|
if (process.platform === "win32" && !fromNpm) throw new CLIError("installation_failed", message("install.npmRequired"));
|
|
@@ -35,9 +51,14 @@ export async function installRuntime(plan, { env = process.env, io = process, si
|
|
|
35
51
|
});
|
|
36
52
|
try {
|
|
37
53
|
const moduleRoot = process.platform === "win32" ? join(plan.prefix, "node_modules") : join(plan.prefix, "lib", "node_modules");
|
|
38
|
-
const
|
|
54
|
+
const installedRoot = join(moduleRoot, "@limecloud", "cli");
|
|
55
|
+
const installed = await cliDistributionSnapshot(installedRoot, plan.version, { installed: true });
|
|
56
|
+
requireMatchingDistribution(expected, installed);
|
|
57
|
+
requireMatchingDistribution(expected, await cliDistributionSnapshot(sourceRoot, plan.version));
|
|
39
58
|
await access(plan.executable);
|
|
40
|
-
if (
|
|
59
|
+
if (process.platform !== "win32" && await realpath(plan.executable) !== await realpath(join(installedRoot, "bin/limecloud-cli.mjs"))) throw new Error("Unexpected executable target");
|
|
60
|
+
return { status: "matched", source: "running-cli-package", compared_files: expected.files.length,
|
|
61
|
+
executable: process.platform === "win32" ? "exists-unverified-wrapper" : "matched", provenance: "not-evaluated", dependency_contents: "not-evaluated" };
|
|
41
62
|
} catch { throw new CLIError("installation_failed", message("install.verificationFailed")); }
|
|
42
63
|
}
|
|
43
64
|
|
package/src/locales/en.json
CHANGED
|
@@ -122,7 +122,7 @@
|
|
|
122
122
|
"arguments.requiredPathParameter": "Required path parameter: {{name}}",
|
|
123
123
|
"arguments.unknownSchema": "Unknown schema path: {{target}}; start with schema",
|
|
124
124
|
"protocol.apiFailed": "API request failed: {{code}} (HTTP {{status}})",
|
|
125
|
-
"help.overview": "limecloud-cli — Lime Cloud CLI (@limecloud/cli)\n\nUsage:\n limecloud-cli <command> [resource] [method] [flags]\n limecloud-cli identity +login | +whoami\n limecloud-cli identity token exchange --data <json>\n limecloud-cli api <METHOD> <contract-path> [--params <json>] [--data <json>]\n limecloud-cli schema [service.resource.method | service resource method]\n\nIdentity:\n identity Public OAuth / OpenID Connect operations and +shortcuts\n\nCLI management:\n install Install this CLI and guide first-time setup\n config init | show | set: local public-client configuration\n auth login | status | whoami | refresh | logout | scopes | check\n whoami Show the local effective identity and token state\n doctor Check local configuration and contract (--offline; no network)\n diagnose Read-only application login configuration diagnosis\n task Prepare, precisely confirm and recover one member-group change\n skills Inspect and export optional public method candidates; never install automatically\n docs List, search and read public knowledge; print Web URLs\n\nAPI tools:\n api Call a contract HTTP path; list/schema/call also accept operationId\n schema Inspect public methods, parameters, responses and authentication\n\n",
|
|
125
|
+
"help.overview": "limecloud-cli — Lime Cloud CLI (@limecloud/cli)\n\nUsage:\n limecloud-cli <command> [resource] [method] [flags]\n limecloud-cli identity +login | +whoami\n limecloud-cli identity token exchange --data <json>\n limecloud-cli api <METHOD> <contract-path> [--params <json>] [--data <json>]\n limecloud-cli schema [service.resource.method | service resource method]\n\nIdentity:\n identity Public OAuth / OpenID Connect operations and +shortcuts\n\nCLI management:\n install Install this CLI and guide first-time setup\n update Check the latest stable CLI without installing (--check)\n config init | show | set: local public-client configuration\n auth login | status | whoami | refresh | logout | scopes | check\n whoami Show the local effective identity and token state\n doctor Check local configuration and contract (--offline; no network)\n diagnose Read-only application login configuration diagnosis\n task Prepare, precisely confirm and recover one member-group change\n skills Inspect and export optional public method candidates; never install automatically\n docs List, search and read public knowledge; print Web URLs\n\nAPI tools:\n api Call a contract HTTP path; list/schema/call also accept operationId\n schema Inspect public methods, parameters, responses and authentication\n\n",
|
|
126
126
|
"help.apiFlags": "API flags:\n --params JSON Query JSON; supports @file or - for stdin\n --data JSON OAuth form or API JSON body; supports @file or - for stdin\n --dry-run Print a redacted request without executing or changing state\n --format FORMAT json (default), ndjson, table or csv\n --fields PATHS Comma-separated data field paths\n --json Shorthand for --format json\n --param name=value Repeat a non-sensitive protocol parameter\n --param-env name=ENV Read a sensitive protocol parameter from an environment variable\n --header name=value Set a contract-declared HTTP header\n --file PATH Binary body for image/png methods; - reads stdin\n --show-secrets Explicitly include API response credentials in stdout\n\n",
|
|
127
127
|
"help.connectionFlags": "Connection flags:\n --issuer ORIGIN Auth origin (HTTPS; HTTP only on loopback)\n --api-origin ORIGIN Explicit business API origin, bound to the saved login\n --client-id ID Already registered sign-in client ID\n --auth-client-id ID Override the OAuth client context for a business API call\n\n --locale TAG Output language (also LIMECLOUD_LOCALE, LC_ALL, LC_MESSAGES, LANG)\n\nRun limecloud-cli <command> --help or schema identity before calling a method.\n",
|
|
128
128
|
"help.auth": "Usage: auth <login|status|whoami|refresh|logout|scopes|check> [flags]\n\nlogin --client-id ID [--scope \"openid profile\"] [--installation-id ID --space-id ID]\n --no-wait: return a verification URL and save private pending state\n --resume: complete the saved authorization without exposing its device code\nstatus: local state; whoami: remote UserInfo; refresh: rotate stored credentials\nlogout [--local-only]: revoke, then forget; --local-only forgets without revocation\nscopes: read supported OIDC scopes and report locally granted scopes\ncheck --scope SCOPES: check unexpired local granted scopes (no network)\n\nAuth commands use an already registered public client. --dry-run previews login, refresh, logout or scopes without execution.\n",
|
|
@@ -143,13 +143,13 @@
|
|
|
143
143
|
"columns.summary": "summary",
|
|
144
144
|
"columns.description": "description",
|
|
145
145
|
"columns.value": "value",
|
|
146
|
-
"help.install": "Usage:\n npx @limecloud/cli@latest install [--prefix DIRECTORY]\n limecloud-cli install --client-id ID --issuer ORIGIN --api-origin ORIGIN [--skip-login]\n limecloud-cli install --dry-run [--no-config]\n\nInstalls this package version to ~/.local by default, then guides public-client configuration and optional device authorization. Missing Client ID defaults come from the issuer public /runtime/v1/cli metadata. --prefix or LIMECLOUD_INSTALL_PREFIX changes the destination. Non-interactive runs leave missing configuration pending. --login explicitly starts device authorization without waiting; --no-config skips setup. Shell profiles and Agent skills are not modified.\n",
|
|
146
|
+
"help.install": "Usage:\n npx @limecloud/cli@latest install [--prefix DIRECTORY]\n limecloud-cli install --client-id ID --issuer ORIGIN --api-origin ORIGIN [--skip-login]\n limecloud-cli install --dry-run [--no-config]\n\nInstalls this package version to ~/.local by default, then guides public-client configuration and optional device authorization. Missing Client ID defaults come from the issuer public /runtime/v1/cli metadata. --prefix or LIMECLOUD_INSTALL_PREFIX changes the destination. Non-interactive runs leave missing configuration pending. --login explicitly starts device authorization without waiting; --no-config skips setup. The updated installer pins npmjs registries, disables npm lifecycle scripts and compares shipped CLI files with this running package before saving setup or starting login. This is not independent provenance or dependency verification. A failed npm installation may already have changed the target; inspect it before running it. Shell profiles and Agent skills are not modified.\n",
|
|
147
147
|
"install.invalidPrefix": "Provide a valid installation directory with --prefix",
|
|
148
148
|
"install.invalidMetadata": "The public CLI setup metadata does not match this identity service or the supported setup contract",
|
|
149
149
|
"install.discoveryUnavailable": "Public CLI setup metadata is unavailable. Run limecloud-cli config init and use the public CLI Client ID provided by your deployment administrator.",
|
|
150
150
|
"install.npmRequired": "Run installation through npx so the npm JavaScript entry point is available on Windows",
|
|
151
151
|
"install.failed": "CLI installation failed; check npm access and permissions for {{prefix}}, then retry install",
|
|
152
|
-
"install.verificationFailed": "npm finished, but the installed
|
|
152
|
+
"install.verificationFailed": "npm finished, but the installed package bytes or executable did not match the running CLI. Configuration and login were not started. The npm installation may already have changed the target; inspect it before running it.",
|
|
153
153
|
"install.usage": "Use install [--prefix DIRECTORY] [--client-id ID] [--skip-login | --login]; --login cannot be combined with --no-config",
|
|
154
154
|
"install.promptIssuer": "Identity service origin [{{value}}]: ",
|
|
155
155
|
"install.promptAPIOrigin": "Business API origin (leave empty for identity-only use) [{{value}}]: ",
|
|
@@ -224,12 +224,12 @@
|
|
|
224
224
|
"groupTask.recoveryHint": "Use the original ID to read the preparation or receipt, or explicitly resolve it. A missing receipt is not proof of non-commit. Never replace the key or automatically repeat execution, login, update or open a browser.",
|
|
225
225
|
"groupTask.resultHint": "This is the original Owner result. Recheck current authority for later actions; the local reference is not a permission or business-state cache.",
|
|
226
226
|
"groupTask.tableHeading": "Field Complete task value (values wrap; no truncation)",
|
|
227
|
-
"help.skills": "Usage: limecloud-cli skills list | inspect ID [--online --issuer ISSUER --api-origin API_ORIGIN] | export ID --host codex|skill --output-dir NEW_DIRECTORY --digest EXACT_DIGEST\nInspection is offline by default; --online compares
|
|
227
|
+
"help.skills": "Usage: limecloud-cli skills list | inspect ID [--installed-dir DIRECTORY] [--online --issuer ISSUER --api-origin API_ORIGIN] | export ID --host codex|skill --output-dir NEW_DIRECTORY --digest EXACT_DIGEST\nInspection is offline by default. --installed-dir compares one explicitly selected local copy; --online compares the candidate's required actions with the selected deployment without credentials. Local bytes, online compatibility and session loading are separate. The default wrapper is codex. Export does not install, enable, update or load a host plugin.\n",
|
|
228
228
|
"skills.loginTitle": "Application login configuration diagnosis",
|
|
229
229
|
"skills.loginDescription": "Guide one read-only Lime application login configuration diagnosis inside an existing Agent host. Actual login and repairs are separate tasks.",
|
|
230
230
|
"skills.groupTitle": "One organization member group change",
|
|
231
231
|
"skills.groupDescription": "Guide one group creation, rename or status change with full preparation review, explicit confirmation and original-result recovery. Membership, roles and bulk changes are separate tasks.",
|
|
232
|
-
"skills.usage": "Use skills list, skills inspect ID [--online --issuer ISSUER --api-origin API_ORIGIN], or skills export ID --output-dir NEW_DIRECTORY --digest EXACT_DIGEST; optional --host codex|skill selects a wrapper. No automatic installation or host changes.",
|
|
232
|
+
"skills.usage": "Use skills list, skills inspect ID [--installed-dir DIRECTORY] [--online --issuer ISSUER --api-origin API_ORIGIN], or skills export ID --output-dir NEW_DIRECTORY --digest EXACT_DIGEST; optional --host codex|skill selects a wrapper. No automatic installation or host changes.",
|
|
233
233
|
"skills.invalidArtifact": "The bundled public Skill artifact is incomplete or its integrity does not match. Obtain the CLI from a trusted source; no artifact was exported.",
|
|
234
234
|
"skills.candidateNotice": "Bundled public candidate only. Integrity matches this installed CLI; current online compatibility, provenance, withdrawal and host loading have not been verified. Inspect the complete files before separately selecting native installation or import.",
|
|
235
235
|
"skills.onlineNotice": "Only the candidate's required action contracts were compared with the selected deployment at the recorded time. Review every action and its expiry; matching is not authorization, provenance, withdrawal clearance or evidence that this method is installed or loaded. No installation, update or business action was performed.",
|
|
@@ -238,5 +238,31 @@
|
|
|
238
238
|
"skills.unsafeDirectory": "The export parent must be an existing physical directory without symbolic-link ancestors.",
|
|
239
239
|
"skills.confirmationChanged": "The selected artifact digest does not match. Inspect this exact wrapper and candidate before exporting.",
|
|
240
240
|
"skills.destinationExists": "The export destination already exists. Select a new directory; existing files and local changes are never overwritten.",
|
|
241
|
-
"skills.notFound": "This public method and wrapper are not included in the installed CLI. No download, installation or update was attempted."
|
|
241
|
+
"skills.notFound": "This public method and wrapper are not included in the installed CLI. No download, installation or update was attempted.",
|
|
242
|
+
"skills.directoryNotice": "Only the explicitly selected directory was compared with this bundled candidate. Review files and declared tool, read/write, required-scope and authentication changes in update_review. Security alternatives remain separate; missing or unsupported declarations stay unknown. Additional contents were not read; remote compatibility was not checked. Matching bytes or declarations do not establish actual method behavior, external data transfers, provenance, authorization, host registration or current-session loading. No files or host settings were changed.",
|
|
243
|
+
"skills.directoryOnlineNotice": "The bundled candidate and selected copy's valid action declaration were compared separately against the same deployment observations. Review update_review tool and access changes, both compatibility results, file binding, coverage and expiry. A missing or unsupported old declaration remains unknown; candidate compatibility cannot stand in for it. Declarations are self-reported and do not prove method behavior, external data transfers, trust, authorization or session loading. No update or business action was performed.",
|
|
244
|
+
"skills.unsafeInstalledDirectory": "The selected method directory is unsafe or exceeds inspection limits. Select a physical directory with ordinary, unlinked files; links, special files and oversized or unsupported entries are rejected.",
|
|
245
|
+
"skills.installedDirectoryChanged": "The selected method directory changed during inspection. No comparison was accepted. Keep the copy stable and explicitly inspect it again.",
|
|
246
|
+
"skills.installedIdentityMismatch": "The selected directory does not identify the requested method and wrapper. Select that single method's root directory; no files were changed.",
|
|
247
|
+
"skills.installedDirectoryUnavailable": "The selected method directory could not be read. Check the explicit path and access, then inspect again; no host or method files were changed.",
|
|
248
|
+
"install.sourceInvalid": "The running CLI package or installation plan cannot be verified. Rebuild a source checkout or obtain a complete CLI package from a trusted channel before installing; npm was not started.",
|
|
249
|
+
"install.invalidVersion": "The CLI installation requires an exact three-part package version.",
|
|
250
|
+
"help.update": "Usage: update --check [--format json|table|csv|ndjson]\n\nExplicitly check the latest stable CLI version at the fixed npmjs registry, without credentials, settings, package downloads or installation. No redirects, retries or cached result. available/current/ahead compares versions only; unavailable means the latest package is marked deprecated; unknown means the check failed or its metadata is invalid. None proves API compatibility, provenance or withdrawal clearance.\nThe installation owner and current host loading remain unknown. For a newer version, review_argv only describes a pinned npx installation preview; running it separately acquires and executes that package. Confirm the real installation owner and target prefix before any update. Skills/plugins update through their own owner. Original operations are never replayed.\n",
|
|
251
|
+
"update.usage": "Use update --check [--format json|table|csv|ndjson]; this command only checks and never installs",
|
|
252
|
+
"update.available": "A newer stable CLI version is advertised by this registry. Review its source and release notes before a separate update.",
|
|
253
|
+
"update.current": "The running version equals the registry latest version observed by this check. This does not assess business compatibility or withdrawal.",
|
|
254
|
+
"update.ahead": "The running version is newer than the registry latest version observed by this check. No downgrade is proposed.",
|
|
255
|
+
"update.registryUnavailable": "The registry check is unavailable. The latest version remains unknown; no fallback, retry or update was performed.",
|
|
256
|
+
"update.invalidMetadata": "The registry response does not identify a supported exact CLI release. The latest version remains unknown.",
|
|
257
|
+
"update.latestDeprecated": "The registry marks its latest package deprecated. No update candidate is proposed. The deprecation text is not executed or used as an alternate source.",
|
|
258
|
+
"update.owner": "The current installation owner and prefix were not detected. Use the actual package or installation manager. A review_argv, when present, only previews an npm-prefix installation; invoking npx separately first acquires and executes the pinned package.",
|
|
259
|
+
"update.methods": "This check installs nothing and updates no Skills or plugins. Use their actual host/import owner for a separate update; current-session loading remains unknown.",
|
|
260
|
+
"update.recovery": "Keep original operation references. Checking or updating never confirms, retries or replays an earlier business operation.",
|
|
261
|
+
"update.currentVersion": "Running version",
|
|
262
|
+
"update.latestVersion": "Registry latest",
|
|
263
|
+
"update.source": "Metadata source",
|
|
264
|
+
"update.observedAt": "Observed at (UTC)",
|
|
265
|
+
"update.result": "Result",
|
|
266
|
+
"update.review": "Optional review argv",
|
|
267
|
+
"update.nextStep": "Next step"
|
|
242
268
|
}
|
package/src/locales/zh-CN.json
CHANGED
|
@@ -122,7 +122,7 @@
|
|
|
122
122
|
"arguments.requiredPathParameter": "缺少必填路径参数:{{name}}",
|
|
123
123
|
"arguments.unknownSchema": "未知 Schema 路径:{{target}};请从 schema 开始",
|
|
124
124
|
"protocol.apiFailed": "API 请求失败:{{code}}(HTTP {{status}})",
|
|
125
|
-
"help.overview": "limecloud-cli — Lime Cloud CLI (@limecloud/cli)\n\n用法:\n limecloud-cli <command> [resource] [method] [flags]\n limecloud-cli identity +login | +whoami\n limecloud-cli identity token exchange --data <json>\n limecloud-cli api <METHOD> <contract-path> [--params <json>] [--data <json>]\n limecloud-cli schema [service.resource.method | service resource method]\n\n身份协议:\n identity 公开 OAuth / OpenID Connect 操作与 +快捷命令\n\nCLI 管理:\n install 安装 CLI 并引导首次配置\n config init | show | set:本地公共客户端配置\n auth login | status | whoami | refresh | logout | scopes | check\n whoami 查看本地有效身份与令牌状态\n doctor 检查本地配置与合同(--offline;不访问网络)\n diagnose 只读应用登录配置诊断\n task 单成员组准备、精确确认及原操作恢复\n skills 检查及导出可选公开方法候选,不自动安装\n docs 列出、检索和读取公开知识,输出网页 URL\n\nAPI 工具:\n api 调用合同中的 HTTP 路径;list/schema/call 也支持 operationId\n schema 查看公开方法、参数、响应与身份验证\n\n",
|
|
125
|
+
"help.overview": "limecloud-cli — Lime Cloud CLI (@limecloud/cli)\n\n用法:\n limecloud-cli <command> [resource] [method] [flags]\n limecloud-cli identity +login | +whoami\n limecloud-cli identity token exchange --data <json>\n limecloud-cli api <METHOD> <contract-path> [--params <json>] [--data <json>]\n limecloud-cli schema [service.resource.method | service resource method]\n\n身份协议:\n identity 公开 OAuth / OpenID Connect 操作与 +快捷命令\n\nCLI 管理:\n install 安装 CLI 并引导首次配置\n update 只检查最新稳定 CLI,不安装(--check)\n config init | show | set:本地公共客户端配置\n auth login | status | whoami | refresh | logout | scopes | check\n whoami 查看本地有效身份与令牌状态\n doctor 检查本地配置与合同(--offline;不访问网络)\n diagnose 只读应用登录配置诊断\n task 单成员组准备、精确确认及原操作恢复\n skills 检查及导出可选公开方法候选,不自动安装\n docs 列出、检索和读取公开知识,输出网页 URL\n\nAPI 工具:\n api 调用合同中的 HTTP 路径;list/schema/call 也支持 operationId\n schema 查看公开方法、参数、响应与身份验证\n\n",
|
|
126
126
|
"help.apiFlags": "API 选项:\n --params JSON 查询 JSON;支持 @file 或 - 从标准输入读取\n --data JSON OAuth 表单或 API JSON 请求体;支持 @file 或 -\n --dry-run 输出脱敏请求,不执行请求或改变状态\n --format FORMAT json(默认)、ndjson、table 或 csv\n --fields PATHS 逗号分隔的数据字段路径\n --json --format json 的简写\n --param name=value 重复提供非敏感协议参数\n --param-env name=ENV 从环境变量读取敏感协议参数\n --header name=value 设置合同声明的 HTTP 请求头\n --file PATH image/png 方法的二进制请求体;- 从标准输入读取\n --show-secrets 显式在标准输出中包含 API 响应凭据\n\n",
|
|
127
127
|
"help.connectionFlags": "连接选项:\n --issuer ORIGIN Auth 来源(HTTPS;HTTP 仅允许回环地址)\n --api-origin ORIGIN 显式指定业务 API 来源,并绑定保存的登录\n --client-id ID 已登记的登录客户端 ID\n --auth-client-id ID 覆盖业务 API 调用的 OAuth 客户端上下文\n --locale TAG 输出语言(也支持 LIMECLOUD_LOCALE、LC_ALL、LC_MESSAGES、LANG)\n\n调用方法前,请运行 limecloud-cli <command> --help 或 schema identity。\n",
|
|
128
128
|
"help.auth": "用法:auth <login|status|whoami|refresh|logout|scopes|check> [flags]\n\nlogin --client-id ID [--scope \"openid profile\"] [--installation-id ID --space-id ID]\n --no-wait:返回验证链接并保存私有待完成授权状态\n --resume:完成已保存授权,不暴露设备码\nstatus:本地状态;whoami:远程 UserInfo;refresh:轮换保存的凭据\nlogout [--local-only]:先撤销再清除;--local-only 仅清除本地凭据\nscopes:读取支持的 OIDC Scope 并显示本地已授予 Scope\ncheck --scope SCOPES:检查未到期的本地已授予 Scope(不访问网络)\n\nAuth 命令使用已登记的公共客户端。--dry-run 可预览 login、refresh、logout 或 scopes,不执行操作。\n",
|
|
@@ -143,13 +143,13 @@
|
|
|
143
143
|
"columns.summary": "摘要",
|
|
144
144
|
"columns.description": "说明",
|
|
145
145
|
"columns.value": "值",
|
|
146
|
-
"help.install": "用法:\n npx @limecloud/cli@latest install [--prefix DIRECTORY]\n limecloud-cli install --client-id ID --issuer ORIGIN --api-origin ORIGIN [--skip-login]\n limecloud-cli install --dry-run [--no-config]\n\n默认将本次包版本安装到 ~/.local,然后引导配置公共客户端及可选的设备授权。缺少 Client ID 时从身份服务的公开 /runtime/v1/cli 元数据获取默认配置。使用 --prefix 或 LIMECLOUD_INSTALL_PREFIX 指定目录。非交互运行时,缺少的配置保持待完成。--login 显式发起设备授权后立即返回;--no-config
|
|
146
|
+
"help.install": "用法:\n npx @limecloud/cli@latest install [--prefix DIRECTORY]\n limecloud-cli install --client-id ID --issuer ORIGIN --api-origin ORIGIN [--skip-login]\n limecloud-cli install --dry-run [--no-config]\n\n默认将本次包版本安装到 ~/.local,然后引导配置公共客户端及可选的设备授权。缺少 Client ID 时从身份服务的公开 /runtime/v1/cli 元数据获取默认配置。使用 --prefix 或 LIMECLOUD_INSTALL_PREFIX 指定目录。非交互运行时,缺少的配置保持待完成。--login 显式发起设备授权后立即返回;--no-config 跳过配置。更新后的安装器固定 npmjs 来源、禁用 npm 生命周期脚本,保存配置或登录前逐文件核对安装内容与当前包。此检查不证明独立发布来源或依赖内容。npm 失败时可能已改动目标目录,先核对再运行。不修改 shell 配置或 Agent Skills。\n",
|
|
147
147
|
"install.invalidPrefix": "请使用 --prefix 提供有效的安装目录",
|
|
148
148
|
"install.invalidMetadata": "公共 CLI 配置元数据与当前身份服务或支持的配置合同不匹配",
|
|
149
149
|
"install.discoveryUnavailable": "暂时无法获取公共 CLI 配置元数据。请运行 limecloud-cli config init,并使用部署管理员提供的公共 CLI Client ID。",
|
|
150
150
|
"install.npmRequired": "在 Windows 上请通过 npx 安装,以使用 npm 的 JavaScript 入口",
|
|
151
151
|
"install.failed": "CLI 安装失败;请检查 npm 访问与 {{prefix}} 的权限后重新执行 install",
|
|
152
|
-
"install.verificationFailed": "npm
|
|
152
|
+
"install.verificationFailed": "npm 已结束,但安装包字节或可执行入口与当前 CLI 不一致。未开始配置或登录;npm 可能已改动目标目录,请先检查再运行。",
|
|
153
153
|
"install.usage": "请使用 install [--prefix DIRECTORY] [--client-id ID] [--skip-login | --login];--login 不能与 --no-config 同时使用",
|
|
154
154
|
"install.promptIssuer": "身份服务来源 [{{value}}]:",
|
|
155
155
|
"install.promptAPIOrigin": "业务 API 来源(仅使用身份协议可留空)[{{value}}]:",
|
|
@@ -224,12 +224,12 @@
|
|
|
224
224
|
"groupTask.recoveryHint": "使用原 ID 读取准备或回执,或显式终结原操作。没有回执不能证明未提交。不得换键或自动重复执行、登录、更新及开页。",
|
|
225
225
|
"groupTask.resultHint": "结果来自原领域 Owner。后续动作仍须核对当前权限;本地引用不是授权或业务状态缓存。",
|
|
226
226
|
"groupTask.tableHeading": "字段 完整任务值(可换行,不截断)",
|
|
227
|
-
"help.skills": "用法:limecloud-cli skills list | inspect ID [--online --issuer ISSUER --api-origin API_ORIGIN] | export ID --host codex|skill --output-dir NEW_DIRECTORY --digest EXACT_DIGEST\n
|
|
227
|
+
"help.skills": "用法:limecloud-cli skills list | inspect ID [--installed-dir DIRECTORY] [--online --issuer ISSUER --api-origin API_ORIGIN] | export ID --host codex|skill --output-dir NEW_DIRECTORY --digest EXACT_DIGEST\n默认离线检查。--installed-dir 比较明确指定的一个本地副本;--online 不带凭据,比较候选所需动作与所选部署。本地字节、线上兼容和会话加载分别报告。默认包装为 codex。导出不安装、启用、更新或加载宿主插件。\n",
|
|
228
228
|
"skills.loginTitle": "应用登录配置诊断",
|
|
229
229
|
"skills.loginDescription": "在现有 Agent 宿主内组织一个 Lime 应用的只读登录配置诊断;真实登录及修复属于独立任务。",
|
|
230
230
|
"skills.groupTitle": "单个企业成员组变更",
|
|
231
231
|
"skills.groupDescription": "组织一个组的创建、改名或状态变更,完整核对准备、明确确认并恢复原结果;成员增删、角色和批量变更属于独立任务。",
|
|
232
|
-
"skills.usage": "使用 skills list、skills inspect ID [--online --issuer ISSUER --api-origin API_ORIGIN],或 skills export ID --output-dir NEW_DIRECTORY --digest EXACT_DIGEST;可用 --host codex|skill 选择包装,不自动安装或修改宿主。",
|
|
232
|
+
"skills.usage": "使用 skills list、skills inspect ID [--installed-dir DIRECTORY] [--online --issuer ISSUER --api-origin API_ORIGIN],或 skills export ID --output-dir NEW_DIRECTORY --digest EXACT_DIGEST;可用 --host codex|skill 选择包装,不自动安装或修改宿主。",
|
|
233
233
|
"skills.invalidArtifact": "随包公开 Skill 制品不完整或摘要不一致。请从可信来源取得 CLI;本次未导出制品。",
|
|
234
234
|
"skills.candidateNotice": "仅为随包公开候选;完整性与当前已安装 CLI 一致,线上兼容、来源证明、撤回及宿主加载均未验证。先检查全部文件,再独立选择原生安装或导入。",
|
|
235
235
|
"skills.onlineNotice": "仅在记录时间逐项比较候选所需动作与所选部署的合同。请核对全部动作及期限;匹配不代表权限、来源证明、撤回状态,也不证明方法已安装或当前会话已加载。没有安装、更新或执行业务动作。",
|
|
@@ -238,5 +238,31 @@
|
|
|
238
238
|
"skills.unsafeDirectory": "导出父路径必须是已存在的真实目录,祖先路径不得包含符号链接。",
|
|
239
239
|
"skills.confirmationChanged": "所选制品摘要不一致,请先检查该精确包装和候选再导出。",
|
|
240
240
|
"skills.destinationExists": "导出目的目录已存在,请选择新目录;不覆盖已有文件或本地修改。",
|
|
241
|
-
"skills.notFound": "当前 CLI 未包含该公开方法或包装,本次未尝试下载、安装或更新。"
|
|
241
|
+
"skills.notFound": "当前 CLI 未包含该公开方法或包装,本次未尝试下载、安装或更新。",
|
|
242
|
+
"skills.directoryNotice": "仅比较明确指定的目录与随包候选。请核对文件和 update_review 中声明的工具、读写、所需 Scope 及认证变化;认证备选关系保持独立,缺失或不支持的声明保持未知。额外正文未读,尚未比较线上兼容性。字节或声明匹配不证明方法实际行为、数据外发、来源、权限、宿主登记或当前会话已加载。没有修改文件或宿主设置。",
|
|
243
|
+
"skills.directoryOnlineNotice": "随包候选与指定副本的有效动作声明分别对照同一批部署观察。请核对 update_review 工具和权限要求变化、两组兼容结果、文件绑定、覆盖及期限;旧声明缺失或不支持仍为未知,候选匹配不能代替它。声明属于自报,不证明方法行为、数据外发、可信来源、权限或会话加载。没有更新或执行业务动作。",
|
|
244
|
+
"skills.unsafeInstalledDirectory": "所选方法目录不安全或超出检查上限。请指定只含普通非链接文件的真实目录;链接、特殊文件及过大或不支持的条目均被拒绝。",
|
|
245
|
+
"skills.installedDirectoryChanged": "所选方法目录在检查期间变化,本次比较未被接受。请保持副本稳定,再明确发起检查。",
|
|
246
|
+
"skills.installedIdentityMismatch": "所选目录的身份与请求的方法或包装不符。请指定该单项方法的根目录;没有修改任何文件。",
|
|
247
|
+
"skills.installedDirectoryUnavailable": "无法读取所选方法目录。请检查明确路径及访问权限后重新检查;没有修改宿主或方法文件。",
|
|
248
|
+
"install.sourceInvalid": "无法核验当前 CLI 包或安装计划。源码运行请先构建,或从受信渠道取得完整 CLI 包后再安装;尚未启动 npm。",
|
|
249
|
+
"install.invalidVersion": "CLI 安装必须使用精确的三段包版本。",
|
|
250
|
+
"help.update": "用法:update --check [--format json|table|csv|ndjson]\n\n显式从固定 npmjs 源检查最新稳定 CLI 版本,不读取凭据或配置、不下载包、不安装。不跟随重定向、重试或缓存。available/current/ahead 只比较版本;unavailable 表示 latest 包声明弃用;unknown 表示检查失败或元数据无效。均不证明 API 兼容、发布来源或未被撤回。\n当前安装 Owner 和宿主加载状态保持未知。有新版时,review_argv 只描述固定版本 npx 安装预览;另行运行会先取得并执行该包。实际升级前确认原安装 Owner 和目标前缀;Skills/插件交由各自 Owner 更新。不会重放原操作。\n",
|
|
251
|
+
"update.usage": "使用 update --check [--format json|table|csv|ndjson];本命令只检查,不安装",
|
|
252
|
+
"update.available": "该源公布了更新的稳定 CLI 版本。另行升级前请核对来源和发布说明。",
|
|
253
|
+
"update.current": "当前运行版本与本次观察的 registry latest 相同,不表示业务兼容或未被撤回。",
|
|
254
|
+
"update.ahead": "当前运行版本高于本次观察的 registry latest,不建议降级。",
|
|
255
|
+
"update.registryUnavailable": "无法完成 registry 检查,最新版本保持未知;未切换来源、重试或更新。",
|
|
256
|
+
"update.invalidMetadata": "源响应未标明受支持的 CLI 精确发行版,最新版本保持未知。",
|
|
257
|
+
"update.latestDeprecated": "源将其 latest 包标为弃用,不提供升级候选;不会执行弃用正文或将其作为其他来源。",
|
|
258
|
+
"update.owner": "未检测当前安装 Owner 或前缀,请使用实际包管理器或安装管理器。review_argv 若存在,只预览 npm 前缀安装;另行调用 npx 会先取得并执行固定版本的包。",
|
|
259
|
+
"update.methods": "本次检查不安装,也不更新 Skills 或插件;单独更新交给实际宿主或导入 Owner,当前会话加载状态保持未知。",
|
|
260
|
+
"update.recovery": "保留原操作引用,检查或升级不会确认、重试或重放此前的业务操作。",
|
|
261
|
+
"update.currentVersion": "当前运行版本",
|
|
262
|
+
"update.latestVersion": "源 latest 版本",
|
|
263
|
+
"update.source": "元数据来源",
|
|
264
|
+
"update.observedAt": "观察时间(UTC)",
|
|
265
|
+
"update.result": "结果",
|
|
266
|
+
"update.review": "可选预览参数",
|
|
267
|
+
"update.nextStep": "后续操作"
|
|
242
268
|
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// 安装前审阅只投影正式合同的声明,不判断当前授权、方法实际行为或宿主加载。
|
|
2
|
+
const record = value => value && typeof value === "object" && !Array.isArray(value);
|
|
3
|
+
const exact = (value, keys) => record(value) && Object.keys(value).length === keys.length && keys.every(key => Object.hasOwn(value, key));
|
|
4
|
+
const scope = value => typeof value === "string" && /^[\x21\x23-\x5b\x5d-\x7e]{1,256}$/.test(value);
|
|
5
|
+
const scopes = value => Array.isArray(value) && value.length <= 100 && value.every(scope) && new Set(value).size === value.length;
|
|
6
|
+
const schemeName = value => /^[A-Za-z0-9_.-]{1,100}$/.test(value);
|
|
7
|
+
const sorted = values => [...values].sort();
|
|
8
|
+
const security = requirements => requirements.map(requirement => Object.fromEntries(Object.keys(requirement).sort()
|
|
9
|
+
.map(name => [name, sorted(requirement[name])]))).sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b), "en"));
|
|
10
|
+
const schemeFields = ["type", "scheme", "name", "in"];
|
|
11
|
+
export const skillEffect = actions => actions.every(action => ["GET", "HEAD", "OPTIONS"].includes(action.method)) ? "read" : "write";
|
|
12
|
+
const normalize = value => ({ operation_id: value.operation_id, required_scopes: sorted(value.required_scopes), security: security(value.security),
|
|
13
|
+
security_schemes: Object.fromEntries(Object.keys(value.security_schemes).sort().map(name => [name,
|
|
14
|
+
Object.fromEntries(schemeFields.filter(key => value.security_schemes[name][key] !== undefined).map(key => [key, value.security_schemes[name][key]]))])) });
|
|
15
|
+
|
|
16
|
+
export function skillActionAccess(contract, operation) {
|
|
17
|
+
const requirements = operation.security ?? contract.security ?? [];
|
|
18
|
+
const names = [...new Set(requirements.flatMap(requirement => Object.keys(requirement)))];
|
|
19
|
+
return normalize({ operation_id: operation.operationId, required_scopes: [...new Set(operation["x-lime-required-scopes"] ?? [])],
|
|
20
|
+
security: requirements, security_schemes: Object.fromEntries(names.map(name => [name,
|
|
21
|
+
Object.fromEntries(schemeFields.filter(key => contract.components?.securitySchemes?.[name]?.[key] !== undefined)
|
|
22
|
+
.map(key => [key, contract.components.securitySchemes[name][key]]))])) });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function validSkillAccess(values, operations) {
|
|
26
|
+
return Array.isArray(values) && values.length === operations.length && new Set(values.map(value => value?.operation_id)).size === values.length &&
|
|
27
|
+
values.every(value => exact(value, ["operation_id", "required_scopes", "security", "security_schemes"]) &&
|
|
28
|
+
operations.some(operation => operation.operation_id === value.operation_id) && scopes(value.required_scopes) &&
|
|
29
|
+
Array.isArray(value.security) && value.security.length <= 16 && value.security.every(requirement => record(requirement) &&
|
|
30
|
+
Object.keys(requirement).length <= 8 && Object.entries(requirement).every(([name, values]) => schemeName(name) && scopes(values))) &&
|
|
31
|
+
record(value.security_schemes) && Object.keys(value.security_schemes).length <= 16 &&
|
|
32
|
+
Object.keys(value.security_schemes).every(name => schemeName(name) && value.security.some(requirement => Object.hasOwn(requirement, name))) &&
|
|
33
|
+
value.security.every(requirement => Object.keys(requirement).every(name => Object.hasOwn(value.security_schemes, name))) &&
|
|
34
|
+
Object.values(value.security_schemes).every(scheme => record(scheme) && Object.keys(scheme).every(key => schemeFields.includes(key)) &&
|
|
35
|
+
["apiKey", "http", "oauth2", "openIdConnect", "mutualTLS"].includes(scheme.type) &&
|
|
36
|
+
Object.values(scheme).every(text => typeof text === "string" && text.length > 0 && text.length <= 128 && !/[\x00-\x20\x7f]/.test(text)) &&
|
|
37
|
+
(scheme.type !== "http" || typeof scheme.scheme === "string") &&
|
|
38
|
+
(scheme.type !== "apiKey" || typeof scheme.name === "string" && ["header", "query", "cookie"].includes(scheme.in))));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const actionIdentity = value => [value.command, value.method, value.path, value.origin, value.contract_revision];
|
|
42
|
+
const changes = (before, after, equals, describe) => {
|
|
43
|
+
const previous = new Map(before.map(value => [value.operation_id, value])), next = new Map(after.map(value => [value.operation_id, value]));
|
|
44
|
+
const added = [], removed = [], changed = [], unchanged = [];
|
|
45
|
+
for (const [id, value] of next) {
|
|
46
|
+
if (!previous.has(id)) added.push(value);
|
|
47
|
+
else if (!equals(previous.get(id), value)) changed.push(describe(previous.get(id), value));
|
|
48
|
+
else unchanged.push(id);
|
|
49
|
+
}
|
|
50
|
+
for (const [id, value] of previous) if (!next.has(id)) removed.push(value);
|
|
51
|
+
return { added, removed, changed, unchanged };
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export function reviewSkillUpdate(artifact, installed) {
|
|
55
|
+
const boundaries = { source: "declared-action-and-access-difference", declaration_only: true, authorization: "not-evaluated",
|
|
56
|
+
external_io: "not-evaluated", provenance: "not-evaluated", online_withdrawal: "not-evaluated", current_session_loaded: "unknown" };
|
|
57
|
+
if (installed.status !== "declared") return { ...boundaries, status: "unknown", reason: installed.reason, actions: null, access: null };
|
|
58
|
+
const actions = changes(installed.operations, artifact.operations, (before, after) => JSON.stringify(actionIdentity(before)) === JSON.stringify(actionIdentity(after)),
|
|
59
|
+
(before, after) => ({ operation_id: after.operation_id, before, after }));
|
|
60
|
+
const access = changes(installed.access_requirements, artifact.access_requirements, (before, after) => JSON.stringify(normalize(before)) === JSON.stringify(normalize(after)),
|
|
61
|
+
(before, after) => ({ operation_id: after.operation_id, before, after,
|
|
62
|
+
added_required_scopes: after.required_scopes.filter(scope => !before.required_scopes.includes(scope)),
|
|
63
|
+
removed_required_scopes: before.required_scopes.filter(scope => !after.required_scopes.includes(scope)),
|
|
64
|
+
security_changed: JSON.stringify({ security: normalize(before).security, schemes: normalize(before).security_schemes }) !==
|
|
65
|
+
JSON.stringify({ security: normalize(after).security, schemes: normalize(after).security_schemes }) }));
|
|
66
|
+
return { ...boundaries, status: [actions, access].some(value => value.added.length || value.removed.length || value.changed.length) ? "changed" : "unchanged",
|
|
67
|
+
installed_method_files: installed.method_files, effect: { before: skillEffect(installed.operations), after: skillEffect(artifact.operations) }, actions, access };
|
|
68
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { validSkillAccess } from "./product-skill-access.mjs";
|
|
3
|
+
|
|
4
|
+
const digest = /^sha256:[a-f0-9]{64}$/;
|
|
5
|
+
const record = value => value && typeof value === "object" && !Array.isArray(value);
|
|
6
|
+
const exact = (value, keys) => record(value) && Object.keys(value).length === keys.length && keys.every(key => Object.hasOwn(value, key));
|
|
7
|
+
const actionKeys = ["operation_id", "command", "method", "path", "origin", "contract_revision"];
|
|
8
|
+
const fileKeys = ["path", "bytes", "sha256"];
|
|
9
|
+
const safePath = value => typeof value === "string" && value.length <= 240 && value.split("/").every(part => /^[A-Za-z0-9_.-]+$/.test(part) && ![".", ".."].includes(part));
|
|
10
|
+
|
|
11
|
+
export const skillContractPath = (id, wrapper) => `${wrapper === "codex" ? `skills/${id}/` : ""}references/action-contracts.json`;
|
|
12
|
+
|
|
13
|
+
export function skillContractDeclaration({ id, wrapper, version, operations, access_requirements, files }) {
|
|
14
|
+
return { schema_version: 2, id, wrapper, version, source: "@limecloud/cli", operations, access_requirements,
|
|
15
|
+
files: files.map(({ path, bytes, sha256 }) => ({ path, bytes, sha256 })) };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function validSkillContractDeclaration(value, { id, wrapper }) {
|
|
19
|
+
const prefix = wrapper === "codex" ? `skills/${id}/` : "";
|
|
20
|
+
return exact(value, ["schema_version", "id", "wrapper", "version", "source", "operations", "access_requirements", "files"]) &&
|
|
21
|
+
value.schema_version === 2 && value.id === id && value.wrapper === wrapper && value.source === "@limecloud/cli" &&
|
|
22
|
+
typeof value.version === "string" && /^\d+\.\d+\.\d+$/.test(value.version) &&
|
|
23
|
+
Array.isArray(value.operations) && value.operations.length > 0 && value.operations.length <= 100 &&
|
|
24
|
+
new Set(value.operations.map(action => action?.operation_id)).size === value.operations.length &&
|
|
25
|
+
value.operations.every(action => exact(action, actionKeys) && typeof action.operation_id === "string" && /^[a-z][a-z0-9_]{0,199}$/.test(action.operation_id) &&
|
|
26
|
+
typeof action.command === "string" && /^[a-z][a-z0-9_]*(\.[a-z][a-z0-9_]*){2}$/.test(action.command) &&
|
|
27
|
+
["GET", "HEAD", "OPTIONS", "POST", "PUT", "PATCH", "DELETE"].includes(action.method) && ["auth", "api"].includes(action.origin) &&
|
|
28
|
+
typeof action.path === "string" && /^\/[A-Za-z0-9_./{}-]+$/.test(action.path) && typeof action.contract_revision === "string" && digest.test(action.contract_revision)) &&
|
|
29
|
+
validSkillAccess(value.access_requirements, value.operations) &&
|
|
30
|
+
Array.isArray(value.files) && value.files.length > 0 && value.files.length <= 19 &&
|
|
31
|
+
new Set(value.files.map(file => file?.path)).size === value.files.length &&
|
|
32
|
+
value.files.every(file => exact(file, fileKeys) && safePath(file.path) &&
|
|
33
|
+
(file.path === "plugin.json" && wrapper === "codex" || file.path === `${prefix}SKILL.md` || file.path.startsWith(`${prefix}references/`) && file.path.endsWith(".md")) &&
|
|
34
|
+
Number.isSafeInteger(file.bytes) && file.bytes >= 0 && file.bytes <= 131072 && typeof file.sha256 === "string" && digest.test(file.sha256)) &&
|
|
35
|
+
value.files.some(file => file.path === `${prefix}SKILL.md`) && (wrapper !== "codex" || value.files.some(file => file.path === "plugin.json"));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// 只解释已按当前允许清单读取的固定声明;声明不能增加文件读取或网络目标。
|
|
39
|
+
export function inspectSkillContractDeclaration(artifact, files, additional, reportedVersion) {
|
|
40
|
+
const path = skillContractPath(artifact.id, artifact.wrapper);
|
|
41
|
+
const file = files.find(file => file.path === path)?.installed;
|
|
42
|
+
const unknown = reason => ({ status: "unknown", reason, path, authorization: "not-evaluated" });
|
|
43
|
+
if (!file) return unknown("declaration-missing");
|
|
44
|
+
let value;
|
|
45
|
+
try { value = JSON.parse(file.content); } catch { return unknown("invalid-declaration"); }
|
|
46
|
+
if (!validSkillContractDeclaration(value, artifact) || reportedVersion !== null && value.version !== reportedVersion) return unknown("invalid-declaration");
|
|
47
|
+
const declared = new Map(value.files.map(file => [file.path, file]));
|
|
48
|
+
const actual = files.filter(file => file.installed && file.path !== path);
|
|
49
|
+
const unavailable = value.files.some(file => !files.some(read => read.path === file.path && read.installed));
|
|
50
|
+
const changed = actual.some(file => !declared.has(file.path) || declared.get(file.path).sha256 !== file.installed.sha256 || declared.get(file.path).bytes !== file.installed.bytes);
|
|
51
|
+
// 额外正文没有读取;目录/文件存在即可判定声明未覆盖完整方法。
|
|
52
|
+
const methodFiles = unavailable ? "unknown" : changed || additional.length ? "different" : "matched";
|
|
53
|
+
return { status: "declared", source: "explicit-local-declaration", path, content_revision: `sha256:${createHash("sha256").update(file.content).digest("hex")}`,
|
|
54
|
+
reported_version: value.version, method_files: methodFiles, operations: value.operations, access_requirements: value.access_requirements,
|
|
55
|
+
compatibility: { remote: "not-evaluated", authorization: "not-evaluated" },
|
|
56
|
+
trust: { provenance: "not-evaluated", action_coverage: "self-reported" } };
|
|
57
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { constants } from "node:fs";
|
|
3
|
+
import { lstat, open, readdir, realpath } from "node:fs/promises";
|
|
4
|
+
import { dirname, join, resolve } from "node:path";
|
|
5
|
+
import { message } from "./i18n.mjs";
|
|
6
|
+
import { CLIError } from "./protocol.mjs";
|
|
7
|
+
import { inspectSkillContractDeclaration } from "./product-skill-contracts.mjs";
|
|
8
|
+
|
|
9
|
+
const reject = (code = "skill_directory_unsafe", key = "skills.unsafeInstalledDirectory") => {
|
|
10
|
+
throw new CLIError(code, message(key));
|
|
11
|
+
};
|
|
12
|
+
const changed = () => reject("skill_directory_changed", "skills.installedDirectoryChanged");
|
|
13
|
+
const identityMismatch = () => reject("skill_directory_identity_mismatch", "skills.installedIdentityMismatch");
|
|
14
|
+
const sha = value => `sha256:${createHash("sha256").update(value).digest("hex")}`;
|
|
15
|
+
const signature = stat => [stat.dev, stat.ino, stat.mode, stat.nlink, stat.size, stat.mtimeNs, stat.ctimeNs].join(":");
|
|
16
|
+
|
|
17
|
+
// 只检查用户指定的单个目录;不扫描宿主位置、不解释或运行其中的指令。
|
|
18
|
+
async function inventory(directory) {
|
|
19
|
+
const entries = new Map();
|
|
20
|
+
async function visit(path, relative = "", depth = 0) {
|
|
21
|
+
if (depth > 8 || entries.size >= 128) reject();
|
|
22
|
+
const stat = await lstat(path, { bigint: true });
|
|
23
|
+
if (stat.isSymbolicLink() || !(stat.isDirectory() || stat.isFile()) || stat.isFile() && stat.nlink !== 1n) reject();
|
|
24
|
+
entries.set(relative, { type: stat.isDirectory() ? "directory" : "file", signature: signature(stat), bytes: Number(stat.size) });
|
|
25
|
+
if (!stat.isDirectory()) return;
|
|
26
|
+
for (const name of (await readdir(path)).sort()) {
|
|
27
|
+
if (!/^[A-Za-z0-9_.-]+$/.test(name) || name === "." || name === "..") reject();
|
|
28
|
+
await visit(join(path, name), relative ? `${relative}/${name}` : name, depth + 1);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
await visit(directory);
|
|
32
|
+
return entries;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function ordinaryContent(directory, path, entry) {
|
|
36
|
+
if (entry.type !== "file" || entry.bytes > 131072) reject();
|
|
37
|
+
const handle = await open(join(directory, path), constants.O_RDONLY | constants.O_NONBLOCK | (constants.O_NOFOLLOW ?? 0));
|
|
38
|
+
try {
|
|
39
|
+
if (signature(await handle.stat({ bigint: true })) !== entry.signature) changed();
|
|
40
|
+
// 有界读取,即使文件在打开后增长也不会读取无限内容。
|
|
41
|
+
const buffer = Buffer.alloc(131073);
|
|
42
|
+
let bytes = 0;
|
|
43
|
+
while (bytes < buffer.length) {
|
|
44
|
+
const read = await handle.read(buffer, bytes, buffer.length - bytes, null);
|
|
45
|
+
if (!read.bytesRead) break;
|
|
46
|
+
bytes += read.bytesRead;
|
|
47
|
+
}
|
|
48
|
+
if (signature(await handle.stat({ bigint: true })) !== entry.signature) changed();
|
|
49
|
+
if (bytes !== entry.bytes || bytes > 131072) changed();
|
|
50
|
+
const value = buffer.subarray(0, bytes);
|
|
51
|
+
let content;
|
|
52
|
+
try { content = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(value); } catch { reject(); }
|
|
53
|
+
return { bytes, sha256: sha(value), content };
|
|
54
|
+
} finally { await handle.close(); }
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export async function inspectSkillDirectory(artifact, directory) {
|
|
58
|
+
try {
|
|
59
|
+
if (typeof directory !== "string" || !directory || directory.length > 2048 || /[\x00-\x1f\x7f]/.test(directory)) reject();
|
|
60
|
+
const absolute = resolve(directory);
|
|
61
|
+
for (let cursor = absolute;; cursor = dirname(cursor)) {
|
|
62
|
+
const stat = await lstat(cursor);
|
|
63
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) reject();
|
|
64
|
+
if (cursor === dirname(cursor)) break;
|
|
65
|
+
}
|
|
66
|
+
if (await realpath(absolute) !== absolute) reject();
|
|
67
|
+
const before = await inventory(absolute), files = [];
|
|
68
|
+
let total = 0;
|
|
69
|
+
for (const candidate of artifact.files) {
|
|
70
|
+
const entry = before.get(candidate.path);
|
|
71
|
+
const installed = entry ? await ordinaryContent(absolute, candidate.path, entry) : null;
|
|
72
|
+
total += installed?.bytes ?? 0;
|
|
73
|
+
if (total > 524288) reject();
|
|
74
|
+
files.push({ path: candidate.path, status: !installed ? "missing" : installed.sha256 === candidate.sha256 ? "matched" : "changed",
|
|
75
|
+
installed, candidate: { bytes: candidate.bytes, sha256: candidate.sha256 } });
|
|
76
|
+
}
|
|
77
|
+
const prefix = artifact.wrapper === "codex" ? `skills/${artifact.id}/` : "";
|
|
78
|
+
const entry = files.find(file => file.path === `${prefix}SKILL.md`)?.installed?.content;
|
|
79
|
+
const frontmatter = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(entry ?? "")?.[1];
|
|
80
|
+
if (!frontmatter || /^name:\s*([a-z0-9-]+)\s*$/m.exec(frontmatter)?.[1] !== artifact.id) identityMismatch();
|
|
81
|
+
let reportedVersion = null;
|
|
82
|
+
if (artifact.wrapper === "codex") {
|
|
83
|
+
let manifest;
|
|
84
|
+
try { manifest = JSON.parse(files.find(file => file.path === "plugin.json")?.installed?.content ?? ""); } catch { identityMismatch(); }
|
|
85
|
+
if (manifest?.name !== artifact.id) identityMismatch();
|
|
86
|
+
reportedVersion = typeof manifest.version === "string" ? manifest.version : null;
|
|
87
|
+
}
|
|
88
|
+
const declared = new Set(["", ...artifact.files.map(file => file.path)]);
|
|
89
|
+
for (const file of artifact.files) for (let parent = dirname(file.path); parent !== "."; parent = dirname(parent)) declared.add(parent);
|
|
90
|
+
// 额外内容只列路径和类型;不读取可能包含凭据或可执行代码的正文。
|
|
91
|
+
const additional = [...before].filter(([path]) => !declared.has(path)).map(([path, entry]) => ({ path, type: entry.type }));
|
|
92
|
+
const after = await inventory(absolute);
|
|
93
|
+
if (await realpath(absolute) !== absolute || JSON.stringify([...before]) !== JSON.stringify([...after])) changed();
|
|
94
|
+
const actionContracts = inspectSkillContractDeclaration(artifact, files, additional, reportedVersion);
|
|
95
|
+
return { schema_version: 1, source: "explicit-local-directory", path: absolute,
|
|
96
|
+
status: files.every(file => file.status === "matched") && !additional.length ? "matched" : "different",
|
|
97
|
+
reported_version: reportedVersion, files, additional_entries: additional, action_contracts: actionContracts,
|
|
98
|
+
coverage: { declared_file_contents: "complete", additional_entry_contents: "not-read", host_registration: "not-evaluated", current_session_loaded: "unknown" },
|
|
99
|
+
trust: { provenance: "not-evaluated", online_withdrawal: "not-evaluated", installed_action_contracts: actionContracts.status === "declared" ? "self-reported" : "unknown", authorization: "not-evaluated" } };
|
|
100
|
+
} catch (error) {
|
|
101
|
+
if (error instanceof CLIError) throw error;
|
|
102
|
+
// 不把系统错误中的其他本地路径、权限或内容写进机器错误。
|
|
103
|
+
reject("skill_directory_unavailable", "skills.installedDirectoryUnavailable");
|
|
104
|
+
}
|
|
105
|
+
}
|