@zq-silk/yui 2.0.0 → 2.2.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 +4 -0
- package/dist/cli/commandCatalog.js +318 -289
- package/dist/cli/commandDiscovery.js +72 -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.js +23 -3030
- package/dist/commands/capabilityCommands.js +5 -5
- package/dist/commands/globalRoleCommands.js +2 -1
- package/dist/commands/taskCommands.js +90 -21
- package/dist/commands/taskIntegrationCommands.js +3 -1
- package/dist/commands/taskUpstreamCommands.js +3 -1
- package/dist/context/runContextPack.js +132 -5
- package/dist/context/sourceRunContext.js +4 -2
- package/dist/context/taskContext.js +12 -3
- package/dist/controlPlaneCli.js +3092 -0
- package/dist/controller/fileSchedulerStoreAdapter.js +19 -6
- package/dist/controller/jobControl.js +21 -0
- package/dist/executor/agentExecutor.js +1 -1
- package/dist/executor/effectiveLaunch.js +18 -5
- package/dist/executor/fileRoleLaunchPlanner.js +15 -19
- package/dist/integration/gitIntegrationService.js +50 -15
- package/dist/kernel/accessAssessment.js +1 -0
- package/dist/kernel/builtinCapabilities.js +28 -1
- package/dist/kernel/capabilityRegistry.js +64 -19
- package/dist/message/messageContinuation.js +7 -2
- package/dist/nativeAgent/agent.js +275 -0
- package/dist/nativeAgent/cliDemo.js +37 -0
- package/dist/nativeAgent/codingTools.js +11 -0
- package/dist/nativeAgent/commandTool.js +215 -0
- package/dist/nativeAgent/compactionDemo.js +158 -0
- package/dist/nativeAgent/composition.js +71 -0
- package/dist/nativeAgent/context/budget.js +44 -0
- package/dist/nativeAgent/context/index.js +339 -0
- package/dist/nativeAgent/context/providerCompressor.js +103 -0
- package/dist/nativeAgent/contracts.js +1 -0
- package/dist/nativeAgent/demo.js +40 -0
- package/dist/nativeAgent/evaluation/cases.js +38 -0
- package/dist/nativeAgent/evaluation/checks.js +91 -0
- package/dist/nativeAgent/evaluation/demo.js +19 -0
- package/dist/nativeAgent/evaluation/files.js +54 -0
- package/dist/nativeAgent/evaluation/fixture.js +36 -0
- package/dist/nativeAgent/evaluation/index.js +239 -0
- package/dist/nativeAgent/executionOwner.js +209 -0
- package/dist/nativeAgent/filePatterns.js +170 -0
- package/dist/nativeAgent/fileToolsSupport.js +202 -0
- package/dist/nativeAgent/index.js +16 -0
- package/dist/nativeAgent/interaction/cli.js +358 -0
- package/dist/nativeAgent/interaction/contracts.js +1 -0
- package/dist/nativeAgent/interaction/index.js +3 -0
- package/dist/nativeAgent/interaction/memoryDemo.js +115 -0
- package/dist/nativeAgent/interaction/renderer.js +34 -0
- package/dist/nativeAgent/localSafety.js +258 -0
- package/dist/nativeAgent/mockProvider.js +43 -0
- package/dist/nativeAgent/model/anthropicMessages.js +204 -0
- package/dist/nativeAgent/model/chatCompletions.js +210 -0
- package/dist/nativeAgent/model/errors.js +47 -0
- package/dist/nativeAgent/model/gateway.js +423 -0
- package/dist/nativeAgent/model/index.js +7 -0
- package/dist/nativeAgent/model/observationAdapter.js +21 -0
- package/dist/nativeAgent/model/protocols.js +19 -0
- package/dist/nativeAgent/model/responses.js +263 -0
- package/dist/nativeAgent/model/types.js +1 -0
- package/dist/nativeAgent/model/wire.js +73 -0
- package/dist/nativeAgent/observability/index.js +220 -0
- package/dist/nativeAgent/product/catalog.js +82 -0
- package/dist/nativeAgent/product/config.js +295 -0
- package/dist/nativeAgent/product/facts.js +30 -0
- package/dist/nativeAgent/product/index.js +62 -0
- package/dist/nativeAgent/product/location.js +44 -0
- package/dist/nativeAgent/product/runtime.js +276 -0
- package/dist/nativeAgent/product/storage.js +49 -0
- package/dist/nativeAgent/product/tools.js +47 -0
- package/dist/nativeAgent/product/transport.js +54 -0
- package/dist/nativeAgent/projectGuidance/index.js +425 -0
- package/dist/nativeAgent/searchTools.js +305 -0
- package/dist/nativeAgent/session/backends.js +293 -0
- package/dist/nativeAgent/session/catalog.js +97 -0
- package/dist/nativeAgent/session/catalogDemo.js +87 -0
- package/dist/nativeAgent/session/contracts.js +1 -0
- package/dist/nativeAgent/session/format.js +269 -0
- package/dist/nativeAgent/session/index.js +5 -0
- package/dist/nativeAgent/session/location.js +36 -0
- package/dist/nativeAgent/session/sqliteFormat.js +134 -0
- package/dist/nativeAgent/session/store.js +248 -0
- package/dist/nativeAgent/textTools.js +293 -0
- package/dist/nativeAgent/toolManager/executor.js +290 -0
- package/dist/nativeAgent/toolManager/index.js +4 -0
- package/dist/nativeAgent/validation.js +95 -0
- package/dist/plugins/pluginService.js +98 -8
- package/dist/resources/projectResourceService.js +23 -1
- package/dist/runtime/managedIdentity.js +6 -0
- package/dist/surface/surfaceContributions.js +3 -0
- package/dist/task/taskAuthority.js +61 -10
- package/dist/web/assets/assetManifest.js +38 -22
- package/dist/web/assets/client/api.js +115 -0
- package/dist/web/assets/client/app.js +615 -862
- 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 +1024 -791
- package/dist/web/assets/client/overview.js +118 -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 +146 -0
- package/dist/web/assets/client/theme.js +61 -22
- package/dist/web/assets/icons.js +39 -0
- package/dist/web/assets/shell.js +125 -118
- package/dist/web/assets/styles/base.js +41 -0
- package/dist/web/assets/styles/components.js +169 -0
- package/dist/web/assets/styles/layout.js +56 -77
- package/dist/web/assets/styles/markdown.js +16 -24
- package/dist/web/assets/styles/responsive.js +41 -44
- package/dist/web/assets/styles/tokens.js +65 -89
- package/dist/web/assets/styles/views.js +292 -0
- package/dist/web/webServer.js +2 -1
- package/docs/agent-result-consumption.md +16 -0
- package/docs/agent-result-consumption.zh-CN.md +13 -0
- package/docs/examples/agent-offline.mjs +194 -0
- package/docs/native-agent.md +283 -0
- package/docs/release-workflow.md +61 -0
- package/docs/release-workflow.zh-CN.md +46 -0
- package/docs/roles-and-configuration.md +32 -0
- package/docs/roles-and-configuration.zh-CN.md +26 -0
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +9 -0
- package/skills/yui-reviewer/SKILL.md +5 -0
- package/skills/yui-runtime/SKILL.md +33 -0
- 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,290 @@
|
|
|
1
|
+
// Match the existing model/tool wire budgets; all sizes are UTF-8 JSON bytes.
|
|
2
|
+
const messageBytes = 512 * 1024;
|
|
3
|
+
const argumentBytes = 64 * 1024;
|
|
4
|
+
const callsPerBatch = 8;
|
|
5
|
+
const object = (value) => value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
6
|
+
&& Object.getPrototypeOf(value) === Object.prototype;
|
|
7
|
+
const nonempty = (value) => typeof value === 'string' && value.trim().length > 0;
|
|
8
|
+
const identifier = (value) => nonempty(value) && Buffer.byteLength(value, 'utf8') <= 1024;
|
|
9
|
+
const bytes = (value) => Buffer.byteLength(JSON.stringify(value), 'utf8');
|
|
10
|
+
function json(value, depth = 0) {
|
|
11
|
+
if (depth > 32)
|
|
12
|
+
return false;
|
|
13
|
+
if (value === null || typeof value === 'string' || typeof value === 'boolean')
|
|
14
|
+
return true;
|
|
15
|
+
if (typeof value === 'number')
|
|
16
|
+
return Number.isFinite(value);
|
|
17
|
+
if (Array.isArray(value))
|
|
18
|
+
return Array.from(value).every(v => json(v, depth + 1));
|
|
19
|
+
return object(value) && Object.values(value).every(v => json(v, depth + 1));
|
|
20
|
+
}
|
|
21
|
+
function freeze(value) {
|
|
22
|
+
if (value && typeof value === 'object') {
|
|
23
|
+
for (const child of Object.values(value))
|
|
24
|
+
freeze(child);
|
|
25
|
+
Object.freeze(value);
|
|
26
|
+
}
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
function snapshot(value, limit) {
|
|
30
|
+
const copy = structuredClone(value);
|
|
31
|
+
if (!json(copy) || bytes(copy) > limit)
|
|
32
|
+
throw new Error('Invalid or oversized JSON');
|
|
33
|
+
return freeze(copy);
|
|
34
|
+
}
|
|
35
|
+
function failure(code, message, effect = 'none') {
|
|
36
|
+
return { ok: false, error: { code, message, effect } };
|
|
37
|
+
}
|
|
38
|
+
function outcome(raw, identity, beforeStart = false) {
|
|
39
|
+
try {
|
|
40
|
+
const value = snapshot(raw, messageBytes);
|
|
41
|
+
if (!object(value))
|
|
42
|
+
throw new Error();
|
|
43
|
+
let normalized;
|
|
44
|
+
if (!beforeStart && value.ok === true && typeof value.content === 'string') {
|
|
45
|
+
normalized = { ok: true, content: value.content };
|
|
46
|
+
}
|
|
47
|
+
else if (value.ok === false && object(value.error) && nonempty(value.error.code)
|
|
48
|
+
&& typeof value.error.message === 'string'
|
|
49
|
+
&& (value.error.effect === 'none' || (!beforeStart && value.error.effect === 'unknown'))) {
|
|
50
|
+
normalized = { ok: false, error: {
|
|
51
|
+
code: value.error.code, message: value.error.message, effect: value.error.effect,
|
|
52
|
+
} };
|
|
53
|
+
}
|
|
54
|
+
else
|
|
55
|
+
throw new Error();
|
|
56
|
+
// Guarantee the consumer can append the paired message without truncation.
|
|
57
|
+
if (bytes({ role: 'tool', toolCallId: identity.toolCallId, name: identity.name, outcome: normalized }) > messageBytes) {
|
|
58
|
+
throw new Error();
|
|
59
|
+
}
|
|
60
|
+
return normalized;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return failure('tool_protocol', 'Invalid or oversized tool result', beforeStart ? 'none' : 'unknown');
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
function status(started, value) {
|
|
67
|
+
if (!started)
|
|
68
|
+
return 'not_executed';
|
|
69
|
+
if (value.ok)
|
|
70
|
+
return 'succeeded';
|
|
71
|
+
if (value.error.effect === 'unknown')
|
|
72
|
+
return 'unknown';
|
|
73
|
+
return value.error.code === 'cancelled' ? 'cancelled' : 'failed';
|
|
74
|
+
}
|
|
75
|
+
export function buildExecutor(options) {
|
|
76
|
+
if (!options?.environment || typeof options.environment.acquire !== 'function') {
|
|
77
|
+
throw new Error('Tool environment.acquire capability is required');
|
|
78
|
+
}
|
|
79
|
+
if (!options.permission || typeof options.permission.check !== 'function') {
|
|
80
|
+
throw new Error('Tool permission.check capability is required');
|
|
81
|
+
}
|
|
82
|
+
if (!Array.isArray(options.tools))
|
|
83
|
+
throw new Error('Tool collection is required');
|
|
84
|
+
const acquire = options.environment.acquire.bind(options.environment);
|
|
85
|
+
const check = options.permission.check.bind(options.permission);
|
|
86
|
+
const registry = new Map();
|
|
87
|
+
for (const tool of options.tools) {
|
|
88
|
+
let definition;
|
|
89
|
+
try {
|
|
90
|
+
definition = snapshot(tool.definition, messageBytes);
|
|
91
|
+
if (!identifier(definition.name) || typeof definition.description !== 'string'
|
|
92
|
+
|| !object(definition.inputSchema) || typeof tool.validate !== 'function'
|
|
93
|
+
|| typeof tool.execute !== 'function')
|
|
94
|
+
throw new Error();
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
throw new Error('Invalid tool declaration or implementation');
|
|
98
|
+
}
|
|
99
|
+
if (registry.has(definition.name))
|
|
100
|
+
throw new Error('Tool names must be unique');
|
|
101
|
+
registry.set(definition.name, {
|
|
102
|
+
definition, validate: tool.validate.bind(tool), execute: tool.execute.bind(tool),
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
const definitions = snapshot([...registry.values()].map(t => t.definition), messageBytes);
|
|
106
|
+
return Object.freeze({
|
|
107
|
+
definitions,
|
|
108
|
+
async executeBatch(request) {
|
|
109
|
+
// Admit the whole immutable batch before acquiring any resource.
|
|
110
|
+
let scope, calls;
|
|
111
|
+
try {
|
|
112
|
+
scope = snapshot(request.scope, 4096);
|
|
113
|
+
calls = snapshot(request.calls, messageBytes);
|
|
114
|
+
if (!identifier(scope.sessionId) || !identifier(scope.turnId) || !Number.isSafeInteger(scope.step)
|
|
115
|
+
|| scope.step < 1 || !Array.isArray(calls) || !calls.length || calls.length > callsPerBatch
|
|
116
|
+
|| calls.some(c => !object(c) || !identifier(c.id) || !identifier(c.name)
|
|
117
|
+
|| !json(c.arguments) || bytes(c.arguments) > argumentBytes)
|
|
118
|
+
|| new Set(calls.map(c => c.id)).size !== calls.length)
|
|
119
|
+
throw new Error();
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
throw new Error('Invalid tool batch: bounded JSON, scope and unique call identities required');
|
|
123
|
+
}
|
|
124
|
+
// Missing recording/cancellation capabilities cannot silently become no-ops.
|
|
125
|
+
const signal = request.signal;
|
|
126
|
+
const beforeExecute = request.beforeExecute;
|
|
127
|
+
const afterExecute = request.afterExecute;
|
|
128
|
+
if (!signal || typeof signal.aborted !== 'boolean' || typeof signal.addEventListener !== 'function'
|
|
129
|
+
|| typeof beforeExecute !== 'function' || typeof afterExecute !== 'function') {
|
|
130
|
+
throw new Error('Tool batch signal, beforeExecute and afterExecute capabilities are required');
|
|
131
|
+
}
|
|
132
|
+
const results = [];
|
|
133
|
+
let stopped = null;
|
|
134
|
+
let recordingError;
|
|
135
|
+
for (const call of calls) {
|
|
136
|
+
const identity = freeze({ ...scope, toolCallId: call.id, name: call.name });
|
|
137
|
+
let started = false;
|
|
138
|
+
let cleanup = { status: 'not_acquired' };
|
|
139
|
+
let value = failure('not_started', 'Tool was not started');
|
|
140
|
+
const cancelled = () => {
|
|
141
|
+
if (!signal.aborted)
|
|
142
|
+
return false;
|
|
143
|
+
stopped ??= 'cancelled';
|
|
144
|
+
value = failure('cancelled_before_start', 'Cancelled before tool execution');
|
|
145
|
+
return true;
|
|
146
|
+
};
|
|
147
|
+
const attempt = async () => {
|
|
148
|
+
if (cancelled())
|
|
149
|
+
return;
|
|
150
|
+
const tool = registry.get(call.name);
|
|
151
|
+
if (!tool) {
|
|
152
|
+
value = failure('unknown_tool', 'Unknown tool');
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
try {
|
|
156
|
+
const invalid = tool.validate(call.arguments);
|
|
157
|
+
if (invalid !== null) {
|
|
158
|
+
value = outcome({ ok: false, error: invalid }, identity, true);
|
|
159
|
+
if (!value.ok && value.error.code === 'tool_protocol')
|
|
160
|
+
stopped = 'capability_failed';
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
value = failure('tool_validation', 'Tool validation capability failed');
|
|
166
|
+
stopped = 'capability_failed';
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
if (cancelled())
|
|
170
|
+
return;
|
|
171
|
+
const invocation = freeze({ identity, call, definition: tool.definition });
|
|
172
|
+
let lease;
|
|
173
|
+
try {
|
|
174
|
+
lease = await acquire(invocation, signal);
|
|
175
|
+
}
|
|
176
|
+
catch {
|
|
177
|
+
value = failure('environment_failed', 'Environment acquisition failed');
|
|
178
|
+
cleanup = { status: 'acquire_failed', error: {
|
|
179
|
+
code: 'acquire_failed', message: 'Environment provider owns partial-resource cleanup; no lease was returned',
|
|
180
|
+
} };
|
|
181
|
+
stopped = 'capability_failed';
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
// A malformed lease cannot prove acquired resources were released.
|
|
185
|
+
let release;
|
|
186
|
+
try {
|
|
187
|
+
if (!lease || typeof lease.release !== 'function')
|
|
188
|
+
throw new Error();
|
|
189
|
+
release = lease.release.bind(lease);
|
|
190
|
+
}
|
|
191
|
+
catch {
|
|
192
|
+
value = failure('environment_protocol', 'Environment returned an invalid resource lease');
|
|
193
|
+
cleanup = { status: 'failed', error: {
|
|
194
|
+
code: 'invalid_lease', message: 'No release capability; resource effects unknown',
|
|
195
|
+
} };
|
|
196
|
+
stopped = 'cleanup_failed';
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
try {
|
|
200
|
+
// Use the same environment for authorization and execution, even if
|
|
201
|
+
// the lease implementation exposes its value through an accessor.
|
|
202
|
+
let environment;
|
|
203
|
+
try {
|
|
204
|
+
environment = lease.value;
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
value = failure('environment_failed', 'Environment value is unavailable');
|
|
208
|
+
stopped = 'capability_failed';
|
|
209
|
+
return;
|
|
210
|
+
}
|
|
211
|
+
if (cancelled())
|
|
212
|
+
return;
|
|
213
|
+
try {
|
|
214
|
+
await beforeExecute(identity);
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
value = failure('record_failed', 'Required execution-intent recording failed');
|
|
218
|
+
stopped = 'capability_failed';
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (cancelled())
|
|
222
|
+
return;
|
|
223
|
+
// Permission is the last awaited boundary before tool invocation.
|
|
224
|
+
let decision;
|
|
225
|
+
try {
|
|
226
|
+
decision = snapshot(await check(invocation, environment, signal), 4096);
|
|
227
|
+
if (!object(decision) || !(decision.allowed === true
|
|
228
|
+
|| (decision.allowed === false && typeof decision.reason === 'string')))
|
|
229
|
+
throw new Error();
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
value = failure('permission_failed', 'Permission capability failed or returned an invalid decision');
|
|
233
|
+
stopped = 'capability_failed';
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (cancelled())
|
|
237
|
+
return;
|
|
238
|
+
if (!decision.allowed) {
|
|
239
|
+
value = failure('permission_denied', decision.reason);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
started = true;
|
|
243
|
+
try {
|
|
244
|
+
value = outcome(await tool.execute(call.arguments, identity, signal, environment), identity);
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
value = failure('tool_exception', 'Tool threw; actual effects are unknown', 'unknown');
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
finally {
|
|
251
|
+
try {
|
|
252
|
+
await release();
|
|
253
|
+
cleanup = { status: 'released' };
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
cleanup = { status: 'failed', error: { code: 'release_failed', message: 'Resource release failed; inspect this invocation' } };
|
|
257
|
+
stopped = 'cleanup_failed';
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
if (!stopped)
|
|
262
|
+
await attempt();
|
|
263
|
+
else if (stopped === 'cancelled')
|
|
264
|
+
cancelled();
|
|
265
|
+
if (!value.ok && value.error.effect === 'unknown')
|
|
266
|
+
stopped = 'unknown_effect';
|
|
267
|
+
if (signal.aborted)
|
|
268
|
+
stopped ??= 'cancelled';
|
|
269
|
+
const settlement = freeze({
|
|
270
|
+
identity, started, status: status(started, value), outcome: value,
|
|
271
|
+
cancellationRequested: signal.aborted, cleanup,
|
|
272
|
+
});
|
|
273
|
+
results.push(settlement);
|
|
274
|
+
if (!recordingError) {
|
|
275
|
+
try {
|
|
276
|
+
await afterExecute(settlement);
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
recordingError = {
|
|
280
|
+
identity, code: 'result_record_failed',
|
|
281
|
+
message: 'Result recording failed; write effect unknown. Reconcile using in-memory receipts',
|
|
282
|
+
};
|
|
283
|
+
stopped ??= 'capability_failed';
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return freeze({ results, stopped, ...(recordingError ? { recordingError } : {}) });
|
|
288
|
+
},
|
|
289
|
+
});
|
|
290
|
+
}
|
|
@@ -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, maxBytes = limits.historyBytes) {
|
|
61
|
+
const pending = new Map();
|
|
62
|
+
const used = new Set();
|
|
63
|
+
if (!Array.isArray(value) || !json(value) || size(value) > maxBytes) {
|
|
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
|
+
}
|
|
@@ -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)
|
|
@@ -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) => {
|
|
@@ -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
|
+
}
|
|
@@ -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 ? [{
|