@zq-silk/yui 2.11.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/artifacts/artifactCapability.js +14 -8
- package/dist/artifacts/artifactCommitLock.js +7 -5
- package/dist/artifacts/jobArtifact.js +93 -0
- package/dist/artifacts/jobArtifactTransport.js +13 -0
- package/dist/artifacts/taskArtifactRepository.js +58 -10
- package/dist/cli/commandCatalog.js +45 -14
- package/dist/cli/commandDiscovery.js +1 -1
- package/dist/commands/durableJobCommands.js +31 -6
- package/dist/commands/globalRoleCommands.js +5 -0
- package/dist/commands/scheduleCommands.js +14 -3
- package/dist/commands/taskCommands.js +5 -2
- package/dist/context/taskCatalog.js +6 -1
- package/dist/controlPlaneCli.js +7 -2
- package/dist/controller/agentJobControl.js +391 -0
- package/dist/controller/controller.js +24 -3
- package/dist/controller/fileSchedulerStoreAdapter.js +12 -1
- package/dist/controller/globalInputDelivery.js +8 -1
- package/dist/controller/jobArtifactControl.js +111 -0
- package/dist/controller/jobControl.js +4 -0
- package/dist/controller/jobDefinitionControl.js +191 -19
- package/dist/controller/jobSupervisor.js +83 -3
- package/dist/controller/jobTaskDelegationControl.js +193 -0
- package/dist/controller/runtime.js +9 -1
- package/dist/controller/scheduleControl.js +83 -20
- package/dist/integration/gitIntegrationService.js +2 -0
- package/dist/job/agentJob.js +131 -0
- package/dist/job/agentJobObservation.js +61 -0
- package/dist/job/agentRoleScope.js +33 -0
- package/dist/job/durableJob.js +63 -28
- package/dist/job/jobAssignmentScope.js +1 -0
- package/dist/job/jobBusinessResult.js +72 -0
- package/dist/job/jobCleanupInspection.js +4 -0
- package/dist/job/jobDefinition.js +26 -2
- package/dist/job/jobRunner.js +4 -3
- package/dist/job/projectRefreshRunner.js +54 -18
- package/dist/job/projectSync.js +162 -0
- package/dist/job/scheduleService.js +55 -0
- package/dist/job/taskDelegation.js +48 -0
- package/dist/message/messageContinuation.js +1 -1
- package/dist/resources/projectResourceService.js +1 -1
- package/dist/resources/resourceGc.js +1 -1
- package/dist/role/role.js +12 -0
- package/dist/schedule/schedule.js +65 -15
- package/dist/scheduler/wakeReason.js +4 -0
- package/dist/storage/agentJobSchema.js +10 -0
- package/dist/storage/recordValidation.js +2 -0
- package/dist/storage/sqliteSchema.js +77 -6
- package/dist/storage/sqliteStore.js +53 -0
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +3 -0
- package/dist/storage/taskCatalog.js +10 -2
- package/dist/storage/upgrade/jobBusinessResultMigration.js +20 -0
- package/dist/storage/upgrade/scheduleDispatchMigration.js +15 -0
- package/dist/task/archivePreflight.js +15 -0
- package/dist/task/completionReadiness.js +7 -0
- package/dist/task/task.js +17 -1
- package/dist/verification/verificationGateService.js +4 -0
- package/dist/web/assets/client/app/catalog.js +16 -0
- package/dist/web/assets/client/app/settings.js +4 -0
- package/dist/web/assets/client/i18n/en.js +75 -2
- package/dist/web/assets/client/i18n/zh-CN.js +75 -2
- package/dist/web/assets/client/index.js +2 -0
- package/dist/web/assets/client/layout/manageShell.js +9 -0
- package/dist/web/assets/client/layout/router.js +1 -0
- package/dist/web/assets/client/views/jobs.js +166 -37
- package/dist/web/assets/client/views/permissions.js +129 -0
- package/dist/web/assets/client/views/task/files.js +5 -4
- package/dist/web/assets/client/views/task/page.js +5 -1
- 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/taskPreviews.js +2 -2
- package/dist/web/webJobs.js +36 -11
- package/dist/web/webMaterials.js +21 -9
- package/dist/web/webPermissions.js +97 -0
- package/dist/web/webServer.js +44 -5
- package/dist/web/webTaskSurface.js +1 -0
- package/docs/authorization.md +35 -0
- package/docs/job-runs.md +392 -17
- package/docs/release-workflow.md +77 -0
- package/docs/release-workflow.zh-CN.md +57 -0
- package/docs/schedules.md +69 -15
- package/docs/storage-baseline.md +38 -1
- package/docs/storage-baseline.zh-CN.md +27 -1
- package/docs/testing/verification-levels.md +2 -2
- package/docs/testing/verification-levels.zh-CN.md +2 -2
- package/i18n/README.zh-CN.md +4 -0
- package/package.json +1 -1
package/dist/web/webJobs.js
CHANGED
|
@@ -5,8 +5,8 @@ import { requireKnownFields } from "../domain/validation.js";
|
|
|
5
5
|
import { WebRequestRejected, webLocalMutation } from "./webMutation.js";
|
|
6
6
|
/** Local-user ingress only, behind the Web token/origin boundary. Never use
|
|
7
7
|
* this adapter for managed RPC callers. The route fixes scope; the server
|
|
8
|
-
*
|
|
9
|
-
*
|
|
8
|
+
* uses public user authority for global controlled actions. Task shell access
|
|
9
|
+
* still resolves the current Operator binding; no invented Session.
|
|
10
10
|
*/
|
|
11
11
|
export function createWebJobs(store, home) {
|
|
12
12
|
function invoke(scope, family, value, write, now) {
|
|
@@ -14,21 +14,31 @@ export function createWebJobs(store, home) {
|
|
|
14
14
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
15
15
|
throw new Error("Expected an operation.");
|
|
16
16
|
requireKnownFields(value, family === "jobs"
|
|
17
|
-
? ["op", "id", "title", "configuration", "expectedRevision", "runId", "requestId"
|
|
18
|
-
|
|
17
|
+
? ["op", "id", "title", "configuration", "expectedRevision", "runId", "requestId",
|
|
18
|
+
"reference", "offset", "expectedSessionId"]
|
|
19
|
+
: ["op", "id", "jobId", "rule", "expectedRevision", "authorization", "target"], "Web Job/Schedule request");
|
|
19
20
|
const input = value;
|
|
20
|
-
const reads = family === "jobs" ? ["list", "show", "runs", "run-get", "
|
|
21
|
-
|
|
21
|
+
const reads = family === "jobs" ? ["list", "show", "runs", "run-get", "run-result", "run-evidence",
|
|
22
|
+
"run-delegation", "logs", "artifact-list", "artifact-preview", "session-inspect"] : ["list", "show"];
|
|
23
|
+
const writes = family === "jobs" ? ["define", "update", "run", "run-cancel", "artifact-register",
|
|
24
|
+
"session-stop", "session-new"] : ["create", "update", "enable", "disable"];
|
|
22
25
|
if (!(write ? writes : reads).includes(String(input.op)))
|
|
23
26
|
throw new Error("Operation is not available on this endpoint.");
|
|
24
27
|
if (scope.kind === "task" && !tx.getTask(scope.taskId))
|
|
25
28
|
throw new Error("Task not found.");
|
|
26
|
-
const session = activeLiveRoleAgentSession(tx.getGlobalRoleSessionSet("operator"));
|
|
27
|
-
if (!session)
|
|
28
|
-
throw new Error("A current Operator Session is required for Job management.");
|
|
29
|
-
const caller = { scope: "global", role: "operator", nativeSessionId: session.nativeSessionId,
|
|
30
|
-
agentId: session.agentId, adapterId: session.adapterId };
|
|
29
|
+
const session = scope.kind === "task" ? activeLiveRoleAgentSession(tx.getGlobalRoleSessionSet("operator")) : null;
|
|
30
|
+
if (scope.kind === "task" && !session)
|
|
31
|
+
throw new Error("A current Operator Session is required for Task Job management.");
|
|
32
|
+
const caller = session ? { scope: "global", role: "operator", nativeSessionId: session.nativeSessionId,
|
|
33
|
+
agentId: session.agentId, adapterId: session.adapterId } : { scope: "user" };
|
|
31
34
|
const { requestId, ...fields } = input;
|
|
35
|
+
if (typeof fields.reference === "string")
|
|
36
|
+
fields.reference = JSON.parse(fields.reference);
|
|
37
|
+
if (fields.offset !== undefined) {
|
|
38
|
+
if (!/^\d{1,9}$/u.test(String(fields.offset)))
|
|
39
|
+
throw new Error("Invalid artifact offset.");
|
|
40
|
+
fields.offset = Number(fields.offset);
|
|
41
|
+
}
|
|
32
42
|
if (input.op === "run" && (typeof requestId !== "string" || !requestId.trim() || requestId.length > 200)) {
|
|
33
43
|
throw new Error("Immediate execution requires a stable requestId.");
|
|
34
44
|
}
|
|
@@ -36,6 +46,21 @@ export function createWebJobs(store, home) {
|
|
|
36
46
|
...(input.op === "run" ? { trigger: { source: "manual", key: requestId } } : {}) };
|
|
37
47
|
return (family === "jobs" ? createJobDefinitionControl(tx, home) : createScheduleControl(tx, home))(params, now);
|
|
38
48
|
};
|
|
49
|
+
// Artifact Git I/O is asynchronous, with its own authorization recheck and
|
|
50
|
+
// short publication transaction. Never keep a SQLite transaction across it.
|
|
51
|
+
if (family === "jobs" && value && typeof value === "object"
|
|
52
|
+
&& "op" in value && String(value.op).startsWith("artifact-")) {
|
|
53
|
+
try {
|
|
54
|
+
return Promise.resolve(apply(store)).catch(error => {
|
|
55
|
+
if (!write)
|
|
56
|
+
throw new WebRequestRejected(error instanceof Error ? error.message : String(error));
|
|
57
|
+
throw error; // A Git/publication failure is not proof of non-submission.
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
catch (error) {
|
|
61
|
+
throw new WebRequestRejected(error instanceof Error ? error.message : String(error));
|
|
62
|
+
}
|
|
63
|
+
}
|
|
39
64
|
if (write)
|
|
40
65
|
return webLocalMutation(store, apply);
|
|
41
66
|
try {
|
package/dist/web/webMaterials.js
CHANGED
|
@@ -30,24 +30,36 @@ async function fixedImage(home, taskId, input) {
|
|
|
30
30
|
/** One selected immutable file, never a workspace path or a remote URL.
|
|
31
31
|
* Markdown is bounded and whole so fences/tables are not split by pagination. */
|
|
32
32
|
export async function readArtifactPreview(home, taskId, ref, offset = 0) {
|
|
33
|
-
if (
|
|
33
|
+
if (ref.taskId !== taskId)
|
|
34
|
+
throw new WebRequestRejected("Materials must belong to the same Task.");
|
|
35
|
+
const valid = validateGitArtifactRef(ref);
|
|
36
|
+
const file = await resolveGitArtifact(home, valid);
|
|
37
|
+
return { ...valid, ...previewArtifactContent(file, offset) };
|
|
38
|
+
}
|
|
39
|
+
/** Shared data-only preview for Task and Job evidence; no path lookup or authority. */
|
|
40
|
+
export function previewArtifactContent(file, offset = 0) {
|
|
41
|
+
if (/\.(svg|gif|avif|bmp|ico|tiff?)$/iu.test(file.relativePath)) {
|
|
34
42
|
throw new WebRequestRejected("Unsupported image format. Save a static PNG, JPEG or WebP instead.");
|
|
35
43
|
}
|
|
36
|
-
if (
|
|
44
|
+
if (file.relativePath.length > 512)
|
|
45
|
+
throw new WebRequestRejected("Artifact path is too long.");
|
|
46
|
+
if (isRasterArtifact(file.relativePath)) {
|
|
37
47
|
if (offset !== 0)
|
|
38
48
|
throw new WebRequestRejected("Images do not have text offsets.");
|
|
39
|
-
return
|
|
49
|
+
return { digest: file.digest, kind: "image", byteSize: file.bytes.length,
|
|
50
|
+
...rasterMetadata(file.bytes), base64: file.bytes.toString("base64") };
|
|
40
51
|
}
|
|
41
|
-
|
|
52
|
+
textPath(file.relativePath);
|
|
53
|
+
const text = decodeText(file.bytes);
|
|
54
|
+
if (/\.md$|\.markdown$/iu.test(file.relativePath)) {
|
|
42
55
|
if (offset !== 0)
|
|
43
56
|
throw new WebRequestRejected("Markdown previews start at offset zero.");
|
|
44
|
-
|
|
45
|
-
if (file.bytes > MAX_MATERIAL_BYTES)
|
|
57
|
+
if (file.bytes.length > MAX_MATERIAL_BYTES)
|
|
46
58
|
throw new WebRequestRejected("Markdown preview exceeds 256 KiB; split the document.");
|
|
47
|
-
return {
|
|
48
|
-
offset: 0, totalCharacters:
|
|
59
|
+
return { digest: file.digest, kind: "markdown", byteSize: file.bytes.length, content: text,
|
|
60
|
+
offset: 0, totalCharacters: text.length, nextOffset: null };
|
|
49
61
|
}
|
|
50
|
-
return
|
|
62
|
+
return { digest: file.digest, byteSize: file.bytes.length, ...page(text, offset) };
|
|
51
63
|
}
|
|
52
64
|
function textPath(path) {
|
|
53
65
|
const safe = safeRelativeArtifactPath(path);
|
|
@@ -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;
|
|
@@ -284,15 +322,16 @@ async function handleHttpRequest(request, response, store, dependencies, token,
|
|
|
284
322
|
let value;
|
|
285
323
|
if (method === "GET") {
|
|
286
324
|
const query = new URL(request.url, "http://localhost").searchParams;
|
|
287
|
-
if ([...query.keys()].some(key => !["op", "id", "runId"].includes(key)
|
|
288
|
-
|
|
289
|
-
|
|
325
|
+
if ([...query.keys()].some(key => !["op", "id", "runId", "reference", "offset"].includes(key)
|
|
326
|
+
|| query.getAll(key).length !== 1))
|
|
327
|
+
throw new WebRequestRejected("Unknown or repeated Job query.");
|
|
328
|
+
value = await dependencies.jobs.read(scope, family, Object.fromEntries(query), now());
|
|
290
329
|
}
|
|
291
330
|
else if (method === "POST") {
|
|
292
331
|
if (headerValue(request, "origin") !== `http://${headerValue(request, "host")}`) {
|
|
293
332
|
throw new WebRequestRejected("Job changes require a same-origin request.");
|
|
294
333
|
}
|
|
295
|
-
value = dependencies.jobs.mutate(scope, family, await readMutationBody(request), now());
|
|
334
|
+
value = await dependencies.jobs.mutate(scope, family, await readMutationBody(request), now());
|
|
296
335
|
}
|
|
297
336
|
else
|
|
298
337
|
throw new WebRequestRejected("Method not allowed.");
|
|
@@ -631,7 +670,7 @@ async function handleHttpRequest(request, response, store, dependencies, token,
|
|
|
631
670
|
try {
|
|
632
671
|
const assetVersion = new URL(request.url, "http://localhost").searchParams.get("v");
|
|
633
672
|
const asset = findWebAsset(pathname, assetVersion);
|
|
634
|
-
const managementPage = /^\/(?:models(?:\/(?:providers|catalog|quota))?|settings(?:\/(?:preferences|skills|server|connection))?)\/?$/.test(pathname);
|
|
673
|
+
const managementPage = /^\/(?:models(?:\/(?:providers|catalog|quota))?|settings(?:\/(?:preferences|skills|jobs|permissions|server|connection))?)\/?$/.test(pathname);
|
|
635
674
|
if (pathname === "/" || pathname === "/index.html" || managementPage) {
|
|
636
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");
|
|
637
676
|
}
|
|
@@ -53,6 +53,7 @@ export function createWebTaskSurface(store, options = {}, observations = [], hos
|
|
|
53
53
|
core: readTaskContext(tx, taskId, environment),
|
|
54
54
|
task: {
|
|
55
55
|
id: task.id, title: clip(task.title), status: task.status, updatedAt: task.updatedAt,
|
|
56
|
+
source: task.source,
|
|
56
57
|
executionGate: task.executionGate, priority: task.priority, tags: task.tags?.slice(0, 16).map(value => clip(value)),
|
|
57
58
|
projectBindings: task.projectBindings.slice(0, 32),
|
|
58
59
|
description: clip(task.description), completionSummary: clip(task.completionSummary),
|
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
|