@zq-silk/yui 2.12.0 → 2.14.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 +5 -0
- package/dist/controller/runtime.js +2 -0
- package/dist/web/assets/client/app/settings.js +4 -0
- package/dist/web/assets/client/i18n/en.js +46 -0
- package/dist/web/assets/client/i18n/zh-CN.js +46 -0
- package/dist/web/assets/client/index.js +2 -0
- package/dist/web/assets/client/views/permissions.js +129 -0
- package/dist/web/assets/shared/manageNav.js +1 -0
- package/dist/web/assets/shell/settingsContent.js +4 -0
- package/dist/web/assets/styles/views/settings.js +13 -0
- package/dist/web/webPermissions.js +97 -0
- package/dist/web/webServer.js +39 -1
- package/docs/authorization.md +35 -0
- package/docs/release-workflow.md +35 -0
- package/docs/release-workflow.zh-CN.md +26 -0
- package/i18n/README.zh-CN.md +4 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -199,6 +199,11 @@ Settings; saving it does not switch the current view or Session.
|
|
|
199
199
|
MCP forms and URL confirmations can be completed in Terminal or declined in
|
|
200
200
|
Conversation. Native additional input is saved as a user Message with its exact
|
|
201
201
|
steer receipt before submission.
|
|
202
|
+
|
|
203
|
+
**Settings → Command permissions** provides a searchable, read-only command ×
|
|
204
|
+
role discovery matrix and built-in capability contracts. Visibility is not
|
|
205
|
+
authorization for a specific call; this view never changes rules or Grants.
|
|
206
|
+
See [discovery and authorization](docs/authorization.md#read-only-web-templates).
|
|
202
207
|
Attachments and native configuration changes
|
|
203
208
|
are not supported by this managed text-input surface.
|
|
204
209
|
The detail has four tabs: Overview (user input, progress and key Decisions),
|
|
@@ -45,6 +45,7 @@ import { archiveOrdinaryTask } from "../task/ordinaryArchive.js";
|
|
|
45
45
|
import { createWebSettings } from "../web/webSettings.js";
|
|
46
46
|
import { createModelManagement } from "../models/modelManagement.js";
|
|
47
47
|
import { createWebSkills } from "../web/webSkills.js";
|
|
48
|
+
import { createWebPermissions } from "../web/webPermissions.js";
|
|
48
49
|
import { createWebJobs } from "../web/webJobs.js";
|
|
49
50
|
import { isTaskOwnedWorkspace, sameManagedWorkspaceIdentity } from "../worktree/managedWorkspace.js";
|
|
50
51
|
import { AgentRuntimeObserver } from "./agentRuntimeObserver.js";
|
|
@@ -417,6 +418,7 @@ export async function startFileTaskControllerRuntime(home, options = {}) {
|
|
|
417
418
|
const web = createControllerWeb(store, {
|
|
418
419
|
previews: () => createTaskPreviews(store, home),
|
|
419
420
|
skills: createWebSkills(store),
|
|
421
|
+
permissions: createWebPermissions(store),
|
|
420
422
|
jobs: createWebJobs(store, home),
|
|
421
423
|
models: createModelManagement(store),
|
|
422
424
|
settings: createWebSettings(store, {
|
|
@@ -9,6 +9,7 @@ import { clearPreference, readSessionAccessMode, writeSessionAccessMode } from "
|
|
|
9
9
|
import { i18n, createManageShell, pageSections, registerManagePage, navigateManage } from "/assets/js/layout/manageShell.js";
|
|
10
10
|
import { mountSkills } from "/assets/js/views/skills.js";
|
|
11
11
|
import { mountJobs } from "/assets/js/views/jobs.js";
|
|
12
|
+
import { mountPermissions } from "/assets/js/views/permissions.js";
|
|
12
13
|
|
|
13
14
|
const $ = selector => document.querySelector(selector);
|
|
14
15
|
const t = i18n.t;
|
|
@@ -20,12 +21,14 @@ function readSection() {
|
|
|
20
21
|
}
|
|
21
22
|
let section = readSection();
|
|
22
23
|
let jobs;
|
|
24
|
+
let permissions;
|
|
23
25
|
const shell = createManageShell({ page: "settings", t, onNavigate: navigate });
|
|
24
26
|
function showSection(next) {
|
|
25
27
|
section = next;
|
|
26
28
|
document.querySelectorAll("#settings-root > .manage-section[data-section]").forEach(panel => { panel.hidden = panel.dataset.section !== next; });
|
|
27
29
|
shell.set(next);
|
|
28
30
|
if (next === "jobs" && !jobs) jobs = mountJobs($("#global-jobs"), t);
|
|
31
|
+
if (next === "permissions" && !permissions) permissions = mountPermissions($("#permission-browser"), t);
|
|
29
32
|
}
|
|
30
33
|
function navigate(next) {
|
|
31
34
|
navigateManage({ page: "settings", section: next });
|
|
@@ -39,6 +42,7 @@ registerManagePage("settings", { show: next => showSection(next.section) });
|
|
|
39
42
|
i18n.subscribe(() => shell.render());
|
|
40
43
|
i18n.subscribe(() => {
|
|
41
44
|
if (jobs && !jobs.isDirty() && !jobs.isBusy()) jobs = mountJobs($("#global-jobs"), t);
|
|
45
|
+
permissions?.translate();
|
|
42
46
|
});
|
|
43
47
|
showSection(section);
|
|
44
48
|
const skills = mountSkills($("#skill-browser"), t, { heading: false, onDirty: value => markDirty("skills", value) });
|
|
@@ -3,6 +3,52 @@
|
|
|
3
3
|
// domain vocabulary (statuses, owners, actions) keeps the exact product terms.
|
|
4
4
|
// Placeholders such as {count} are filled by fill() in lib/format.
|
|
5
5
|
export const EN = {
|
|
6
|
+
"settings.section.permissions": "Command permissions",
|
|
7
|
+
"permissions.intro": "Read-only discovery templates, not authorization for a specific call. No rules or Grants are changed.",
|
|
8
|
+
"permissions.notice": "Compare CLI discovery by role. Visible means included in that role's default help/completion, not permission to execute. Choosing a role does not switch your identity.",
|
|
9
|
+
"permissions.commands": "Command × role matrix",
|
|
10
|
+
"permissions.capabilities": "Built-in capability contracts",
|
|
11
|
+
"permissions.view": "View",
|
|
12
|
+
"permissions.search": "Find a command or capability",
|
|
13
|
+
"permissions.role": "Role template",
|
|
14
|
+
"permissions.category": "Command category",
|
|
15
|
+
"permissions.allRoles": "Compare all role templates",
|
|
16
|
+
"permissions.allCategories": "All categories",
|
|
17
|
+
"permissions.configured": "Configured GlobalRole",
|
|
18
|
+
"permissions.role.public": "Terminal / unmanaged",
|
|
19
|
+
"permissions.role.operator": "Operator",
|
|
20
|
+
"permissions.role.global": "GlobalRole",
|
|
21
|
+
"permissions.role.leader": "Task Leader",
|
|
22
|
+
"permissions.role.worker": "Task Worker",
|
|
23
|
+
"permissions.role.reviewer": "Task Reviewer",
|
|
24
|
+
"permissions.role.unbound": "Unbound identity",
|
|
25
|
+
"permissions.command": "Command",
|
|
26
|
+
"permissions.visible": "Visible",
|
|
27
|
+
"permissions.hidden": "Hidden",
|
|
28
|
+
"permissions.tableHint": "Discovery only · select a command for role-specific usage and boundaries. Scroll horizontally to compare roles.",
|
|
29
|
+
"permissions.reason.visible": "Included by the current catalog projection. Options below are filtered by the same role audience as CLI help.",
|
|
30
|
+
"permissions.reason.hidden": "Not discoverable as an executable command in this audience's catalog. This is not a missing-Grant assessment.",
|
|
31
|
+
"permissions.unevaluated": "No specific authorization assessment performed. No target, live Session or Grant has been inspected.",
|
|
32
|
+
"permissions.boundaries": "Execution still checks the actual Session, Task/lifecycle, planning or delivery authority, Assignment/Review, Project/workspace and resource/Grant bounds as applicable. The catalog cannot enumerate every domain check; never infer authorization from visibility, command name or a query effect.",
|
|
33
|
+
"permissions.boundary.public": "Ordinary terminals and unmanaged sessions share the public surface. This does not grant a managed identity or authorize unrequested effects.",
|
|
34
|
+
"permissions.boundary.operator": "Requires the current Operator binding and the domain's operation-specific checks. An offline template cannot establish that binding.",
|
|
35
|
+
"permissions.boundary.global": "Configured GlobalRoles share this discovery audience, not Operator authority. Role configuration and exact domain scope remain authoritative.",
|
|
36
|
+
"permissions.boundary.leader": "Own Task and captured Project scope. Planning Sessions do not acquire delivery authority just because a Task becomes active.",
|
|
37
|
+
"permissions.boundary.assignment": "Worker and Reviewer share discovery templates, but their exact Assignment/Review, frozen references and writable workspace may differ.",
|
|
38
|
+
"permissions.boundary.unbound": "Incomplete/conflicting managed identity has no public fallback. Historical or revoked Sessions are rechecked at execution even if offline help is visible.",
|
|
39
|
+
"permissions.capabilityNotice": "Built-in descriptors from the existing registry definition, not a second CLI-to-capability map. Permission categories and required inputs are contracts, not role grants. Task-local plugin descriptors and live assessments are intentionally excluded.",
|
|
40
|
+
"permissions.effect": "Declared effect",
|
|
41
|
+
"permissions.requiredPermissions": "Required permission categories",
|
|
42
|
+
"permissions.requiredInputs": "Required target inputs",
|
|
43
|
+
"permissions.inputs": "All input fields",
|
|
44
|
+
"permissions.legend": "Meaning, limits and sources",
|
|
45
|
+
"permissions.states": "Exact capability discovery may return hidden, requestable (with a bounded request route), or authorized (rechecked at execution). Without exact input there is no access assessment; this page never labels a role/command pair authorized or requestable.",
|
|
46
|
+
"permissions.sources": "Sources",
|
|
47
|
+
"permissions.previous": "Previous",
|
|
48
|
+
"permissions.next": "Next",
|
|
49
|
+
"permissions.loading": "Loading templates…",
|
|
50
|
+
"permissions.empty": "No matching templates.",
|
|
51
|
+
"permissions.retry": "Retry read",
|
|
6
52
|
"tabs.jobs": "Jobs",
|
|
7
53
|
"settings.section.jobs": "Jobs & schedules",
|
|
8
54
|
"jobs.builtinSync": "Built-in Project sync · safe fast-forward only",
|
|
@@ -1,6 +1,52 @@
|
|
|
1
1
|
// Simplified Chinese UI copy. Typed against the English catalog, so a key that
|
|
2
2
|
// does not exist in en.ts is a compile error; a missing key falls back to en.
|
|
3
3
|
export const ZH_CN = {
|
|
4
|
+
"settings.section.permissions": "命令权限",
|
|
5
|
+
"permissions.intro": "只读的命令发现模板,不是具体调用的授权结论。不会修改规则或 Grant。",
|
|
6
|
+
"permissions.notice": "按角色比较 CLI 发现范围。“可见”仅表示默认帮助与补全包含该命令,不代表可以执行。选择角色不会切换你的身份。",
|
|
7
|
+
"permissions.commands": "命令 × 角色矩阵",
|
|
8
|
+
"permissions.capabilities": "内置能力契约",
|
|
9
|
+
"permissions.view": "视图",
|
|
10
|
+
"permissions.search": "查找命令或能力",
|
|
11
|
+
"permissions.role": "角色模板",
|
|
12
|
+
"permissions.category": "命令分类",
|
|
13
|
+
"permissions.allRoles": "比较全部角色模板",
|
|
14
|
+
"permissions.allCategories": "全部分类",
|
|
15
|
+
"permissions.configured": "已配置 GlobalRole",
|
|
16
|
+
"permissions.role.public": "终端 / 非管控会话",
|
|
17
|
+
"permissions.role.operator": "Operator",
|
|
18
|
+
"permissions.role.global": "GlobalRole",
|
|
19
|
+
"permissions.role.leader": "Task Leader",
|
|
20
|
+
"permissions.role.worker": "Task Worker",
|
|
21
|
+
"permissions.role.reviewer": "Task Reviewer",
|
|
22
|
+
"permissions.role.unbound": "未绑定身份",
|
|
23
|
+
"permissions.command": "命令",
|
|
24
|
+
"permissions.visible": "发现可见",
|
|
25
|
+
"permissions.hidden": "隐藏",
|
|
26
|
+
"permissions.tableHint": "仅展示发现范围 · 选择命令查看各角色用法与边界;可横向滚动比较角色。",
|
|
27
|
+
"permissions.reason.visible": "当前目录投影包含该命令。下方选项与 CLI 帮助使用相同的角色过滤规则。",
|
|
28
|
+
"permissions.reason.hidden": "该角色的目录中不展示此可执行命令。这不表示仅缺少一个 Grant。",
|
|
29
|
+
"permissions.unevaluated": "尚未进行具体授权评估;未检查目标、实时 Session 或 Grant。",
|
|
30
|
+
"permissions.boundaries": "执行仍按实际操作检查 Session、Task/生命周期、planning 或 delivery 权限、Assignment/Review、Project/工作区及资源/Grant 边界。目录不能枚举全部领域检查;不可根据可见性、命令名称或 query 效果推断已授权。",
|
|
31
|
+
"permissions.boundary.public": "普通终端与非 Yui 管控会话共用公共入口;这不赋予管控身份,也不授权未被请求的操作。",
|
|
32
|
+
"permissions.boundary.operator": "需要当前 Operator 绑定及领域操作检查。离线模板不能证明该绑定有效。",
|
|
33
|
+
"permissions.boundary.global": "已配置 GlobalRole 共用此发现范围,但不继承 Operator 权限;角色配置和具体领域范围仍是权威来源。",
|
|
34
|
+
"permissions.boundary.leader": "限自身 Task 及已捕获的 Project 范围。Task 激活不会将 planning Session 自动提升为 delivery。",
|
|
35
|
+
"permissions.boundary.assignment": "Worker 与 Reviewer 共用发现模板,但各自 Assignment/Review、冻结引用和可写工作区可能不同。",
|
|
36
|
+
"permissions.boundary.unbound": "不完整或冲突的管控身份不能回退为公共身份。历史或已撤销 Session 即使能看到离线帮助,执行时仍会被重新检查。",
|
|
37
|
+
"permissions.capabilityNotice": "直接展示现有 registry 的内置描述,不维护另一份 CLI 与能力映射。权限类别和必填输入是契约,不是角色授权;不展示 Task 私有插件描述或实时评估。",
|
|
38
|
+
"permissions.effect": "声明效果",
|
|
39
|
+
"permissions.requiredPermissions": "所需权限类别",
|
|
40
|
+
"permissions.requiredInputs": "必填目标输入",
|
|
41
|
+
"permissions.inputs": "全部输入字段",
|
|
42
|
+
"permissions.legend": "含义、限制与来源",
|
|
43
|
+
"permissions.states": "具体能力发现可能返回 hidden(隐藏)、requestable(可经限定渠道申请)或 authorized(已授权,执行时仍复核)。缺少具体输入时不提供 access 结论;此页不会将角色与命令组合标为已授权或可申请。",
|
|
44
|
+
"permissions.sources": "来源",
|
|
45
|
+
"permissions.previous": "上一页",
|
|
46
|
+
"permissions.next": "下一页",
|
|
47
|
+
"permissions.loading": "正在读取模板…",
|
|
48
|
+
"permissions.empty": "没有匹配的模板。",
|
|
49
|
+
"permissions.retry": "重新读取",
|
|
4
50
|
"tabs.jobs": "Job",
|
|
5
51
|
"settings.section.jobs": "Job 与时间计划",
|
|
6
52
|
"jobs.builtinSync": "内置 Project 同步 · 仅安全快进",
|
|
@@ -61,6 +61,7 @@ import { TASK_VIEW_SCRIPT } from "./app/taskView.js";
|
|
|
61
61
|
import { SETTINGS_SCRIPT } from "./app/settings.js";
|
|
62
62
|
import { SKILLS_SCRIPT } from "./views/skills.js";
|
|
63
63
|
import { JOBS_SCRIPT } from "./views/jobs.js";
|
|
64
|
+
import { PERMISSIONS_SCRIPT } from "./views/permissions.js";
|
|
64
65
|
import { MODELS_VIEW_SCRIPT } from "./views/models.js";
|
|
65
66
|
import { MODELS_SCRIPT } from "./app/models.js";
|
|
66
67
|
import { MANAGE_SHELL_SCRIPT } from "./layout/manageShell.js";
|
|
@@ -69,6 +70,7 @@ export { APP_SCRIPT } from "./app/main.js";
|
|
|
69
70
|
export const CLIENT_MODULES = Object.freeze({
|
|
70
71
|
"views/skills": SKILLS_SCRIPT,
|
|
71
72
|
"views/jobs": JOBS_SCRIPT,
|
|
73
|
+
"views/permissions": PERMISSIONS_SCRIPT,
|
|
72
74
|
"views/models": MODELS_VIEW_SCRIPT,
|
|
73
75
|
"lib/dom": DOM_SCRIPT,
|
|
74
76
|
"lib/format": FORMAT_SCRIPT,
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
export const PERMISSIONS_SCRIPT = String.raw `
|
|
2
|
+
import { h } from "/assets/js/lib/dom.js";
|
|
3
|
+
import { requestJson } from "/assets/js/lib/api.js";
|
|
4
|
+
|
|
5
|
+
export function mountPermissions(root, t) {
|
|
6
|
+
let meta, generation = 0, timer;
|
|
7
|
+
let mode = "commands", role = "", category = "", q = "", cursor = "0", previous = [];
|
|
8
|
+
const label = r => r.name ? t("permissions.configured") + ": " + r.name : t("permissions.role." + r.id);
|
|
9
|
+
let status, results, prev, next, search, roleSelect, categorySelect;
|
|
10
|
+
function reset() { cursor = "0"; previous = []; load(); }
|
|
11
|
+
function controls() {
|
|
12
|
+
const modeSelect = h("select", { onchange: () => {
|
|
13
|
+
mode = modeSelect.value; roleSelect.disabled = categorySelect.disabled = mode !== "commands"; reset();
|
|
14
|
+
} }, ["commands", "capabilities"].map(id => h("option", { value: id }, t("permissions." + id))));
|
|
15
|
+
modeSelect.value = mode;
|
|
16
|
+
search = h("input", { type: "search", value: q, maxlength: 200, oninput: () => {
|
|
17
|
+
q = search.value; clearTimeout(timer);
|
|
18
|
+
// Invalidate any older result immediately, even during debounce.
|
|
19
|
+
generation++; prev.disabled = next.disabled = true;
|
|
20
|
+
timer = setTimeout(reset, 180);
|
|
21
|
+
} });
|
|
22
|
+
roleSelect = h("select", { onchange: () => { role = roleSelect.value; reset(); } },
|
|
23
|
+
h("option", { value: "" }, t("permissions.allRoles")),
|
|
24
|
+
meta.roles.map(r => h("option", { value: r.id }, label(r))));
|
|
25
|
+
roleSelect.value = role;
|
|
26
|
+
categorySelect = h("select", { onchange: () => { category = categorySelect.value; reset(); } },
|
|
27
|
+
h("option", { value: "" }, t("permissions.allCategories")),
|
|
28
|
+
meta.categories.map(c => h("option", { value: c }, c)));
|
|
29
|
+
categorySelect.value = category;
|
|
30
|
+
roleSelect.disabled = categorySelect.disabled = mode !== "commands";
|
|
31
|
+
status = h("p", { role: "status", "aria-live": "polite" });
|
|
32
|
+
results = h("div.permissions-results");
|
|
33
|
+
prev = h("button.btn", { type: "button", disabled: true, onclick: () => { cursor = previous.pop(); load(); } }, t("permissions.previous"));
|
|
34
|
+
next = h("button.btn", { type: "button", disabled: true }, t("permissions.next"));
|
|
35
|
+
const field = (key, input) => h("label.field", null, h("span", null, t("permissions." + key)), input);
|
|
36
|
+
root.replaceChildren(
|
|
37
|
+
h("div.card.settings-card.settings-stack", null,
|
|
38
|
+
h("p", null, t("permissions.notice")),
|
|
39
|
+
h("div.settings-grid", null, field("view", modeSelect), field("search", search),
|
|
40
|
+
field("role", roleSelect), field("category", categorySelect))),
|
|
41
|
+
status, results, h("div.settings-actions", null, prev, next),
|
|
42
|
+
h("details.settings-card", null, h("summary", null, t("permissions.legend")),
|
|
43
|
+
h("p", null, t("permissions.states")),
|
|
44
|
+
h("p", null, t("permissions.boundaries")),
|
|
45
|
+
h("p", null, t("permissions.sources") + ": " + meta.sources.join(" · "))));
|
|
46
|
+
}
|
|
47
|
+
function detailContent(data) {
|
|
48
|
+
return h("div.settings-stack", null,
|
|
49
|
+
h("p", null, t("permissions.unevaluated")),
|
|
50
|
+
h("p", null, t("permissions.boundaries")),
|
|
51
|
+
data.roles.map(r => h("div", null,
|
|
52
|
+
h("h3", null, label(meta.roles.find(item => item.id === r.role))),
|
|
53
|
+
h("p", null, t("permissions.reason." + (r.visible ? "visible" : "hidden"))),
|
|
54
|
+
h("p", null, t("permissions.boundary." + r.audience)),
|
|
55
|
+
r.usage.length ? h("pre", null, r.usage.join("\n")) : null)),
|
|
56
|
+
h("small", null, t("permissions.sources") + ": " + data.source));
|
|
57
|
+
}
|
|
58
|
+
function commandTable(page, version) {
|
|
59
|
+
const shown = role ? meta.roles.filter(r => r.id === role) : meta.roles.filter(r => !r.name);
|
|
60
|
+
const body = h("tbody");
|
|
61
|
+
page.items.forEach((item, index) => {
|
|
62
|
+
const detail = h("td", { colSpan: shown.length + 1 });
|
|
63
|
+
const detailRow = h("tr", { hidden: true, id: "permission-detail-" + version + "-" + index }, detail);
|
|
64
|
+
let loaded = false;
|
|
65
|
+
const button = h("button.permissions-command", { type: "button", "aria-expanded": "false",
|
|
66
|
+
"aria-controls": detailRow.id, onclick: async () => {
|
|
67
|
+
detailRow.hidden = !detailRow.hidden;
|
|
68
|
+
button.setAttribute("aria-expanded", String(!detailRow.hidden));
|
|
69
|
+
if (detailRow.hidden || loaded) return;
|
|
70
|
+
loaded = true; detail.textContent = t("permissions.loading");
|
|
71
|
+
try {
|
|
72
|
+
const params = new URLSearchParams({ id: item.id, ...(role ? { role } : {}) });
|
|
73
|
+
const data = await requestJson("/api/permissions/command?" + params);
|
|
74
|
+
if (version !== generation) return;
|
|
75
|
+
detail.replaceChildren(detailContent(data));
|
|
76
|
+
} catch (error) { loaded = false; detail.textContent = error.message; }
|
|
77
|
+
} }, item.id);
|
|
78
|
+
body.append(h("tr", null, h("th", { scope: "row" }, button, h("small", null, item.summary)),
|
|
79
|
+
item.roles.map(r => h("td", null, h("span", { class: r.visible ? "permissions-visible" : "permissions-hidden" },
|
|
80
|
+
t("permissions." + (r.visible ? "visible" : "hidden")))))), detailRow);
|
|
81
|
+
});
|
|
82
|
+
return h("div.permissions-scroll", { tabIndex: 0, role: "region", "aria-label": t("permissions.commands") },
|
|
83
|
+
h("table.permissions-table", null,
|
|
84
|
+
h("caption", null, t("permissions.tableHint")),
|
|
85
|
+
h("thead", null, h("tr", null, h("th", { scope: "col" }, t("permissions.command")),
|
|
86
|
+
shown.map(r => h("th", { scope: "col" }, label(r))))), body));
|
|
87
|
+
}
|
|
88
|
+
async function load() {
|
|
89
|
+
clearTimeout(timer);
|
|
90
|
+
const version = ++generation;
|
|
91
|
+
status.textContent = t("permissions.loading");
|
|
92
|
+
results.replaceChildren(); prev.disabled = next.disabled = true;
|
|
93
|
+
const params = new URLSearchParams({ q, cursor, ...(mode === "commands" ? { role, category } : {}) });
|
|
94
|
+
try {
|
|
95
|
+
const page = await requestJson("/api/permissions/" + mode + "?" + params);
|
|
96
|
+
if (version !== generation) return;
|
|
97
|
+
status.textContent = page.items.length ? (Number(cursor) + 1) + "–" + (Number(cursor) + page.items.length) + " / " + page.total : t("permissions.empty");
|
|
98
|
+
if (mode === "commands") results.append(commandTable(page, version));
|
|
99
|
+
else results.append(h("p", null, t("permissions.capabilityNotice")), ...page.items.map(item =>
|
|
100
|
+
h("details.settings-card", null, h("summary", null, item.name),
|
|
101
|
+
h("p", null, item.summary), h("p", null, t("permissions.unevaluated")),
|
|
102
|
+
h("p", null, t("permissions.effect") + ": " + item.effect),
|
|
103
|
+
h("p", null, t("permissions.requiredPermissions") + ": " + (item.requiredPermissions.join(", ") || "—")),
|
|
104
|
+
h("p", null, t("permissions.requiredInputs") + ": " + (item.requiredInputs.join(", ") || "—")),
|
|
105
|
+
h("p", null, t("permissions.inputs") + ": " + (item.inputs.join(", ") || "—")),
|
|
106
|
+
h("small", null, t("permissions.sources") + ": " + page.source + " → " + item.source))));
|
|
107
|
+
prev.disabled = previous.length === 0;
|
|
108
|
+
next.disabled = page.nextCursor === null;
|
|
109
|
+
next.onclick = () => { previous.push(cursor); cursor = page.nextCursor; load(); };
|
|
110
|
+
} catch (error) {
|
|
111
|
+
if (version !== generation) return;
|
|
112
|
+
status.textContent = error.message;
|
|
113
|
+
results.append(h("button.btn", { type: "button", onclick: load }, t("permissions.retry")));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async function init() {
|
|
117
|
+
root.replaceChildren(h("p", { role: "status" }, t("permissions.loading")));
|
|
118
|
+
try {
|
|
119
|
+
meta = await requestJson("/api/permissions");
|
|
120
|
+
controls(); load();
|
|
121
|
+
} catch (error) {
|
|
122
|
+
root.replaceChildren(h("p", { role: "status" }, error.message),
|
|
123
|
+
h("button.btn", { type: "button", onclick: init }, t("permissions.retry")));
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
init();
|
|
127
|
+
return { translate() { if (meta) { controls(); load(); } } };
|
|
128
|
+
}
|
|
129
|
+
`;
|
|
@@ -8,6 +8,7 @@ export const MANAGE_SECTIONS = Object.freeze({
|
|
|
8
8
|
{ page: "settings", section: "preferences", key: "settings.section.preferences", label: "Preferences" },
|
|
9
9
|
{ page: "settings", section: "skills", key: "settings.section.skills", label: "Skills" },
|
|
10
10
|
{ page: "settings", section: "jobs", key: "settings.section.jobs", label: "Jobs & schedules" },
|
|
11
|
+
{ page: "settings", section: "permissions", key: "settings.section.permissions", label: "Command permissions" },
|
|
11
12
|
{ page: "settings", section: "server", key: "settings.section.server", label: "Agents & server" },
|
|
12
13
|
{ page: "models", section: "connection", key: "models.section.connection", label: "Model connection" }
|
|
13
14
|
]
|
|
@@ -30,4 +30,8 @@ export const SETTINGS_MAIN = `
|
|
|
30
30
|
<label class="field settings-search"><span data-i18n="settings.search">Find a setting</span><input id="settings-search" type="search" autocomplete="off"></label>
|
|
31
31
|
<p id="settings-status" role="status"></p>
|
|
32
32
|
<div id="settings-groups" class="settings-stack"></div>
|
|
33
|
+
</section>
|
|
34
|
+
<section class="manage-section" data-section="permissions" aria-labelledby="settings-permissions-title" hidden>
|
|
35
|
+
${manageHead("settings-permissions-title", "settings.section.permissions", "Command permissions", "permissions.intro", "Read-only discovery templates, not authorization for a specific call. No rules or Grants are changed.")}
|
|
36
|
+
<section id="permission-browser" class="settings-stack"></section>
|
|
33
37
|
</section>`;
|
|
@@ -33,6 +33,19 @@ export const SETTINGS_STYLES = `
|
|
|
33
33
|
.settings-search input{width:100%;max-width:520px}
|
|
34
34
|
#settings-status:empty,#browser-receipt:empty{display:none}
|
|
35
35
|
.mono-line{overflow-wrap:anywhere}
|
|
36
|
+
.permissions-results{display:grid;gap:12px;min-width:0}
|
|
37
|
+
.permissions-scroll{overflow:auto;border:1px solid var(--line);border-radius:var(--r-lg);background:var(--surface)}
|
|
38
|
+
.permissions-table{border-collapse:collapse;width:100%;font-size:var(--fs-xs)}
|
|
39
|
+
.permissions-table caption{text-align:left;padding:14px;color:var(--ink-2)}
|
|
40
|
+
.permissions-table th,.permissions-table td{text-align:left;vertical-align:top;padding:12px;border-bottom:1px solid var(--line)}
|
|
41
|
+
.permissions-table thead th{background:var(--surface-2);min-width:100px;font-weight:600}
|
|
42
|
+
.permissions-table th[scope=row]{min-width:240px;max-width:360px;font-weight:400}
|
|
43
|
+
.permissions-table small{display:block;color:var(--ink-3);margin-top:6px;overflow-wrap:anywhere}
|
|
44
|
+
.permissions-command{font-family:var(--font-mono);color:var(--accent);background:none;border:0;padding:4px 0;cursor:pointer;text-align:left;overflow-wrap:anywhere}
|
|
45
|
+
.permissions-command:hover{text-decoration:underline}
|
|
46
|
+
.permissions-table pre{white-space:pre-wrap;overflow-wrap:anywhere;font-size:var(--fs-xs);max-width:700px}
|
|
47
|
+
.permissions-visible{color:var(--ink);white-space:nowrap}
|
|
48
|
+
.permissions-hidden{color:var(--ink-3);white-space:nowrap}
|
|
36
49
|
@media(max-width:${BREAKPOINTS.small}px){
|
|
37
50
|
.settings-grid{grid-template-columns:minmax(0,1fr)}
|
|
38
51
|
.settings-card{padding:14px}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { ROOT_COMMAND } from "../cli/commandCatalog.js";
|
|
2
|
+
import { discoveryCommandTree } from "../cli/commandDiscovery.js";
|
|
3
|
+
import { BUILTIN_CAPABILITIES } from "../kernel/builtinCapabilities.js";
|
|
4
|
+
const templates = [
|
|
5
|
+
{ id: "public", audience: "public" },
|
|
6
|
+
{ id: "operator", audience: "operator" },
|
|
7
|
+
{ id: "global", audience: "global" },
|
|
8
|
+
{ id: "leader", audience: "leader" },
|
|
9
|
+
{ id: "worker", audience: "assignment" },
|
|
10
|
+
{ id: "reviewer", audience: "assignment" },
|
|
11
|
+
{ id: "unbound", audience: "unbound" }
|
|
12
|
+
];
|
|
13
|
+
function executableNodes(node) {
|
|
14
|
+
return [...(node.kind !== "group" && node.discovery?.surface !== "runtime" ? [node] : []),
|
|
15
|
+
...node.children.flatMap(executableNodes)];
|
|
16
|
+
}
|
|
17
|
+
const commands = executableNodes(ROOT_COMMAND);
|
|
18
|
+
// The same pure projection as CLI help/completion. No fake Session, caller
|
|
19
|
+
// environment, authority issuance, registry invocation or domain assessment.
|
|
20
|
+
const projections = new Map(templates.map(role => [role.audience,
|
|
21
|
+
new Map(executableNodes(discoveryCommandTree({}, role.audience)).map(node => [node.path.join(" "), node]))]));
|
|
22
|
+
const pageSize = 30;
|
|
23
|
+
function searchText(value = "") {
|
|
24
|
+
if (value.length > 200)
|
|
25
|
+
throw new Error("Search is limited to 200 characters.");
|
|
26
|
+
return value.trim().toLowerCase();
|
|
27
|
+
}
|
|
28
|
+
function page(items, cursor = "0") {
|
|
29
|
+
if (!/^(0|[1-9]\d*)$/.test(cursor) || !Number.isSafeInteger(Number(cursor)))
|
|
30
|
+
throw new Error("Invalid cursor.");
|
|
31
|
+
const start = Number(cursor);
|
|
32
|
+
return { items: items.slice(start, start + pageSize), total: items.length,
|
|
33
|
+
nextCursor: start + pageSize < items.length ? String(start + pageSize) : null };
|
|
34
|
+
}
|
|
35
|
+
/** Read-only metadata projection for the authenticated local Web user.
|
|
36
|
+
* Only configured GlobalRole names cross the Store boundary. Task contents,
|
|
37
|
+
* bindings, profiles, Sessions, Grants and plugin descriptors are not read.
|
|
38
|
+
* These templates deliberately have no AccessAssessment field. */
|
|
39
|
+
export function createWebPermissions(store) {
|
|
40
|
+
const roles = () => [...templates, ...store.listGlobalRoles()
|
|
41
|
+
.filter(role => role.name !== "operator").map(role => ({
|
|
42
|
+
id: `global:${role.name}`, name: role.name, audience: "global"
|
|
43
|
+
}))];
|
|
44
|
+
const selected = (id) => {
|
|
45
|
+
if (!id)
|
|
46
|
+
return templates;
|
|
47
|
+
const role = roles().find(role => role.id === id);
|
|
48
|
+
if (!role)
|
|
49
|
+
throw new Error("Unknown role template.");
|
|
50
|
+
return [role];
|
|
51
|
+
};
|
|
52
|
+
const cells = (id, selectedRoles) => selectedRoles.map(role => {
|
|
53
|
+
const projected = projections.get(role.audience).get(id);
|
|
54
|
+
return { role: role.id, visible: !!projected };
|
|
55
|
+
});
|
|
56
|
+
return {
|
|
57
|
+
index() {
|
|
58
|
+
return { roles: roles(), categories: [...new Set(commands.map(node => node.path[1]))].sort(),
|
|
59
|
+
sources: ["cli/commandCatalog.ts", "cli/commandDiscovery.ts", "kernel/builtinCapabilities.ts", "docs/authorization.md"],
|
|
60
|
+
evaluation: "not-performed" };
|
|
61
|
+
},
|
|
62
|
+
commands(query = {}) {
|
|
63
|
+
const q = searchText(query.q);
|
|
64
|
+
const selectedRoles = selected(query.role);
|
|
65
|
+
return { ...page(commands.filter(node => (!query.category || node.path[1] === query.category)
|
|
66
|
+
&& `${node.path.join(" ")} ${node.summary}`.toLowerCase().includes(q))
|
|
67
|
+
.map(node => ({ id: node.path.join(" "), summary: node.summary, category: node.path[1],
|
|
68
|
+
roles: cells(node.path.join(" "), selectedRoles) })), query.cursor),
|
|
69
|
+
evaluation: "not-performed" };
|
|
70
|
+
},
|
|
71
|
+
command(id, role) {
|
|
72
|
+
const node = commands.find(node => node.path.join(" ") === id);
|
|
73
|
+
if (!node)
|
|
74
|
+
throw new Error("Unknown command template.");
|
|
75
|
+
return { id, summary: node.summary, evaluation: "not-performed",
|
|
76
|
+
source: "cli/commandCatalog.ts → cli/commandDiscovery.ts",
|
|
77
|
+
roles: selected(role).map(role => {
|
|
78
|
+
const projected = projections.get(role.audience).get(id);
|
|
79
|
+
return { role: role.id, audience: role.audience, visible: !!projected,
|
|
80
|
+
// Use the projected usage/options, including option audience gates.
|
|
81
|
+
usage: projected?.usage ?? [], options: projected?.options ?? [] };
|
|
82
|
+
}) };
|
|
83
|
+
},
|
|
84
|
+
capabilities(query = {}) {
|
|
85
|
+
const q = searchText(query.q);
|
|
86
|
+
return { ...page(BUILTIN_CAPABILITIES
|
|
87
|
+
.filter(entry => `${entry.name} ${entry.summary} ${entry.requiredPermissions.join(" ")}`.toLowerCase().includes(q))
|
|
88
|
+
.map(entry => ({
|
|
89
|
+
name: entry.name, summary: entry.summary, effect: entry.effect, source: entry.source,
|
|
90
|
+
requiredPermissions: entry.requiredPermissions,
|
|
91
|
+
requiredInputs: entry.inputSchema.required ?? [],
|
|
92
|
+
inputs: Object.keys(entry.inputSchema.properties ?? {})
|
|
93
|
+
})), query.cursor), evaluation: "not-performed",
|
|
94
|
+
source: "kernel/builtinCapabilities.ts (registry built-in descriptors)" };
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
}
|
package/dist/web/webServer.js
CHANGED
|
@@ -126,6 +126,44 @@ async function handleHttpRequest(request, response, store, dependencies, token,
|
|
|
126
126
|
sendJson(response, 403, { error: "Invalid Yui web token.", disposition: "not-submitted" }, method === "HEAD");
|
|
127
127
|
return;
|
|
128
128
|
}
|
|
129
|
+
if (pathname === "/api/permissions" || pathname.startsWith("/api/permissions/")) {
|
|
130
|
+
if (method !== "GET") {
|
|
131
|
+
sendJson(response, 405, { error: "Permission templates are read-only.", disposition: "not-submitted" }, method === "HEAD");
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const allowed = {
|
|
135
|
+
"/api/permissions": [],
|
|
136
|
+
"/api/permissions/commands": ["q", "role", "category", "cursor"],
|
|
137
|
+
"/api/permissions/command": ["id", "role"],
|
|
138
|
+
"/api/permissions/capabilities": ["q", "cursor"]
|
|
139
|
+
};
|
|
140
|
+
if (!Object.hasOwn(allowed, pathname)) {
|
|
141
|
+
sendJson(response, 404, { error: "Unknown permission template route." }, false);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
if (!dependencies.permissions) {
|
|
145
|
+
sendJson(response, 503, { error: "Permission templates unavailable." }, false);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
try {
|
|
149
|
+
const query = new URL(request.url, "http://localhost").searchParams;
|
|
150
|
+
if ([...query.keys()].some(key => !allowed[pathname].includes(key) || query.getAll(key).length !== 1)) {
|
|
151
|
+
throw new Error("Unknown or repeated template query.");
|
|
152
|
+
}
|
|
153
|
+
const params = Object.fromEntries(query);
|
|
154
|
+
const service = dependencies.permissions;
|
|
155
|
+
const value = pathname === "/api/permissions" ? service.index()
|
|
156
|
+
: pathname.endsWith("/commands") ? service.commands(params)
|
|
157
|
+
: pathname.endsWith("/command") ? service.command(params.id ?? "", params.role)
|
|
158
|
+
: service.capabilities(params);
|
|
159
|
+
sendJson(response, 200, value, false);
|
|
160
|
+
}
|
|
161
|
+
catch (error) {
|
|
162
|
+
sendJson(response, 400, { error: error instanceof Error ? error.message : "Invalid template query.",
|
|
163
|
+
disposition: "not-submitted" }, false);
|
|
164
|
+
}
|
|
165
|
+
return;
|
|
166
|
+
}
|
|
129
167
|
if (pathname.startsWith("/preview/") && dependencies.previews) {
|
|
130
168
|
await dependencies.previews.serve(request, response);
|
|
131
169
|
return;
|
|
@@ -632,7 +670,7 @@ async function handleHttpRequest(request, response, store, dependencies, token,
|
|
|
632
670
|
try {
|
|
633
671
|
const assetVersion = new URL(request.url, "http://localhost").searchParams.get("v");
|
|
634
672
|
const asset = findWebAsset(pathname, assetVersion);
|
|
635
|
-
const managementPage = /^\/(?:models(?:\/(?:providers|catalog|quota))?|settings(?:\/(?:preferences|skills|jobs|server|connection))?)\/?$/.test(pathname);
|
|
673
|
+
const managementPage = /^\/(?:models(?:\/(?:providers|catalog|quota))?|settings(?:\/(?:preferences|skills|jobs|permissions|server|connection))?)\/?$/.test(pathname);
|
|
636
674
|
if (pathname === "/" || pathname === "/index.html" || managementPage) {
|
|
637
675
|
sendText(response, 200, "text/html; charset=utf-8", prepareWebDocument(managementPage ? MODELS_HTML.replace("__YUI_WEB_TOKEN__", token) : dashboardHtml(token), managementPage ? "/assets/js/app/manage.js" : "/assets/app.js"), method === "HEAD");
|
|
638
676
|
}
|
package/docs/authorization.md
CHANGED
|
@@ -111,6 +111,41 @@ remains a limited template, while execution reports the original storage/identit
|
|
|
111
111
|
failure. Complete launch hints can still show a role's static help on a broken
|
|
112
112
|
Home; they cannot authorize reads or effects.
|
|
113
113
|
|
|
114
|
+
## Read-only Web templates
|
|
115
|
+
|
|
116
|
+
Open **Settings → Command permissions** (`/settings/permissions`) to compare
|
|
117
|
+
CLI discovery for ordinary terminals/unmanaged sessions, Operator, GlobalRole,
|
|
118
|
+
Task Leader, Worker and Reviewer. Configured GlobalRole names are selectable;
|
|
119
|
+
they use the existing global discovery audience, not a new role impersonation
|
|
120
|
+
path. Search, category/role filters and 30-row pages keep the view bounded.
|
|
121
|
+
Expand a command for its audience-filtered usage and scope explanation.
|
|
122
|
+
|
|
123
|
+
“Visible” means the existing `discoveryCommandTree` retains the executable
|
|
124
|
+
command; “Hidden” means it does not. Neither is an exact authorization
|
|
125
|
+
assessment. Worker and Reviewer can share a template while having different
|
|
126
|
+
Assignment/Review and workspace constraints. Planning/delivery, lifecycle,
|
|
127
|
+
revocation and resource/Grant checks remain in their existing execution owners.
|
|
128
|
+
The directory cannot express every domain condition and this page does not
|
|
129
|
+
attempt to infer them from command names.
|
|
130
|
+
|
|
131
|
+
The adjacent **Built-in capability contracts** view projects
|
|
132
|
+
`BUILTIN_CAPABILITIES`, the same descriptors registered in `CapabilityRegistry`,
|
|
133
|
+
including required permission categories, input names, declared effect and
|
|
134
|
+
semantic source. It does not invent a CLI-to-capability mapping or interpret
|
|
135
|
+
those categories as role grants. Task-local plugin descriptors are excluded.
|
|
136
|
+
Exact `hidden` / `requestable` / `authorized` observations still require the
|
|
137
|
+
existing authenticated discovery path and target input; this page performs no
|
|
138
|
+
such assessment and emits no `access` field.
|
|
139
|
+
|
|
140
|
+
The `/api/permissions` metadata, `/commands`, `/command` and `/capabilities`
|
|
141
|
+
routes are GET-only, use the existing Web authentication, Host/Origin and page
|
|
142
|
+
token protection, and reject target/identity arguments. The only Store read is
|
|
143
|
+
the configured global role list, projected to names. No Session, Task contents,
|
|
144
|
+
Grant, plugin instance or Provider is read/acquired, and no operation is invoked.
|
|
145
|
+
New catalog commands enter automatically; do not maintain a Web ACL.
|
|
146
|
+
`web-permissions.test.js` checks full projection coverage, option filtering,
|
|
147
|
+
private-read/write exclusion and the HTTP read-only boundary.
|
|
148
|
+
|
|
114
149
|
## Verification and maintenance
|
|
115
150
|
|
|
116
151
|
`cli-authority.test.js` exercises isolated launcher-backed public reads, Draft
|
package/docs/release-workflow.md
CHANGED
|
@@ -51,6 +51,41 @@ merely to avoid a version decision. Package versions remain distinct from protoc
|
|
|
51
51
|
and persistent-storage contracts: their compatibility checks and required migrations
|
|
52
52
|
are unchanged, and the storage rules below do not authorize a package release.
|
|
53
53
|
|
|
54
|
+
### 2.14.0 compatibility and migration notes
|
|
55
|
+
|
|
56
|
+
The `v2.13.0` tag is retained as failed release evidence: its publication gates
|
|
57
|
+
failed before npm publication. This replacement minor retains the feature below
|
|
58
|
+
and fixes the package-smoke handover observation: one Controller snapshot and a
|
|
59
|
+
bounded 60-second multi-turn wait, preserving the input, Host identity, terminal
|
|
60
|
+
and no-replay assertions. Production runtime timeouts and release gates are
|
|
61
|
+
unchanged; the full fresh-install matrix must pass before publication.
|
|
62
|
+
|
|
63
|
+
This minor adds Settings → Permissions: a read-only command-by-role discovery
|
|
64
|
+
matrix and built-in capability contracts, with bounded search, filters and
|
|
65
|
+
details. The view derives from the existing command catalog, discovery projection
|
|
66
|
+
and capability registry; it is not a second ACL or an authorization engine.
|
|
67
|
+
|
|
68
|
+
Role-template visibility is not an exact invocation's authorization result.
|
|
69
|
+
Task, Project, Assignment, lifecycle, resource and Grant boundaries still apply
|
|
70
|
+
at execution time. Viewing a role does not impersonate it, execute a command,
|
|
71
|
+
issue or revoke authorization, or consume the viewed operation's Grant.
|
|
72
|
+
The new GET endpoints retain the existing Web access and request protections.
|
|
73
|
+
Existing CLI authorization, discovery and default permissions are unchanged.
|
|
74
|
+
See [authorization](authorization.md).
|
|
75
|
+
|
|
76
|
+
Storage remains **1.20**; this feature adds no storage migration or protocol
|
|
77
|
+
change. Earlier supported Homes still use the existing contiguous same-major
|
|
78
|
+
migration chain. With separate update authorization, use
|
|
79
|
+
`yui update --version 2.14.0`; the updater owns preflight, backup, migration when
|
|
80
|
+
needed, and Controller handover. Do not open upgraded Homes with an older binary
|
|
81
|
+
or edit storage manually. Minor numbering is not a general compatibility promise.
|
|
82
|
+
|
|
83
|
+
Release gates require exact-commit Core CI and fresh installation of the same
|
|
84
|
+
runtime tarball on Linux x64 and macOS x64/arm64 with Node 20/22/24/26.
|
|
85
|
+
These are deterministic isolated checks, not real-model validation. Publication
|
|
86
|
+
does not update a global CLI, migrate a production Home, replace its Controller,
|
|
87
|
+
or deploy or start a production Web listener.
|
|
88
|
+
|
|
54
89
|
### 2.12.0 compatibility and migration notes
|
|
55
90
|
|
|
56
91
|
This minor completes the scoped Job platform: explicit business results and fixed
|
|
@@ -36,6 +36,32 @@ Agent 选择一个预先声明的计划,设施从持久状态驱动该计划
|
|
|
36
36
|
不能将不兼容冒称为兼容,也不为回避版本选择新增历史兼容机制。包版本与协议、
|
|
37
37
|
持久存储契约仍然独立:兼容性检查和必要迁移要求不变,下文存储规则不授予发版权限。
|
|
38
38
|
|
|
39
|
+
### 2.14.0 兼容性及迁移说明
|
|
40
|
+
|
|
41
|
+
保留 `v2.13.0` 标签作为失败发布证据:其发布门禁在 npm 发布前失败。
|
|
42
|
+
本次替代 minor 保留下述功能,并修正包 smoke 的接续观察方式:使用一次 Controller
|
|
43
|
+
快照和有界的 60 秒多回合等待,保留输入、Host 身份、终态及不重放断言。
|
|
44
|
+
生产运行时超时和发布门禁不变;发布前仍须通过完整的全新安装矩阵。
|
|
45
|
+
|
|
46
|
+
本 minor 新增 Settings → Permissions:命令×角色的只读发现矩阵与内置能力契约,
|
|
47
|
+
支持有界搜索、筛选和详情。视图派生自现有 command catalog、discovery 投影及
|
|
48
|
+
capability registry,不维护第二份 ACL,也不是新的授权引擎。
|
|
49
|
+
|
|
50
|
+
角色模板的可见性不代表某次具体调用已获授权。Task、Project、Assignment、生命周期、
|
|
51
|
+
资源和 Grant 边界仍在执行时检查。查看角色不会冒充该角色、执行命令、签发或撤销授权,
|
|
52
|
+
也不会消费被查看操作的 Grant。新增 GET 接口沿用现有 Web 访问及请求保护。
|
|
53
|
+
既有 CLI 授权、命令发现和默认权限不变。参见[授权说明](authorization.md)。
|
|
54
|
+
|
|
55
|
+
存储保持 **1.20**;本功能不新增存储迁移或协议变更。更早的受支持 Home 仍通过现有
|
|
56
|
+
连续同 major 迁移链升级。另获更新授权后使用 `yui update --version 2.14.0`;
|
|
57
|
+
更新器负责预检、备份、必要迁移及 Controller 接续。不要用旧二进制打开已升级的 Home,
|
|
58
|
+
也不要手改存储。minor 编号不是通用的兼容性承诺。
|
|
59
|
+
|
|
60
|
+
发布门禁要求精确提交的 Core CI,并在 Linux x64、macOS x64/arm64 与
|
|
61
|
+
Node 20/22/24/26 上全新安装同一个 runtime tarball。这些是确定性隔离检查,
|
|
62
|
+
不是真实模型验证。发布不会更新全局 CLI、迁移生产 Home、替换其 Controller,
|
|
63
|
+
也不会部署或启动生产 Web。
|
|
64
|
+
|
|
39
65
|
### 2.12.0 兼容性及迁移说明
|
|
40
66
|
|
|
41
67
|
本 minor 完成作用域明确的 Job 平台:显式业务结果及固定产物、Schedule 目标与服务
|
package/i18n/README.zh-CN.md
CHANGED
|
@@ -179,6 +179,10 @@ Agent 可以重新读取任务上下文,继续兼容的 Session,或在必要
|
|
|
179
179
|
|
|
180
180
|
### 产品结构
|
|
181
181
|
|
|
182
|
+
Web 的 **设置 → 命令权限** 提供可搜索的只读“命令 × 角色”发现矩阵与内置能力契约。
|
|
183
|
+
“发现可见”不代表具体调用已授权;页面不会修改权限规则或 Grant。
|
|
184
|
+
详见[发现与授权说明](../docs/authorization.md#read-only-web-templates)。
|
|
185
|
+
|
|
182
186
|
Yui 为你组织的东西 —— 是持久对象,而不是进程:
|
|
183
187
|
|
|
184
188
|
```text
|