@zq-silk/yui 1.1.2 → 2.1.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/ARCHITECTURE.md +2 -1
- package/ARCHITECTURE.zh-CN.md +2 -1
- package/README.md +2 -0
- package/dist/cli/commandCatalog.js +344 -324
- package/dist/cli/commandDiscovery.js +69 -0
- package/dist/cli/completion.js +22 -227
- package/dist/cli/dynamicCompletion.js +12 -6
- package/dist/cli/invocationAuthority.js +2 -2
- package/dist/cli/invocationRouter.js +18 -7
- package/dist/cli/managedDiagnostics.js +2 -1
- package/dist/cli/updatePorts.js +0 -5
- package/dist/cli.js +184 -130
- package/dist/commands/capabilityCommands.js +5 -5
- package/dist/commands/controllerCommands.js +2 -1
- package/dist/commands/globalRoleCommands.js +86 -44
- package/dist/commands/grantCommands.js +48 -8
- package/dist/commands/taskCommands.js +157 -196
- package/dist/commands/taskContextCommand.js +29 -6
- package/dist/commands/taskFactCommands.js +10 -69
- package/dist/commands/taskInputCommands.js +26 -35
- package/dist/commands/taskPublicationCommands.js +2 -35
- package/dist/commands/taskPublicationVerifyCommand.js +1 -2
- package/dist/commands/taskUpstreamCommands.js +2 -1
- package/dist/context/runContextPack.js +8 -29
- package/dist/context/sessionBootstrapManifest.js +5 -101
- package/dist/context/taskContext.js +104 -41
- package/dist/controller/clientRuntime.js +16 -8
- package/dist/controller/fileSchedulerStoreAdapter.js +24 -0
- package/dist/controller/runtime.js +6 -0
- package/dist/core/controllerClient.js +46 -21
- package/dist/core/protocol.js +12 -7
- package/dist/doctor/doctor.js +4 -3
- package/dist/errors/cliError.js +4 -4
- package/dist/errors/cliFailure.js +213 -0
- package/dist/executor/fileRoleLaunchPlanner.js +1 -4
- package/dist/grant/capabilityGrant.js +13 -0
- package/dist/grant/taskAuthorization.js +127 -0
- package/dist/kernel/accessAssessment.js +1 -0
- package/dist/kernel/builtinCapabilities.js +88 -9
- package/dist/kernel/capabilityRegistry.js +64 -19
- package/dist/kernel/kernelPorts.js +2 -2
- package/dist/nativeAgent/agent.js +173 -0
- package/dist/nativeAgent/contracts.js +1 -0
- package/dist/nativeAgent/demo.js +29 -0
- package/dist/nativeAgent/index.js +3 -0
- package/dist/nativeAgent/mockProvider.js +43 -0
- package/dist/nativeAgent/textTools.js +156 -0
- package/dist/nativeAgent/validation.js +95 -0
- package/dist/output/boundedRead.js +118 -0
- package/dist/plugins/pluginService.js +98 -8
- package/dist/release/releaseWorkflowEngine.js +8 -1
- package/dist/resources/projectResourceService.js +23 -1
- package/dist/runtime/managedCaller.js +2 -2
- package/dist/runtime/managedIdentity.js +6 -0
- package/dist/runtime/runtimeCoherence.js +10 -8
- package/dist/storage/sqliteSchema.js +10 -0
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/taskStore.js +3 -0
- package/dist/surface/surfaceContributions.js +3 -0
- package/dist/task/leaderArchive.js +111 -0
- package/dist/task/leaderArchiveAuthority.js +24 -0
- package/dist/task/taskAuthority.js +5 -10
- package/dist/tmux/commandExecutor.js +7 -5
- package/dist/web/assets/assetManifest.js +38 -22
- package/dist/web/assets/client/api.js +115 -0
- package/dist/web/assets/client/app.js +555 -863
- package/dist/web/assets/client/components.js +323 -897
- package/dist/web/assets/client/detail.js +333 -0
- package/dist/web/assets/client/dock.js +225 -0
- package/dist/web/assets/client/dom.js +55 -3
- package/dist/web/assets/client/evidence.js +172 -0
- package/dist/web/assets/client/format.js +25 -14
- package/dist/web/assets/client/forms.js +230 -0
- package/dist/web/assets/client/i18n.js +1020 -791
- package/dist/web/assets/client/overview.js +115 -0
- package/dist/web/assets/client/records.js +63 -0
- package/dist/web/assets/client/sections.js +313 -0
- package/dist/web/assets/client/sidebar.js +117 -0
- package/dist/web/assets/client/theme.js +61 -22
- package/dist/web/assets/icons.js +39 -0
- package/dist/web/assets/shell.js +124 -118
- package/dist/web/assets/styles/base.js +41 -0
- package/dist/web/assets/styles/components.js +167 -0
- package/dist/web/assets/styles/layout.js +49 -77
- package/dist/web/assets/styles/markdown.js +16 -24
- package/dist/web/assets/styles/responsive.js +33 -44
- package/dist/web/assets/styles/tokens.js +65 -89
- package/dist/web/assets/styles/views.js +289 -0
- package/dist/web/webServer.js +2 -1
- package/docs/architecture/README.md +1 -0
- package/docs/architecture/README.zh-CN.md +1 -0
- package/docs/cli-information-contract.md +107 -0
- package/docs/cli-information-contract.zh-CN.md +81 -0
- package/docs/managed-turn-and-session-runtime.md +9 -0
- package/docs/managed-turn-and-session-runtime.zh-CN.md +6 -0
- package/docs/plugin-sdk.md +7 -3
- package/docs/plugin-sdk.zh-CN.md +5 -2
- package/docs/release-workflow.md +42 -9
- package/docs/release-workflow.zh-CN.md +30 -7
- package/docs/storage-baseline.md +5 -0
- package/docs/testing/verification-levels.md +2 -2
- package/i18n/README.zh-CN.md +2 -0
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +8 -1
- package/skills/yui-leader/references/authorization.md +64 -0
- package/skills/yui-leader/references/execution.md +18 -4
- package/skills/yui-leader/references/task-plugins.md +4 -2
- package/skills/yui-operator/SKILL.md +9 -2
- package/skills/yui-runtime/SKILL.md +77 -9
- package/skills/yui-runtime/references/publication.md +3 -1
- package/dist/web/assets/client/taskSummary.js +0 -350
- package/dist/web/assets/client/taskSurface.js +0 -616
- package/dist/web/assets/client/view.js +0 -608
- package/dist/web/assets/styles/cards.js +0 -247
- package/dist/web/assets/styles/widgets.js +0 -168
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
// JSON escaping can expand a 64 KiB text result to almost 384 KiB.
|
|
2
|
+
export const limits = { messageBytes: 512 * 1024, argumentBytes: 64 * 1024, historyBytes: 1024 * 1024, callsPerStep: 8 };
|
|
3
|
+
export class AgentFault extends Error {
|
|
4
|
+
code;
|
|
5
|
+
constructor(code, message) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.code = code;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
export function snapshot(value) {
|
|
11
|
+
const copy = structuredClone(value);
|
|
12
|
+
const freeze = (item) => {
|
|
13
|
+
if (item && typeof item === 'object') {
|
|
14
|
+
for (const child of Object.values(item))
|
|
15
|
+
freeze(child);
|
|
16
|
+
Object.freeze(item);
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
freeze(copy);
|
|
20
|
+
return copy;
|
|
21
|
+
}
|
|
22
|
+
function object(value) {
|
|
23
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
24
|
+
}
|
|
25
|
+
function text(value) { return typeof value === 'string'; }
|
|
26
|
+
function id(value) { return text(value) && value.trim().length > 0; }
|
|
27
|
+
function json(value, depth = 0) {
|
|
28
|
+
if (depth > 32)
|
|
29
|
+
return false;
|
|
30
|
+
if (value === null || text(value) || typeof value === 'boolean')
|
|
31
|
+
return true;
|
|
32
|
+
if (typeof value === 'number')
|
|
33
|
+
return Number.isFinite(value);
|
|
34
|
+
if (Array.isArray(value))
|
|
35
|
+
return Array.from(value).every(v => json(v, depth + 1));
|
|
36
|
+
return object(value) && Object.getPrototypeOf(value) === Object.prototype
|
|
37
|
+
&& Object.values(value).every(v => json(v, depth + 1));
|
|
38
|
+
}
|
|
39
|
+
export function size(value) { return Buffer.byteLength(JSON.stringify(value), 'utf8'); }
|
|
40
|
+
function bounded(value) { return json(value) && size(value) <= limits.messageBytes; }
|
|
41
|
+
function calls(value) {
|
|
42
|
+
return Array.isArray(value) && value.length <= limits.callsPerStep
|
|
43
|
+
&& value.every(c => object(c) && id(c.id) && id(c.name) && bounded(c.arguments)
|
|
44
|
+
&& size(c.arguments) <= limits.argumentBytes)
|
|
45
|
+
&& new Set(value.map(c => c.id)).size === value.length;
|
|
46
|
+
}
|
|
47
|
+
export function validOutcome(value) {
|
|
48
|
+
return object(value) && bounded(value) && (value.ok === true ? text(value.content)
|
|
49
|
+
: value.ok === false && object(value.error) && id(value.error.code) && text(value.error.message)
|
|
50
|
+
&& (value.error.effect === 'none' || value.error.effect === 'unknown'));
|
|
51
|
+
}
|
|
52
|
+
export function response(value, usedIds) {
|
|
53
|
+
if (!object(value) || !bounded(value) || !text(value.content)
|
|
54
|
+
|| !(value.kind === 'final' || (value.kind === 'tool_calls' && calls(value.calls)
|
|
55
|
+
&& value.calls.length > 0 && value.calls.every(c => !usedIds.has(c.id))))) {
|
|
56
|
+
throw new AgentFault('provider_protocol', 'Invalid, oversized or duplicate model response');
|
|
57
|
+
}
|
|
58
|
+
return snapshot(value);
|
|
59
|
+
}
|
|
60
|
+
export function history(value) {
|
|
61
|
+
const pending = new Map();
|
|
62
|
+
const used = new Set();
|
|
63
|
+
if (!Array.isArray(value) || !json(value) || size(value) > limits.historyBytes) {
|
|
64
|
+
throw new AgentFault('invalid_history', 'History must be bounded JSON messages');
|
|
65
|
+
}
|
|
66
|
+
for (const m of value) {
|
|
67
|
+
if (!object(m) || !bounded(m))
|
|
68
|
+
throw new AgentFault('invalid_history', 'Invalid history message');
|
|
69
|
+
if (m.role === 'tool') {
|
|
70
|
+
if (!id(m.toolCallId) || !id(m.name) || !validOutcome(m.outcome)
|
|
71
|
+
|| pending.get(m.toolCallId) !== m.name || !pending.has(m.toolCallId)) {
|
|
72
|
+
throw new AgentFault('invalid_history', 'Tool results must match exactly one pending call');
|
|
73
|
+
}
|
|
74
|
+
pending.delete(m.toolCallId);
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
if (pending.size || !text(m.content))
|
|
78
|
+
throw new AgentFault('invalid_history', 'Unsettled calls or invalid content');
|
|
79
|
+
if (m.role === 'assistant' && calls(m.toolCalls)) {
|
|
80
|
+
for (const c of m.toolCalls) {
|
|
81
|
+
if (used.has(c.id))
|
|
82
|
+
throw new AgentFault('invalid_history', 'Duplicate tool call identity');
|
|
83
|
+
used.add(c.id);
|
|
84
|
+
pending.set(c.id, c.name);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
else if (m.role !== 'system' && m.role !== 'user') {
|
|
88
|
+
throw new AgentFault('invalid_history', 'Invalid message role or assistant calls');
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (pending.size)
|
|
93
|
+
throw new AgentFault('invalid_history', 'History contains unsettled calls');
|
|
94
|
+
return snapshot([...value]);
|
|
95
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { usageError } from "../errors/cliError.js";
|
|
3
|
+
export const READ_PAGE_BYTES = 32 * 1024;
|
|
4
|
+
export const INLINE_DOCUMENT_BYTES = 16 * 1024;
|
|
5
|
+
function assertSource(source) {
|
|
6
|
+
if (Buffer.byteLength(source) > 1024)
|
|
7
|
+
throw usageError("Read source identity exceeds its 1024-byte budget.");
|
|
8
|
+
}
|
|
9
|
+
export function readDigest(value) {
|
|
10
|
+
return createHash("sha256").update(JSON.stringify(value)).digest("hex");
|
|
11
|
+
}
|
|
12
|
+
/** A save receipt is not another read of the submitted body. Keep the exact
|
|
13
|
+
* delivery/control facts and identity; body/history are read on demand.
|
|
14
|
+
*/
|
|
15
|
+
export function messageReceipt(message) {
|
|
16
|
+
const { body: _body, ...facts } = message;
|
|
17
|
+
return { ...facts, digest: readDigest(message), bodyBytes: Buffer.byteLength(message.body) };
|
|
18
|
+
}
|
|
19
|
+
export function encodeReadCursor(cursor) {
|
|
20
|
+
return Buffer.from(JSON.stringify(cursor)).toString("base64url");
|
|
21
|
+
}
|
|
22
|
+
export function decodeReadCursor(raw, source, digest) {
|
|
23
|
+
let cursor;
|
|
24
|
+
try {
|
|
25
|
+
if (raw.length > 4096)
|
|
26
|
+
throw new Error();
|
|
27
|
+
cursor = JSON.parse(Buffer.from(raw, "base64url").toString("utf8"));
|
|
28
|
+
if (cursor.source !== source || !Number.isSafeInteger(cursor.offset) || cursor.offset < 0
|
|
29
|
+
|| typeof cursor.digest !== "string")
|
|
30
|
+
throw new Error();
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
throw usageError("Invalid read cursor for this source/scope.");
|
|
34
|
+
}
|
|
35
|
+
if (cursor.digest !== digest)
|
|
36
|
+
throw usageError("Read source changed; restart the read instead of mixing versions.");
|
|
37
|
+
return cursor.offset;
|
|
38
|
+
}
|
|
39
|
+
export function readLimit(value) {
|
|
40
|
+
const limit = value ?? 20;
|
|
41
|
+
if (!Number.isSafeInteger(limit) || limit < 1 || limit > 100) {
|
|
42
|
+
throw usageError("Read limit must be between 1 and 100.");
|
|
43
|
+
}
|
|
44
|
+
return limit;
|
|
45
|
+
}
|
|
46
|
+
/** For small in-memory authorized collections. Authorize/filter BEFORE calling.
|
|
47
|
+
* Cursors bind the selected collection, never grant access, and store no state.
|
|
48
|
+
*/
|
|
49
|
+
export function recordPage(records, source, options = {}) {
|
|
50
|
+
assertSource(source);
|
|
51
|
+
const limit = readLimit(options.limit);
|
|
52
|
+
const digest = readDigest(records);
|
|
53
|
+
const offset = options.cursor === undefined ? 0 : decodeReadCursor(options.cursor, source, digest);
|
|
54
|
+
if (offset > records.length)
|
|
55
|
+
throw usageError("Read cursor exceeds this collection.");
|
|
56
|
+
const items = [];
|
|
57
|
+
// Reserve room for identities, counts and continuation. Oversized individual
|
|
58
|
+
// records belong in a document read, not silently truncated list metadata.
|
|
59
|
+
let bytes = 0;
|
|
60
|
+
for (const record of records.slice(offset, offset + limit)) {
|
|
61
|
+
const size = Buffer.byteLength(JSON.stringify(record)) + 1;
|
|
62
|
+
if (size > READ_PAGE_BYTES / 2)
|
|
63
|
+
throw usageError("List entry exceeds its summary budget; read the exact record.");
|
|
64
|
+
if (bytes + size > READ_PAGE_BYTES - 4096)
|
|
65
|
+
break;
|
|
66
|
+
items.push(record);
|
|
67
|
+
bytes += size;
|
|
68
|
+
}
|
|
69
|
+
const end = offset + items.length;
|
|
70
|
+
return {
|
|
71
|
+
source, digest, items, total: records.length, complete: end === records.length,
|
|
72
|
+
nextCursor: end < records.length ? encodeReadCursor({ source, digest, offset: end }) : null,
|
|
73
|
+
limits: { items: limit, bytes: READ_PAGE_BYTES }
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
/** Small exact details stay native JSON. Large details explicitly become a
|
|
77
|
+
* contentPage. Concatenate text in order, then JSON.parse once, never each chunk.
|
|
78
|
+
* JSON character offsets (not bytes) preserve Unicode and escaped controls.
|
|
79
|
+
* The source is an exact authorized read identity; reauthorize every page.
|
|
80
|
+
*/
|
|
81
|
+
export function boundedDocument(value, source, cursor) {
|
|
82
|
+
assertSource(source);
|
|
83
|
+
const text = JSON.stringify(value);
|
|
84
|
+
const totalBytes = Buffer.byteLength(text);
|
|
85
|
+
if (cursor === undefined && totalBytes <= INLINE_DOCUMENT_BYTES)
|
|
86
|
+
return value;
|
|
87
|
+
const digest = readDigest(value);
|
|
88
|
+
const offset = cursor === undefined ? 0 : decodeReadCursor(cursor, source, digest);
|
|
89
|
+
if (offset >= text.length && offset !== 0)
|
|
90
|
+
throw usageError("Read cursor exceeds this document.");
|
|
91
|
+
let end = Math.min(offset + 4096, text.length);
|
|
92
|
+
// Never cut between UTF-16 surrogate halves.
|
|
93
|
+
if (end < text.length && /[\uD800-\uDBFF]/u.test(text[end - 1]))
|
|
94
|
+
end--;
|
|
95
|
+
return { contentPage: {
|
|
96
|
+
source, digest, encoding: "json", offset, totalCharacters: text.length,
|
|
97
|
+
totalBytes, text: text.slice(offset, end), complete: end === text.length,
|
|
98
|
+
nextCursor: end < text.length ? encodeReadCursor({ source, digest, offset: end }) : null
|
|
99
|
+
} };
|
|
100
|
+
}
|
|
101
|
+
/** Shared parser for list/detail command tails; no options reach mutations. */
|
|
102
|
+
export function readOptions(args, flags = ["--cursor", "--limit"]) {
|
|
103
|
+
const positionals = [];
|
|
104
|
+
const values = new Map();
|
|
105
|
+
for (let i = 0; i < args.length; i++) {
|
|
106
|
+
const arg = args[i];
|
|
107
|
+
if (!arg.startsWith("--")) {
|
|
108
|
+
positionals.push(arg);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
if (!flags.includes(arg) || values.has(arg) || args[i + 1] === undefined || args[i + 1].startsWith("--")) {
|
|
112
|
+
throw usageError(`Invalid read option: ${arg}.`);
|
|
113
|
+
}
|
|
114
|
+
values.set(arg, args[++i]);
|
|
115
|
+
}
|
|
116
|
+
return { positionals, values, cursor: values.get("--cursor"),
|
|
117
|
+
...(values.has("--limit") ? { limit: Number(values.get("--limit")) } : {}) };
|
|
118
|
+
}
|
|
@@ -98,24 +98,39 @@ export function createPluginService(store, host, registry) {
|
|
|
98
98
|
source: `plugin:${pkg.manifest.id}@${pkg.digest}`, required: pkg.manifest.required
|
|
99
99
|
}));
|
|
100
100
|
const refFor = (taskId, pkg) => ({ id: keyFor(taskId, pkg.manifest.id), generation: randomUUID() });
|
|
101
|
+
const executionParams = (taskId, preparationId, pkg, phase) => ({
|
|
102
|
+
pluginId: pkg.manifest.id, digest: pkg.digest,
|
|
103
|
+
environmentRef: `${taskId}/${preparationId}`, trust: "trusted-local", phase
|
|
104
|
+
});
|
|
105
|
+
const executionGrant = (taskId, params, path, plannedUses = new Map()) => store.listCapabilityGrants(taskId).find(candidate => Object.entries(params).every(([key, value]) => candidate.parameterBounds[key]?.includes(value))
|
|
106
|
+
&& (candidate.scope.taskId === undefined || candidate.scope.taskId === taskId)
|
|
107
|
+
&& candidate.scope.projectIds === undefined && candidate.scope.repositories === undefined
|
|
108
|
+
&& candidate.scope.packages === undefined
|
|
109
|
+
&& (candidate.scope.homePath === undefined || candidate.scope.homePath === path)
|
|
110
|
+
&& checkGrant({ ...candidate, usesUsed: candidate.usesUsed + (plannedUses.get(candidate.id) ?? 0) }, { action: "plugin.execute", params, irreversibility: "irreversible" }, new Date()).allowed);
|
|
111
|
+
const environmentAccess = (taskId, preparationId, via) => {
|
|
112
|
+
const prepared = store.getEnvironmentPreparation(taskId, preparationId);
|
|
113
|
+
if (!prepared)
|
|
114
|
+
throw new Error("Plugin environment preparation is missing.");
|
|
115
|
+
for (const resourceId of prepared.resourceRefs) {
|
|
116
|
+
const access = resources.assessLocalAccess(taskId, resourceId, prepared.access, via, prepared.id, true);
|
|
117
|
+
if (access.state !== "authorized")
|
|
118
|
+
return access;
|
|
119
|
+
}
|
|
120
|
+
return { state: "authorized" };
|
|
121
|
+
};
|
|
101
122
|
/** Each execution consumes one use. A live call's bound closure carries its
|
|
102
123
|
* admission; it is never resumed from a durable reservation or sent to code.
|
|
103
124
|
* Subsequent actions still check current authority, not cached permission. */
|
|
104
125
|
const execution = (taskId, preparationId, pkg, phase) => {
|
|
105
|
-
const params =
|
|
106
|
-
environmentRef: `${taskId}/${preparationId}`, trust: "trusted-local", phase };
|
|
126
|
+
const params = executionParams(taskId, preparationId, pkg, phase);
|
|
107
127
|
// This grants the capability to run code without OS effect confinement;
|
|
108
128
|
// it does not claim that every invocation actually has irreversible effects.
|
|
109
129
|
const request = { action: "plugin.execute", params, irreversibility: "irreversible" };
|
|
110
130
|
const reservation = phase === "call" ? undefined : `plugin/${randomUUID()}`;
|
|
111
131
|
const grantId = store.transaction((tx) => {
|
|
112
132
|
const env = environment(taskId, preparationId);
|
|
113
|
-
const grant =
|
|
114
|
-
&& (candidate.scope.taskId === undefined || candidate.scope.taskId === taskId)
|
|
115
|
-
&& candidate.scope.projectIds === undefined && candidate.scope.repositories === undefined
|
|
116
|
-
&& candidate.scope.packages === undefined
|
|
117
|
-
&& (candidate.scope.homePath === undefined || candidate.scope.homePath === env.directory.path)
|
|
118
|
-
&& checkGrant(candidate, request, new Date()).allowed);
|
|
133
|
+
const grant = executionGrant(taskId, params, env.directory.path);
|
|
119
134
|
if (!grant)
|
|
120
135
|
throw new Error(`Explicit trusted-local execution grant unavailable (${phase}); pluginId=${params.pluginId}, digest=${params.digest}, environmentRef=${params.environmentRef}.`);
|
|
121
136
|
tx.saveCapabilityGrant(taskId, recordGrantUse(grant, new Date(), reservation));
|
|
@@ -173,6 +188,81 @@ export function createPluginService(store, host, registry) {
|
|
|
173
188
|
}
|
|
174
189
|
};
|
|
175
190
|
return {
|
|
191
|
+
assess(taskId, descriptor, input, via) {
|
|
192
|
+
let validationId;
|
|
193
|
+
let phase;
|
|
194
|
+
let pkg;
|
|
195
|
+
let preparationId;
|
|
196
|
+
let path;
|
|
197
|
+
if (descriptor.name === "plugin.validate") {
|
|
198
|
+
if (input === undefined)
|
|
199
|
+
return undefined;
|
|
200
|
+
const params = input;
|
|
201
|
+
const prepared = store.getEnvironmentPreparation(taskId, params.preparationId);
|
|
202
|
+
if (!prepared?.directory)
|
|
203
|
+
throw new Error("Plugin validation needs an existing directory preparation.");
|
|
204
|
+
const access = environmentAccess(taskId, prepared.id, via);
|
|
205
|
+
if (access.state !== "authorized")
|
|
206
|
+
return access;
|
|
207
|
+
const directory = resolve(prepared.directory.path, params.directory);
|
|
208
|
+
packagePath(relative(prepared.directory.path, directory));
|
|
209
|
+
// Capture data only. Never run a build, prepare an environment or
|
|
210
|
+
// initialize a Provider just to assess the source's exact grant.
|
|
211
|
+
pkg = readPluginPackage(directory, false);
|
|
212
|
+
preparationId = prepared.id;
|
|
213
|
+
path = prepared.directory.path;
|
|
214
|
+
phase = "validate";
|
|
215
|
+
}
|
|
216
|
+
else if (descriptor.name === "plugin.activate") {
|
|
217
|
+
if (input === undefined)
|
|
218
|
+
return undefined;
|
|
219
|
+
validationId = input.validationId;
|
|
220
|
+
phase = "activate";
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
const selected = active.get(descriptor.provider.id);
|
|
224
|
+
if (!selected || selected.ref.generation !== descriptor.provider.generation)
|
|
225
|
+
return undefined;
|
|
226
|
+
validationId = selected.validationId;
|
|
227
|
+
phase = "call";
|
|
228
|
+
}
|
|
229
|
+
// Inspect captured authorization parameters, not Host availability,
|
|
230
|
+
// filesystem readiness or author code. Execution owns those checks.
|
|
231
|
+
if (validationId !== undefined) {
|
|
232
|
+
const report = store.getPluginValidation(taskId, validationId);
|
|
233
|
+
if (!report)
|
|
234
|
+
throw new Error("Plugin validation not found in this Task.");
|
|
235
|
+
pkg = report.package;
|
|
236
|
+
preparationId = report.preparationId;
|
|
237
|
+
path = report.environment.path;
|
|
238
|
+
}
|
|
239
|
+
if (phase !== "validate") {
|
|
240
|
+
const access = environmentAccess(taskId, preparationId, via);
|
|
241
|
+
if (access.state !== "authorized")
|
|
242
|
+
return access;
|
|
243
|
+
}
|
|
244
|
+
if (pkg.manifest.kind === "declarative")
|
|
245
|
+
return { state: "authorized" };
|
|
246
|
+
const params = executionParams(taskId, preparationId, pkg, phase);
|
|
247
|
+
const phases = phase === "validate" && pkg.manifest.build ? ["build", "validate"] : [phase];
|
|
248
|
+
// Simulate the same ordered selection as execution, without persisting
|
|
249
|
+
// uses. A broad Grant may fund multiple phases only if it has capacity.
|
|
250
|
+
const plannedUses = new Map();
|
|
251
|
+
const missing = phases.filter(value => {
|
|
252
|
+
const grant = executionGrant(taskId, { ...params, phase: value }, path, plannedUses);
|
|
253
|
+
if (!grant)
|
|
254
|
+
return true;
|
|
255
|
+
plannedUses.set(grant.id, (plannedUses.get(grant.id) ?? 0) + 1);
|
|
256
|
+
return false;
|
|
257
|
+
});
|
|
258
|
+
if (!missing.length)
|
|
259
|
+
return { state: "authorized" };
|
|
260
|
+
return { state: "requestable", request: {
|
|
261
|
+
action: "plugin.execute", taskId,
|
|
262
|
+
bounds: { ...Object.fromEntries(Object.entries(params).map(([key, value]) => [key, [value]])), phase: missing },
|
|
263
|
+
via, explanation: "Trusted-local author code needs an exact, irreversible execution Grant. Use the existing authorization source and Grant path; discovery consumes no uses."
|
|
264
|
+
} };
|
|
265
|
+
},
|
|
176
266
|
current: inspect,
|
|
177
267
|
list(taskId) {
|
|
178
268
|
return store.transaction(() => store.listPluginIntents(taskId)
|
|
@@ -228,7 +228,8 @@ async function runReleaseWorkflowLocked(store, taskId, workflowId, ports, option
|
|
|
228
228
|
if (grant === null) {
|
|
229
229
|
return finish(workflow, "unauthorized", "unauthorized:grant-missing", attempted);
|
|
230
230
|
}
|
|
231
|
-
const params = resolveParams(workflow, plan)
|
|
231
|
+
const params = grant.authorizationSource === undefined ? resolveParams(workflow, plan)
|
|
232
|
+
: Object.freeze({ ...resolveParams(workflow, plan), sourceCommit: workflow.source.commit });
|
|
232
233
|
const effectiveIrreversibility = effectiveStepIrreversibility(plan);
|
|
233
234
|
const decision = checkGrant(grant, {
|
|
234
235
|
action: grantAction(plan),
|
|
@@ -615,6 +616,12 @@ function cliUpdateVersionDenial(plan, params) {
|
|
|
615
616
|
function versionTagCheckoutDenial(grant, plan, params) {
|
|
616
617
|
if (plan.kind !== "version-tag")
|
|
617
618
|
return undefined;
|
|
619
|
+
// Leader authorization names a release version; the adapter pushes tag,
|
|
620
|
+
// not version. Validate that actual effect before consuming a grant use.
|
|
621
|
+
if (grant.authorizationSource !== undefined
|
|
622
|
+
&& (!params.version || ![params.version, `v${params.version}`].includes(params.tag ?? ""))) {
|
|
623
|
+
return "grant-version-tag-mismatch";
|
|
624
|
+
}
|
|
618
625
|
const repositories = grant.scope.repositories;
|
|
619
626
|
if (repositories === undefined || repositories.length === 0)
|
|
620
627
|
return undefined;
|
|
@@ -42,7 +42,7 @@ export function createProjectResources(store, now = () => new Date()) {
|
|
|
42
42
|
throw new Error(`Local resource not found: ${resourceId}.`);
|
|
43
43
|
return value;
|
|
44
44
|
};
|
|
45
|
-
const
|
|
45
|
+
const findGrant = (taskId, resourceId, access, reservation, reservedOnly = false) => {
|
|
46
46
|
const target = resource(resourceId);
|
|
47
47
|
const grant = store.listCapabilityGrants(taskId).find((candidate) => {
|
|
48
48
|
// A Project scope is visibility, not an exhaustive Resource grant. A
|
|
@@ -57,6 +57,10 @@ export function createProjectResources(store, now = () => new Date()) {
|
|
|
57
57
|
return false;
|
|
58
58
|
return checkGrant(candidate, { action: `resource.local.${access}`, params: { resourceId } }, now(), { skipUsesCheck: reservation !== undefined && candidate.useReservations.includes(reservation) }).allowed;
|
|
59
59
|
});
|
|
60
|
+
return grant;
|
|
61
|
+
};
|
|
62
|
+
const grantFor = (taskId, resourceId, access, reservation, reservedOnly = false) => {
|
|
63
|
+
const grant = findGrant(taskId, resourceId, access, reservation, reservedOnly);
|
|
60
64
|
if (!grant)
|
|
61
65
|
throw new Error(`Resource grant unavailable: ${resourceId}/${access}.`);
|
|
62
66
|
return grant;
|
|
@@ -109,6 +113,24 @@ export function createProjectResources(store, now = () => new Date()) {
|
|
|
109
113
|
});
|
|
110
114
|
};
|
|
111
115
|
return {
|
|
116
|
+
assessLocalAccess(taskId, resourceId, access, via, reservation, reservedOnly = false) {
|
|
117
|
+
const current = task(taskId);
|
|
118
|
+
// Only already-discoverable references may receive request guidance.
|
|
119
|
+
// Do not resolve an arbitrary global ID merely to confirm its existence.
|
|
120
|
+
const known = current.projectBindings.some(({ projectId }) => store.getProject(projectId)?.resourceRefs?.includes(resourceId))
|
|
121
|
+
|| store.listCapabilityGrants(taskId).some(grant => grant.parameterBounds.resourceId?.includes(resourceId))
|
|
122
|
+
|| store.listEnvironmentPreparations(taskId).some(value => value.resourceRefs.includes(resourceId))
|
|
123
|
+
|| store.listEvents(taskId).some(event => event.type === "resource.local.registered" && event.payload.resourceId === resourceId);
|
|
124
|
+
if (!known)
|
|
125
|
+
return { state: "hidden" };
|
|
126
|
+
if (findGrant(taskId, resourceId, access, reservation, reservedOnly))
|
|
127
|
+
return { state: "authorized" };
|
|
128
|
+
return { state: "requestable", request: {
|
|
129
|
+
action: `resource.local.${access}`, taskId, bounds: { resourceId: [resourceId] }, via,
|
|
130
|
+
explanation: "Request this exact resource and access through existing Message/InputRequest and Grant authority. A response alone is not a Grant."
|
|
131
|
+
+ (reservedOnly ? " This environment's adoption reservation is no longer valid; inspect preparation/adoption before reusing it." : "")
|
|
132
|
+
} };
|
|
133
|
+
},
|
|
112
134
|
resolveExecutionEnvironment,
|
|
113
135
|
bindEnvironment(taskId, roleName, preparationId, source = { source: "environment.bind" }) {
|
|
114
136
|
return store.transaction((tx) => {
|
|
@@ -30,8 +30,8 @@ export function requireManagedGlobalCaller(store, env) {
|
|
|
30
30
|
*/
|
|
31
31
|
export class ManagedRuntimeDriftError extends Error {
|
|
32
32
|
name = "ManagedRuntimeDriftError";
|
|
33
|
-
constructor(message) {
|
|
34
|
-
super(message);
|
|
33
|
+
constructor(message, options) {
|
|
34
|
+
super(message, options);
|
|
35
35
|
}
|
|
36
36
|
}
|
|
37
37
|
/** Reads the process's own immutable managed identity, or undefined when unmanaged. */
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** Presence is not authentication. Even partial transport identity must not
|
|
2
|
+
* silently acquire the authority of an unbound public caller. */
|
|
3
|
+
export function hasManagedIdentity(environment) {
|
|
4
|
+
return ["YUI_SESSION_SCOPE", "YUI_ROLE", "YUI_AGENT_ID", "YUI_NATIVE_SESSION_ID",
|
|
5
|
+
"YUI_TASK_ID", "YUI_SESSION_MANIFEST"].some(key => environment[key] !== undefined);
|
|
6
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { resolve } from "node:path";
|
|
2
|
+
import { StorageSchemaError } from "../storage/storageSchema.js";
|
|
2
3
|
import { isStorageVersion, compareStorageVersions } from "../storage/storageVersions.js";
|
|
3
4
|
import { callController as defaultCallController, ControllerClientError } from "../core/controllerClient.js";
|
|
4
5
|
import { inspectStorageSchema } from "../storage/storageSchema.js";
|
|
@@ -8,10 +9,12 @@ export async function assertRuntimeCoherence(input, options = {}) {
|
|
|
8
9
|
const identity = validateVersionIdentity(options.identity ?? yuiVersionIdentity());
|
|
9
10
|
const storage = (options.inspectStorage ?? inspectStorageSchema)(home);
|
|
10
11
|
if (storage.status !== "current") {
|
|
11
|
-
throw new
|
|
12
|
+
throw new StorageSchemaError(storage.status === "uninitialized" ? "STORAGE_UNINITIALIZED"
|
|
13
|
+
: storage.status === "unsupported" ? "STORAGE_SCHEMA_UNSUPPORTED" : "STORAGE_SCHEMA_INVALID", `Managed control-plane storage is not current: ${storage.status}.`
|
|
14
|
+
+ ("detail" in storage && typeof storage.detail === "string" ? ` ${storage.detail}` : ""));
|
|
12
15
|
}
|
|
13
16
|
if (storage.currentVersion !== identity.storageVersion) {
|
|
14
|
-
throw new
|
|
17
|
+
throw new StorageSchemaError("STORAGE_SCHEMA_UNSUPPORTED", "Managed control-plane storage version is incompatible "
|
|
15
18
|
+ `(expected ${identity.storageVersion}, found `
|
|
16
19
|
+ `${storage.currentVersion ?? "unknown"}).`);
|
|
17
20
|
}
|
|
@@ -37,7 +40,7 @@ export async function assertRuntimeCoherence(input, options = {}) {
|
|
|
37
40
|
}
|
|
38
41
|
export function assertControllerStatusIdentity(status, expected = yuiVersionIdentity()) {
|
|
39
42
|
if (!isRecord(status) || status.running !== true) {
|
|
40
|
-
throw new
|
|
43
|
+
throw new ControllerClientError("INVALID_RESPONSE", "Controller status does not describe a running Controller.");
|
|
41
44
|
}
|
|
42
45
|
assertControllerField(status.protocolVersion, expected.controllerProtocolVersion, "protocol");
|
|
43
46
|
assertControllerField(status.version, expected.version, "version");
|
|
@@ -66,20 +69,19 @@ function validateVersionIdentity(value) {
|
|
|
66
69
|
}
|
|
67
70
|
function assertControllerContinuityIdentity(status, expected) {
|
|
68
71
|
if (!isRecord(status) || status.running !== true) {
|
|
69
|
-
throw new
|
|
72
|
+
throw new ControllerClientError("INVALID_RESPONSE", "Controller status does not describe a running Controller.");
|
|
70
73
|
}
|
|
71
74
|
if (typeof status.version !== "string" || status.version.trim().length === 0) {
|
|
72
|
-
throw new
|
|
75
|
+
throw new ControllerClientError("CONTROLLER_PROTOCOL_MISMATCH", "Controller version is invalid at the managed continuity gate.");
|
|
73
76
|
}
|
|
74
77
|
assertControllerField(status.protocolVersion, expected.controllerProtocolVersion, "protocol");
|
|
75
78
|
assertControllerField(status.storageVersion, expected.storageVersion, "storage version");
|
|
76
79
|
}
|
|
77
80
|
function assertControllerField(actual, expected, label) {
|
|
78
81
|
if (actual !== expected) {
|
|
79
|
-
throw new
|
|
82
|
+
throw new ControllerClientError("CONTROLLER_PROTOCOL_MISMATCH", `Controller ${label} is incompatible with the exact control plane `
|
|
80
83
|
+ `(expected ${expected}, found ${typeof actual === "string" || typeof actual === "number" ? actual : "unknown"}). `
|
|
81
|
-
+ "
|
|
82
|
-
+ "before writing new Task records.");
|
|
84
|
+
+ "Verify Home, CLI and Controller identity before an authorized restart through the matching invocation.");
|
|
83
85
|
}
|
|
84
86
|
}
|
|
85
87
|
function requireText(value, label) {
|
|
@@ -24,6 +24,16 @@ const MINOR_UPGRADES = Object.freeze([{
|
|
|
24
24
|
targetChecksum: CURRENT_SCHEMA_CHECKSUM,
|
|
25
25
|
sql: "",
|
|
26
26
|
dataMigration: "task-main-workspace"
|
|
27
|
+
}, {
|
|
28
|
+
fromVersion: "1.1",
|
|
29
|
+
toVersion: "1.2",
|
|
30
|
+
name: "task-authorization-source",
|
|
31
|
+
introducedIn: "next",
|
|
32
|
+
sourceChecksum: CURRENT_SCHEMA_CHECKSUM,
|
|
33
|
+
targetChecksum: CURRENT_SCHEMA_CHECKSUM,
|
|
34
|
+
// New grants can carry source evidence. Valid historical Operator grants
|
|
35
|
+
// remain unchanged; migration must not invent authorization provenance.
|
|
36
|
+
sql: ""
|
|
27
37
|
}]);
|
|
28
38
|
export function storageMinorUpgradePlan(from) {
|
|
29
39
|
if (from === CURRENT_STORAGE_VERSION)
|
|
@@ -180,6 +180,8 @@ export function storedCapabilityGrant(value) {
|
|
|
180
180
|
fields.push("revokedAt");
|
|
181
181
|
if (grant.revokedBy !== undefined)
|
|
182
182
|
fields.push("revokedBy");
|
|
183
|
+
if (grant.authorizationSource !== undefined)
|
|
184
|
+
fields.push("authorizationSource");
|
|
183
185
|
exact(grant, fields, "Capability grant");
|
|
184
186
|
requireRecordIdentity(grant.id, "Capability grant id");
|
|
185
187
|
requireRecordIdentity(grant.taskId, "Capability grant Task id");
|
|
@@ -303,6 +305,7 @@ export function isValidCapabilityGrantTransition(existing, candidate) {
|
|
|
303
305
|
const immutable = candidate.id === existing.id
|
|
304
306
|
&& candidate.taskId === existing.taskId
|
|
305
307
|
&& candidate.granter === existing.granter
|
|
308
|
+
&& isDeepStrictEqual(candidate.authorizationSource, existing.authorizationSource)
|
|
306
309
|
&& isDeepStrictEqual(candidate.scope, existing.scope)
|
|
307
310
|
&& isDeepStrictEqual(candidate.actions, existing.actions)
|
|
308
311
|
&& isDeepStrictEqual(candidate.parameterBounds, existing.parameterBounds)
|
|
@@ -10,11 +10,14 @@ export class SurfaceContributions {
|
|
|
10
10
|
return this.registry.search(context).map((entry) => ({
|
|
11
11
|
...reference(entry), name: entry.surfaces?.cli?.name ?? entry.name,
|
|
12
12
|
help: entry.surfaces?.cli?.help ?? entry.summary, effect: entry.effect,
|
|
13
|
+
...(entry.access === undefined ? {} : { access: entry.access }),
|
|
13
14
|
...(entry.unavailable === undefined ? {} : { unavailable: entry.unavailable })
|
|
14
15
|
}));
|
|
15
16
|
}
|
|
16
17
|
listPanels(context) {
|
|
17
18
|
return this.registry.search(context).flatMap((entry) => {
|
|
19
|
+
if (entry.access?.state === "requestable")
|
|
20
|
+
return [];
|
|
18
21
|
const panel = entry.surfaces?.panel
|
|
19
22
|
?? (entry.effect === "query" ? { kind: "data", title: entry.summary, renderer: "json" } : undefined);
|
|
20
23
|
return panel ? [{
|