@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
|
@@ -267,7 +267,8 @@ export class FileSchedulerStoreAdapter {
|
|
|
267
267
|
default:
|
|
268
268
|
outcome = "obsolete";
|
|
269
269
|
}
|
|
270
|
-
if (outcome === "applied" && !isRunTerminalObservation(input)
|
|
270
|
+
if (outcome === "applied" && !isRunTerminalObservation(input)
|
|
271
|
+
&& input.kind !== "goal.updated" && input.kind !== "goal.cleared") {
|
|
271
272
|
this.persistRuntimeObservation(input, now);
|
|
272
273
|
}
|
|
273
274
|
return outcome;
|
|
@@ -700,7 +701,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
700
701
|
return "applied";
|
|
701
702
|
});
|
|
702
703
|
}
|
|
703
|
-
persistRuntimeObservation(input, now) {
|
|
704
|
+
persistRuntimeObservation(input, now, notifyGoal = true) {
|
|
704
705
|
this.store.transaction((store) => {
|
|
705
706
|
const taskId = input.fence.taskId;
|
|
706
707
|
const events = store.listEvents(taskId);
|
|
@@ -762,8 +763,7 @@ export class FileSchedulerStoreAdapter {
|
|
|
762
763
|
? "provider-continuation-report"
|
|
763
764
|
: "provider-continuation-settled", now);
|
|
764
765
|
}
|
|
765
|
-
if ((input.kind === "goal.cleared"
|
|
766
|
-
|| (input.kind === "goal.updated" && input.payload.goalStatus !== "active"))
|
|
766
|
+
if (notifyGoal && (input.kind === "goal.cleared" || input.kind === "goal.updated")
|
|
767
767
|
&& input.fence.roleName !== "leader") {
|
|
768
768
|
routeRoleEvent(store, observationEvent, input.fence.roleName, input.kind === "goal.cleared"
|
|
769
769
|
? "provider-goal-cleared"
|
|
@@ -2363,6 +2363,8 @@ export class FileSchedulerStoreAdapter {
|
|
|
2363
2363
|
observeProviderGoal(input, now) {
|
|
2364
2364
|
return this.store.transaction((store) => {
|
|
2365
2365
|
const taskId = input.fence.taskId;
|
|
2366
|
+
if (hasPersistedRuntimeObservation(store.listEvents(taskId), input))
|
|
2367
|
+
return "applied";
|
|
2366
2368
|
const sessions = store.getTaskRoleSessionSet(taskId, input.fence.roleName);
|
|
2367
2369
|
const binding = sessions?.providerBinding;
|
|
2368
2370
|
if (sessions === null || sessions === undefined || binding === null || binding === undefined
|
|
@@ -2383,7 +2385,16 @@ export class FileSchedulerStoreAdapter {
|
|
|
2383
2385
|
? {}
|
|
2384
2386
|
: { tokenBudget: input.payload.goalTokenBudget })
|
|
2385
2387
|
});
|
|
2388
|
+
// Startup/re-attachment observes current state, not necessarily a change.
|
|
2389
|
+
// Compare under the same transaction that persists and routes the fact.
|
|
2390
|
+
if (isDeepStrictEqual(binding.goal, updated.goal)) {
|
|
2391
|
+
// Retain the existing dedup evidence: an old empty observation replayed
|
|
2392
|
+
// after a later real update must not clear that newer state.
|
|
2393
|
+
this.persistRuntimeObservation(input, now, false);
|
|
2394
|
+
return "applied";
|
|
2395
|
+
}
|
|
2386
2396
|
store.saveTaskRoleSessionSet(updateTaskRoleProviderRuntime(sessions, updated, now));
|
|
2397
|
+
this.persistRuntimeObservation(input, now);
|
|
2387
2398
|
return "applied";
|
|
2388
2399
|
});
|
|
2389
2400
|
}
|
|
@@ -3127,14 +3138,16 @@ function mapRole(store, role) {
|
|
|
3127
3138
|
? resolveEffectiveLaunch({
|
|
3128
3139
|
role,
|
|
3129
3140
|
purpose,
|
|
3130
|
-
...(workspace === undefined ? {} : { workspace })
|
|
3141
|
+
...(workspace === undefined ? {} : { workspace }),
|
|
3142
|
+
...(item === null ? {} : { workItemWriteProjectIds: item.writeProjectIds })
|
|
3131
3143
|
})
|
|
3132
3144
|
: liveSession !== null && workspace?.owner.type === "task"
|
|
3133
3145
|
? effectiveLaunchWithTaskMainWorkspace(liveSession.effective, workspace)
|
|
3134
3146
|
: liveSession?.effective ?? resolveEffectiveLaunch({
|
|
3135
3147
|
role,
|
|
3136
3148
|
purpose,
|
|
3137
|
-
...(workspace === undefined ? {} : { workspace })
|
|
3149
|
+
...(workspace === undefined ? {} : { workspace }),
|
|
3150
|
+
...(item === null ? {} : { workItemWriteProjectIds: item.writeProjectIds })
|
|
3138
3151
|
});
|
|
3139
3152
|
return {
|
|
3140
3153
|
taskId: role.taskId,
|
|
@@ -19,6 +19,7 @@ import { requireManagedTaskCaller, ManagedRuntimeDriftError } from "../runtime/m
|
|
|
19
19
|
import { CliError } from "../errors/cliError.js";
|
|
20
20
|
import { assertJobAssignmentScope, JobAssignmentScopeError } from "../job/jobAssignmentScope.js";
|
|
21
21
|
import { assertContextRecordReadable } from "../context/taskContext.js";
|
|
22
|
+
import { assertTaskProjectWriteAuthority } from "../task/taskAuthority.js";
|
|
22
23
|
export function createDurableJobControl(store) {
|
|
23
24
|
const authority = createJobCallAuthority(store);
|
|
24
25
|
return {
|
|
@@ -167,8 +168,27 @@ export function authorizeJobStart(store, job) {
|
|
|
167
168
|
throw jobDomainError("Job caller binding was revoked; no execution was started.");
|
|
168
169
|
}
|
|
169
170
|
requireJobAssignment(store, job, { scope, role });
|
|
171
|
+
assertLeaderJobWriteScope(store, job, { scope, role });
|
|
170
172
|
validateJobTarget(store, job);
|
|
171
173
|
}
|
|
174
|
+
function assertLeaderJobWriteScope(store, target, caller) {
|
|
175
|
+
if (caller.scope !== "task" || caller.role !== "leader")
|
|
176
|
+
return;
|
|
177
|
+
if (target.owner.kind !== "task" && target.owner.kind !== "integration-attempt") {
|
|
178
|
+
throw jobControlError("UNAUTHORIZED", "Direct Leader delivery does not grant another Assignment or Review workspace. Use the owning executor for its Job.");
|
|
179
|
+
}
|
|
180
|
+
const { taskId, projectId } = target;
|
|
181
|
+
const session = activeLiveRoleAgentSession(store.getTaskRoleSessionSet(taskId, caller.role));
|
|
182
|
+
try {
|
|
183
|
+
assertTaskProjectWriteAuthority(store, {
|
|
184
|
+
YUI_SESSION_SCOPE: "task", YUI_TASK_ID: taskId, YUI_ROLE: caller.role,
|
|
185
|
+
YUI_NATIVE_SESSION_ID: caller.nativeSessionId ?? session?.nativeSessionId
|
|
186
|
+
}, taskId, projectId);
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
throw jobControlError("UNAUTHORIZED", error instanceof Error ? error.message : String(error));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
172
192
|
function requireJobAssignment(store, target, caller) {
|
|
173
193
|
try {
|
|
174
194
|
assertJobAssignmentScope(store, target, caller);
|
|
@@ -244,6 +264,7 @@ function assertNonSecretJobInput(params) {
|
|
|
244
264
|
function validateStartParams(store, params) {
|
|
245
265
|
validateJobTarget(store, params);
|
|
246
266
|
assertCallerAuthorized(store, params.caller, params.taskId);
|
|
267
|
+
assertLeaderJobWriteScope(store, params, params.caller);
|
|
247
268
|
if (params.caller.scope === "task") {
|
|
248
269
|
const session = store.getTaskRoleSessionSet(params.taskId, params.caller.role);
|
|
249
270
|
if (session?.sessions[session.activeAgentId]?.effective.executionAuthority !== "delivery") {
|
|
@@ -317,7 +317,7 @@ export function roleAgentSessionResumeMode(set, agentId, desired) {
|
|
|
317
317
|
if (roleSessionMayContinue(session.effective, desired))
|
|
318
318
|
return "resume";
|
|
319
319
|
throw new Error(`Role Agent session cannot continue under the next launch: ${agentId}. `
|
|
320
|
-
+ "Its Agent, adapter
|
|
320
|
+
+ "Its Agent, adapter, physical workspace or captured authority changed. Explicitly select a new Session "
|
|
321
321
|
+ "after resolving existing execution and resource ownership.");
|
|
322
322
|
}
|
|
323
323
|
export function bindTaskRoleProviderRuntime(set, binding, updatedAt) {
|
|
@@ -117,8 +117,8 @@ export function sameEffectiveLaunch(existing, desired) {
|
|
|
117
117
|
*
|
|
118
118
|
* Only facts that make continuation impossible participate: the Session
|
|
119
119
|
* protocol, the provider identity that owns the conversation, and the physical
|
|
120
|
-
* workspace the Session runs
|
|
121
|
-
* permission, Role context,
|
|
120
|
+
* workspace and captured authority the Session runs with. Launch configuration
|
|
121
|
+
* such as model, effort, provider permission, Role context, and AgentRun-scoped facts like
|
|
122
122
|
* ReviewRound identity or candidate commits shape the next Host process
|
|
123
123
|
* instead of ending the Session; that divergence is acknowledged where the
|
|
124
124
|
* configuration changes and stays visible as launch provenance.
|
|
@@ -151,6 +151,8 @@ function sessionContinuitySnapshot(snapshot) {
|
|
|
151
151
|
return {
|
|
152
152
|
schemaVersion: snapshot.schemaVersion,
|
|
153
153
|
executionAuthority: snapshot.executionAuthority,
|
|
154
|
+
profileAccess: snapshot.profileAccess,
|
|
155
|
+
writeProjectIds: snapshot.writeProjectIds,
|
|
154
156
|
contextProtocolVersion: snapshot.contextProtocolVersion,
|
|
155
157
|
agentId: snapshot.agentId,
|
|
156
158
|
// Two products on one connection plan are two different conversations. A
|
|
@@ -329,6 +331,8 @@ function effectiveLaunchConfigUnchecked(snapshot) {
|
|
|
329
331
|
: claudeConfigFromSnapshot(snapshot);
|
|
330
332
|
}
|
|
331
333
|
function effectiveWriteProjects(input, workspace) {
|
|
334
|
+
if (input.purpose === "planning" || input.executionAuthority === "planning")
|
|
335
|
+
return [];
|
|
332
336
|
if (input.purpose === "review") {
|
|
333
337
|
if (!("taskId" in input.role)) {
|
|
334
338
|
throw new Error("Review launch requires a Task Role.");
|
|
@@ -358,9 +362,18 @@ function effectiveWriteProjects(input, workspace) {
|
|
|
358
362
|
.map(({ projectId }) => projectId), "Workspace writable Project");
|
|
359
363
|
if (!("taskId" in input.role))
|
|
360
364
|
return [];
|
|
361
|
-
//
|
|
362
|
-
//
|
|
363
|
-
|
|
365
|
+
// Direct delivery belongs to the Leader in this Task's adopted main
|
|
366
|
+
// workspace. Assignment scope (including explicit []) still takes priority.
|
|
367
|
+
// Never infer authority from a path, a different owner's workspace or a
|
|
368
|
+
// desired Profile change to an already-frozen Session.
|
|
369
|
+
if (input.workItemWriteProjectIds === undefined) {
|
|
370
|
+
return input.role.name === "leader"
|
|
371
|
+
&& input.role.defaultAccess === "write"
|
|
372
|
+
&& input.workspace?.owner.type === "task"
|
|
373
|
+
&& input.workspace.owner.taskId === input.role.taskId
|
|
374
|
+
? workspaceWrite : [];
|
|
375
|
+
}
|
|
376
|
+
if (input.role.defaultAccess !== "write")
|
|
364
377
|
return [];
|
|
365
378
|
const requested = uniqueIdentities(input.workItemWriteProjectIds, "WorkItem writable Project");
|
|
366
379
|
if (requested.length === 0)
|
|
@@ -216,7 +216,7 @@ export class FileRoleLaunchPlanner {
|
|
|
216
216
|
// attempt was rejected, cancelled, or completed.
|
|
217
217
|
assertProviderConversationReplaceable(sessionSet.providerBinding);
|
|
218
218
|
}
|
|
219
|
-
return this.#compile(role, input, { scope: "task", taskId: task.id }, resolveTaskRoleSessionTitle(input.mode === "resume" ? existing?.title : undefined, task, role.name), input.mode === "resume" && compatibleExisting ? existing.nativeSessionId : undefined,
|
|
219
|
+
return this.#compile(role, input, { scope: "task", taskId: task.id }, resolveTaskRoleSessionTitle(input.mode === "resume" ? existing?.title : undefined, task, role.name), input.mode === "resume" && compatibleExisting ? existing.nativeSessionId : undefined, effective, {
|
|
220
220
|
purpose
|
|
221
221
|
});
|
|
222
222
|
}
|
|
@@ -246,9 +246,9 @@ export class FileRoleLaunchPlanner {
|
|
|
246
246
|
}
|
|
247
247
|
return this.#compile(role, input, { scope: "global" }, input.mode === "new" && role.name === "operator"
|
|
248
248
|
? operatorSessionTitle(this.#now(), this.store.getConfig().timeZone)
|
|
249
|
-
: undefined, input.mode === "resume" && compatibleExisting ? existing.nativeSessionId : undefined,
|
|
249
|
+
: undefined, input.mode === "resume" && compatibleExisting ? existing.nativeSessionId : undefined, effective, { purpose: "execution" });
|
|
250
250
|
}
|
|
251
|
-
#compile(role, input, owner, sessionTitle, knownNativeSessionId,
|
|
251
|
+
#compile(role, input, owner, sessionTitle, knownNativeSessionId, effective, sessionPolicy) {
|
|
252
252
|
const launchRole = effectiveRoleForLaunch(role, effective);
|
|
253
253
|
const binding = activeRoleAgentBinding(launchRole);
|
|
254
254
|
if (binding.agentId !== input.agentId || binding.adapterId !== input.adapterId) {
|
|
@@ -580,7 +580,7 @@ export class FileRoleLaunchPlanner {
|
|
|
580
580
|
childLifecycle: driver.capabilities.lifecycle.providerProcess
|
|
581
581
|
};
|
|
582
582
|
const scopedLaunch = owner.scope === "task"
|
|
583
|
-
? this.#applyWorkspaceScope(
|
|
583
|
+
? this.#applyWorkspaceScope(launch, effective)
|
|
584
584
|
: launch;
|
|
585
585
|
const ordinaryConversationLaunch = withCodexThreadEnvironment(scopedLaunch);
|
|
586
586
|
return {
|
|
@@ -640,20 +640,11 @@ export class FileRoleLaunchPlanner {
|
|
|
640
640
|
}
|
|
641
641
|
return { attemptId: run.attemptId, turnId: run.nativeTurnId };
|
|
642
642
|
}
|
|
643
|
-
#applyWorkspaceScope(
|
|
644
|
-
const workspace = workspaceOverride
|
|
645
|
-
?? (role.name === "leader"
|
|
646
|
-
? this.store.getTaskWorkspace(taskId)
|
|
647
|
-
: this.store.listWorkItems(taskId)
|
|
648
|
-
.find((item) => item.assignee === role.name && item.status === "open") === undefined
|
|
649
|
-
? this.store.getTaskWorkspace(taskId)
|
|
650
|
-
: this.store.getWorkItemWorkspace(taskId, this.store.listWorkItems(taskId).find((item) => item.assignee === role.name && item.status === "open").id));
|
|
651
|
-
if (workspace === null || workspace === undefined)
|
|
652
|
-
return launch;
|
|
643
|
+
#applyWorkspaceScope(launch, effective) {
|
|
653
644
|
return {
|
|
654
645
|
...launch,
|
|
655
646
|
env: {
|
|
656
|
-
...workspaceScopeEnvironment(launch.env,
|
|
647
|
+
...workspaceScopeEnvironment(launch.env, effective)
|
|
657
648
|
}
|
|
658
649
|
};
|
|
659
650
|
}
|
|
@@ -711,16 +702,21 @@ function resolveTaskRoleEffectiveLaunch(store, role, purpose = "execution") {
|
|
|
711
702
|
...(item === null ? {} : { workItemWriteProjectIds: item.writeProjectIds })
|
|
712
703
|
});
|
|
713
704
|
}
|
|
714
|
-
function workspaceScopeEnvironment(environment,
|
|
705
|
+
function workspaceScopeEnvironment(environment, effective) {
|
|
706
|
+
const entries = effective.workspace.entries.map(entry => ({
|
|
707
|
+
...entry,
|
|
708
|
+
access: entry.access === "write" && effective.writeProjectIds.includes(entry.projectId)
|
|
709
|
+
? "write" : "read"
|
|
710
|
+
}));
|
|
715
711
|
return {
|
|
716
712
|
...environment,
|
|
717
|
-
YUI_WRITABLE_PROJECT_IDS: JSON.stringify(
|
|
713
|
+
YUI_WRITABLE_PROJECT_IDS: JSON.stringify(entries
|
|
718
714
|
.filter(({ access }) => access === "write")
|
|
719
715
|
.map(({ projectId }) => projectId)),
|
|
720
|
-
YUI_CONTEXT_PROJECT_IDS: JSON.stringify(
|
|
716
|
+
YUI_CONTEXT_PROJECT_IDS: JSON.stringify(entries
|
|
721
717
|
.filter(({ access }) => access === "read")
|
|
722
718
|
.map(({ projectId }) => projectId)),
|
|
723
|
-
YUI_WORKSPACE_PROJECTS: JSON.stringify(Object.fromEntries(
|
|
719
|
+
YUI_WORKSPACE_PROJECTS: JSON.stringify(Object.fromEntries(entries.map(({ projectId, directory, access, path }) => [
|
|
724
720
|
projectId,
|
|
725
721
|
{ directory, access, path }
|
|
726
722
|
])))
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execFile, spawn } from "node:child_process";
|
|
2
|
-
import { lstat, mkdir, open, rm, stat } from "node:fs/promises";
|
|
2
|
+
import { lstat, mkdir, open, realpath, rm, stat } from "node:fs/promises";
|
|
3
3
|
import { dirname, join, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { promisify } from "node:util";
|
|
5
5
|
import { selectEnvironment } from "../agent/launchEnvironment.js";
|
|
@@ -14,6 +14,7 @@ import { ResourceRegistrar } from "../resources/resourceRegistrar.js";
|
|
|
14
14
|
import { FileTaskRuntimeIsolation } from "../runtime/taskRuntimeIsolation.js";
|
|
15
15
|
import { integrationTmuxSocketRoot, managedIntegrationRuntimeRoot } from "../storage/homeLayout.js";
|
|
16
16
|
import { advanceTaskProjectCommit } from "../task/task.js";
|
|
17
|
+
import { taskIntegrationSettlementCheckout, taskIntegrationTargetCheckout } from "../task/taskAuthority.js";
|
|
17
18
|
import { yuiTmuxServerName } from "../tmux/tmuxManager.js";
|
|
18
19
|
import { recordGateArtifactReuse } from "../verification/gateArtifact.js";
|
|
19
20
|
import { touchGateArtifact } from "../verification/gateArtifactStore.js";
|
|
@@ -75,8 +76,12 @@ export class GitIntegrationService {
|
|
|
75
76
|
#resourceRegistrar() {
|
|
76
77
|
return this.#resourceRegistrarValue ??= new ResourceRegistrar(this.home, this.now);
|
|
77
78
|
}
|
|
79
|
+
#targetCheckout(attempt) {
|
|
80
|
+
return taskIntegrationTargetCheckout(this.store, this.environment, attempt.taskId, attempt.projectId, attempt.targetRef);
|
|
81
|
+
}
|
|
78
82
|
async integrate(taskId, integrationId, signal) {
|
|
79
83
|
let initial = requireIntegration(this.store, taskId, integrationId);
|
|
84
|
+
this.#targetCheckout(initial);
|
|
80
85
|
if (!["running", "conflicted", "blocked", "validating"].includes(initial.status)) {
|
|
81
86
|
throw new Error(`Integration cannot continue from ${initial.status}.`);
|
|
82
87
|
}
|
|
@@ -95,6 +100,7 @@ export class GitIntegrationService {
|
|
|
95
100
|
throw new Error("Integration changed before execution admission; read its current record.");
|
|
96
101
|
}
|
|
97
102
|
initial = currentUnderFence;
|
|
103
|
+
this.#targetCheckout(initial);
|
|
98
104
|
const task = this.store.getTask(initial.taskId);
|
|
99
105
|
if (task === null || !task.projectBindings.some(({ projectId }) => projectId === initial.projectId)) {
|
|
100
106
|
throw new Error(`Integration Task Project is unavailable: ${initial.taskId}.`);
|
|
@@ -116,6 +122,7 @@ export class GitIntegrationService {
|
|
|
116
122
|
|| taskRepository === undefined) {
|
|
117
123
|
throw new Error(`Integration Task clone is unavailable: ${task.id}/${project.id}.`);
|
|
118
124
|
}
|
|
125
|
+
await assertTargetCheckout(taskRepository, initial.targetRef, this.#targetCheckout(initial));
|
|
119
126
|
// Issue 08: a Project with a VerificationPlan gates through its plan
|
|
120
127
|
// (bootstrap + L2) and reuses exact-SHA artifacts; an unconfigured
|
|
121
128
|
// Project keeps the existing explicit check path unchanged.
|
|
@@ -193,7 +200,7 @@ export class GitIntegrationService {
|
|
|
193
200
|
// moved or its worktree is dirty, so the check commands never run on a
|
|
194
201
|
// target that cannot be advanced. advanceTargetRef re-verifies both
|
|
195
202
|
// after the checks, so a move during the gate is still fenced at CAS.
|
|
196
|
-
await assertTargetReadyForChecks(taskRepository, current.targetRef, current.beforeCommit);
|
|
203
|
+
await assertTargetReadyForChecks(taskRepository, current.targetRef, current.beforeCommit, this.#targetCheckout(current));
|
|
197
204
|
if (gate !== undefined) {
|
|
198
205
|
return await this.#runVerificationGate(current, workspace, prepared.path, managedWorkspace, taskRepository, gate);
|
|
199
206
|
}
|
|
@@ -221,7 +228,7 @@ export class GitIntegrationService {
|
|
|
221
228
|
checks: checkResults
|
|
222
229
|
}, this.now());
|
|
223
230
|
this.store.saveIntegrationAttempt(task.id, current);
|
|
224
|
-
await advanceTargetRef(taskRepository, current.targetRef, candidateCommit, current.beforeCommit);
|
|
231
|
+
await advanceTargetRef(taskRepository, current.targetRef, candidateCommit, current.beforeCommit, this.#targetCheckout(current));
|
|
225
232
|
const committed = this.#recordCommitted(current);
|
|
226
233
|
return this.#terminalResult("committed", committed, workspace);
|
|
227
234
|
}
|
|
@@ -234,7 +241,7 @@ export class GitIntegrationService {
|
|
|
234
241
|
if (target === current.candidateCommit) {
|
|
235
242
|
try {
|
|
236
243
|
await assertIntegrationCandidate(workspace.path, current.candidateCommit, workspace.branch);
|
|
237
|
-
await assertTargetReadyForChecks(taskRepository, current.targetRef, current.candidateCommit);
|
|
244
|
+
await assertTargetReadyForChecks(taskRepository, current.targetRef, current.candidateCommit, this.#targetCheckout(current));
|
|
238
245
|
const committed = this.#recordCommitted(current);
|
|
239
246
|
return this.#terminalResult("committed", committed, workspace);
|
|
240
247
|
}
|
|
@@ -307,8 +314,21 @@ export class GitIntegrationService {
|
|
|
307
314
|
}
|
|
308
315
|
const target = await resolveRef(mainEntry.path, attempt.targetRef);
|
|
309
316
|
if (target === attempt.candidateCommit && target !== attempt.beforeCommit) {
|
|
310
|
-
|
|
311
|
-
|
|
317
|
+
if (attempt.checks.some(check => check.outcome === "failed")
|
|
318
|
+
|| attempt.jobId !== undefined
|
|
319
|
+
&& jobs.find(job => job.id === attempt.jobId).status !== "succeeded") {
|
|
320
|
+
throw new Error("Delivered Integration lacks successful candidate check evidence; inspect before settlement.");
|
|
321
|
+
}
|
|
322
|
+
await assertIntegrationCandidate(workspace.root, attempt.candidateCommit, workspaceEntry.branch);
|
|
323
|
+
assertRecordedSourceCandidate(attempt, { path: workspace.root, branch: workspaceEntry.branch });
|
|
324
|
+
await assertTargetReadyForChecks(mainEntry.path, attempt.targetRef, target, taskIntegrationSettlementCheckout(this.store, this.environment, taskId, attempt.projectId, attempt.targetRef));
|
|
325
|
+
return this.#recordCommitted(attempt, (tx, id) => {
|
|
326
|
+
authorize(tx, id);
|
|
327
|
+
taskIntegrationSettlementCheckout(tx, this.environment, id, attempt.projectId, attempt.targetRef);
|
|
328
|
+
if (JSON.stringify(requireIntegration(tx, id, integrationId)) !== JSON.stringify(attempt)) {
|
|
329
|
+
throw new Error("Integration changed before delivered settlement; inspect its current record.");
|
|
330
|
+
}
|
|
331
|
+
});
|
|
312
332
|
}
|
|
313
333
|
if (target !== attempt.beforeCommit) {
|
|
314
334
|
throw new Error(`Target moved to ${target}; cannot prove this Integration is unadvanced. Inspect before settlement.`);
|
|
@@ -419,7 +439,7 @@ export class GitIntegrationService {
|
|
|
419
439
|
throw new Error(`Integration check admission requires a running, proven candidate: ${attempt.id}.`);
|
|
420
440
|
}
|
|
421
441
|
await assertIntegrationCandidate(path, attempt.candidateCommit, workspace.branch);
|
|
422
|
-
await assertTargetReadyForChecks(repositoryPath, attempt.targetRef, attempt.beforeCommit);
|
|
442
|
+
await assertTargetReadyForChecks(repositoryPath, attempt.targetRef, attempt.beforeCommit, this.#targetCheckout(attempt));
|
|
423
443
|
const { runtime, head, steps, environment, inputDigest } = await this.#checkSpecification(attempt, managedWorkspace, gate);
|
|
424
444
|
if (attempt.checkInputDigest !== undefined && attempt.checkInputDigest !== inputDigest) {
|
|
425
445
|
throw new Error("Integration check conditions changed since admission; inspect the original Job, then abort or start a new attempt.");
|
|
@@ -479,7 +499,7 @@ export class GitIntegrationService {
|
|
|
479
499
|
const job = await this.jobPort.getJob(attempt.taskId, attempt.jobId);
|
|
480
500
|
assertCheckJobIdentity(attempt, job, path);
|
|
481
501
|
await assertIntegrationCandidate(path, job.head, workspace.branch);
|
|
482
|
-
await assertTargetReadyForChecks(repositoryPath, attempt.targetRef, attempt.beforeCommit);
|
|
502
|
+
await assertTargetReadyForChecks(repositoryPath, attempt.targetRef, attempt.beforeCommit, this.#targetCheckout(attempt));
|
|
483
503
|
if (gate?.planDigest !== attempt.gatePlanDigest
|
|
484
504
|
|| gate?.toolchainDigest !== attempt.gateToolchainDigest) {
|
|
485
505
|
throw new Error("Integration verification plan/toolchain changed; the original Job cannot prove the current gate.");
|
|
@@ -642,7 +662,7 @@ export class GitIntegrationService {
|
|
|
642
662
|
checks
|
|
643
663
|
}, this.now());
|
|
644
664
|
this.store.saveIntegrationAttempt(attempt.taskId, validating);
|
|
645
|
-
await advanceTargetRef(repositoryPath, validating.targetRef, candidateCommit, validating.beforeCommit);
|
|
665
|
+
await advanceTargetRef(repositoryPath, validating.targetRef, candidateCommit, validating.beforeCommit, this.#targetCheckout(validating));
|
|
646
666
|
const committed = this.#recordCommitted(validating);
|
|
647
667
|
return this.#terminalResult("committed", committed, workspace);
|
|
648
668
|
}
|
|
@@ -709,9 +729,9 @@ export class GitIntegrationService {
|
|
|
709
729
|
}
|
|
710
730
|
assertCheckJobIdentity(attempt, job, workspace.path);
|
|
711
731
|
}
|
|
712
|
-
await advanceTargetRef(repositoryPath, attempt.targetRef, attempt.candidateCommit, attempt.beforeCommit);
|
|
732
|
+
await advanceTargetRef(repositoryPath, attempt.targetRef, attempt.candidateCommit, attempt.beforeCommit, this.#targetCheckout(attempt));
|
|
713
733
|
}
|
|
714
|
-
await assertTargetReadyForChecks(repositoryPath, attempt.targetRef, attempt.candidateCommit);
|
|
734
|
+
await assertTargetReadyForChecks(repositoryPath, attempt.targetRef, attempt.candidateCommit, this.#targetCheckout(attempt));
|
|
715
735
|
const committed = this.#recordCommitted(attempt);
|
|
716
736
|
return this.#terminalResult("committed", committed, workspace);
|
|
717
737
|
}
|
|
@@ -1138,12 +1158,12 @@ async function resolveRef(repositoryPath, ref) {
|
|
|
1138
1158
|
* {@link advanceTargetRef} re-verifies both before the CAS, so a target that
|
|
1139
1159
|
* moves during the gate is still fenced.
|
|
1140
1160
|
*/
|
|
1141
|
-
async function assertTargetReadyForChecks(repositoryPath, targetRef, expectedHead) {
|
|
1161
|
+
async function assertTargetReadyForChecks(repositoryPath, targetRef, expectedHead, expectedCheckout) {
|
|
1142
1162
|
const current = await resolveRef(repositoryPath, targetRef);
|
|
1143
1163
|
if (current !== expectedHead) {
|
|
1144
1164
|
throw new Error(`Target moved to ${current}; expected ${expectedHead}.`);
|
|
1145
1165
|
}
|
|
1146
|
-
const checkedOutPaths = await
|
|
1166
|
+
const checkedOutPaths = await assertTargetCheckout(repositoryPath, targetRef, expectedCheckout);
|
|
1147
1167
|
if (checkedOutPaths.length > 1) {
|
|
1148
1168
|
throw new Error(`Integration target is checked out in multiple worktrees: ${targetRef}.`);
|
|
1149
1169
|
}
|
|
@@ -1156,9 +1176,18 @@ async function assertTargetReadyForChecks(repositoryPath, targetRef, expectedHea
|
|
|
1156
1176
|
}
|
|
1157
1177
|
}
|
|
1158
1178
|
}
|
|
1159
|
-
async function
|
|
1179
|
+
async function assertTargetCheckout(repositoryPath, targetRef, expectedCheckout) {
|
|
1180
|
+
const paths = await checkedOutWorktreePaths(repositoryPath, fullTargetRef(targetRef));
|
|
1181
|
+
if (expectedCheckout !== undefined && (await realpath(repositoryPath) !== await realpath(expectedCheckout)
|
|
1182
|
+
|| paths.length !== 1 || await realpath(paths[0]) !== await realpath(expectedCheckout)
|
|
1183
|
+
|| (await git(["-C", expectedCheckout, "symbolic-ref", "HEAD"])).trim() !== fullTargetRef(targetRef))) {
|
|
1184
|
+
throw new Error("Managed Leader Integration target must be physically checked out in its captured Task-main workspace.");
|
|
1185
|
+
}
|
|
1186
|
+
return paths;
|
|
1187
|
+
}
|
|
1188
|
+
async function advanceTargetRef(repositoryPath, targetRef, candidateCommit, expectedHead, expectedCheckout) {
|
|
1160
1189
|
const ref = fullTargetRef(targetRef);
|
|
1161
|
-
const checkedOutPaths = await
|
|
1190
|
+
const checkedOutPaths = await assertTargetCheckout(repositoryPath, targetRef, expectedCheckout);
|
|
1162
1191
|
if (checkedOutPaths.length === 0) {
|
|
1163
1192
|
await git([
|
|
1164
1193
|
"-C", repositoryPath, "update-ref",
|
|
@@ -1182,6 +1211,9 @@ async function advanceTargetRef(repositoryPath, targetRef, candidateCommit, expe
|
|
|
1182
1211
|
if (current !== expectedHead) {
|
|
1183
1212
|
throw new Error(`Target moved to ${current}; expected ${expectedHead}.`);
|
|
1184
1213
|
}
|
|
1214
|
+
if (expectedCheckout !== undefined) {
|
|
1215
|
+
await assertTargetCheckout(repositoryPath, targetRef, expectedCheckout);
|
|
1216
|
+
}
|
|
1185
1217
|
// The candidate may be a rebased upstream result and therefore need not be
|
|
1186
1218
|
// a descendant of the old Task head. Advance the branch with a real
|
|
1187
1219
|
// compare-and-swap, then update only this checkout's index and worktree.
|
|
@@ -1193,6 +1225,9 @@ async function advanceTargetRef(repositoryPath, targetRef, candidateCommit, expe
|
|
|
1193
1225
|
candidateCommit,
|
|
1194
1226
|
expectedHead
|
|
1195
1227
|
]);
|
|
1228
|
+
if (expectedCheckout !== undefined) {
|
|
1229
|
+
await assertTargetCheckout(repositoryPath, targetRef, expectedCheckout);
|
|
1230
|
+
}
|
|
1196
1231
|
await git(["-C", checkout, "read-tree", "--reset", "-u", candidateCommit]);
|
|
1197
1232
|
const advanced = await resolveRef(repositoryPath, targetRef);
|
|
1198
1233
|
if (advanced !== candidateCommit) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -452,7 +452,34 @@ export function createBuiltinCapabilities(host, store, jobs, signal = () => unde
|
|
|
452
452
|
throw new Error(`Permission unavailable: ${permission}.`);
|
|
453
453
|
}
|
|
454
454
|
return { taskIds: [task.id], projectIds: task.projectBindings.map((binding) => binding.projectId) };
|
|
455
|
-
}, BUILTIN_CAPABILITIES)
|
|
455
|
+
}, BUILTIN_CAPABILITIES, (context, descriptor, input) => {
|
|
456
|
+
const caller = current(context);
|
|
457
|
+
const params = input;
|
|
458
|
+
const via = caller.scope === "global" ? "operator"
|
|
459
|
+
: caller.role === "leader" ? "leader-input" : "leader-message";
|
|
460
|
+
const forCaller = (access) => caller.scope === "user" && access.state === "requestable" ? { state: "hidden" } : access;
|
|
461
|
+
const pluginAccess = plugins.assess(context.targetId, descriptor, input, via);
|
|
462
|
+
if (pluginAccess)
|
|
463
|
+
return forCaller(pluginAccess);
|
|
464
|
+
if (input === undefined)
|
|
465
|
+
return undefined;
|
|
466
|
+
if (descriptor.name === "resource.local.read") {
|
|
467
|
+
return forCaller(resources.assessLocalAccess(context.targetId, params.resourceId, "read", via));
|
|
468
|
+
}
|
|
469
|
+
if (descriptor.name === "environment.prepare") {
|
|
470
|
+
const plan = params.plan;
|
|
471
|
+
return plan.kind === "local"
|
|
472
|
+
? forCaller(resources.assessLocalAccess(context.targetId, plan.resourceId, plan.access, via))
|
|
473
|
+
: { state: "authorized" };
|
|
474
|
+
}
|
|
475
|
+
if (descriptor.name === "task.read" || descriptor.name === "config.read"
|
|
476
|
+
|| descriptor.name === "resource.workspaces" || descriptor.name === "environment.list") {
|
|
477
|
+
return { state: "authorized" };
|
|
478
|
+
}
|
|
479
|
+
// Other domain operations retain their exact execution checks. Do not
|
|
480
|
+
// claim target authorization from a descriptor's coarse permission alone.
|
|
481
|
+
return undefined;
|
|
482
|
+
});
|
|
456
483
|
const plugins = createPluginService(store, host, registry);
|
|
457
484
|
return {
|
|
458
485
|
registry,
|
|
@@ -17,11 +17,13 @@ const reserved = new Set(["yui", "task", "context", "config", "job", "resource",
|
|
|
17
17
|
export class CapabilityRegistry {
|
|
18
18
|
host;
|
|
19
19
|
authorize;
|
|
20
|
+
assess;
|
|
20
21
|
#descriptors = [];
|
|
21
22
|
#coreProviders = new Set();
|
|
22
|
-
constructor(host, authorize, builtins = []) {
|
|
23
|
+
constructor(host, authorize, builtins = [], assess) {
|
|
23
24
|
this.host = host;
|
|
24
25
|
this.authorize = authorize;
|
|
26
|
+
this.assess = assess;
|
|
25
27
|
this.#publish(builtins, true);
|
|
26
28
|
builtins.forEach((entry) => this.#coreProviders.add(entry.provider.id));
|
|
27
29
|
}
|
|
@@ -38,7 +40,7 @@ export class CapabilityRegistry {
|
|
|
38
40
|
if (!prepared.length)
|
|
39
41
|
throw new Error("Plugin must contribute capabilities.");
|
|
40
42
|
const available = [
|
|
41
|
-
...this
|
|
43
|
+
...this.#authorized(context).filter((entry) => entry.provider.id !== prepared[0].provider.id),
|
|
42
44
|
...prepared
|
|
43
45
|
];
|
|
44
46
|
const check = (entry, path) => {
|
|
@@ -47,7 +49,8 @@ export class CapabilityRegistry {
|
|
|
47
49
|
throw new Error("Required capability dependency cycle.");
|
|
48
50
|
for (const dependency of entry.required ?? []) {
|
|
49
51
|
const matches = available.filter((candidate) => candidate.name === dependency.name
|
|
50
|
-
&& candidate.contractVersion === dependency.contractVersion && candidate.unavailable === undefined
|
|
52
|
+
&& candidate.contractVersion === dependency.contractVersion && candidate.unavailable === undefined
|
|
53
|
+
&& (prepared.includes(candidate) || this.host.isAvailable(candidate.provider)));
|
|
51
54
|
if (matches.length !== 1)
|
|
52
55
|
throw new Error(`Required capability is missing or ambiguous: ${dependency.name}.`);
|
|
53
56
|
check(matches[0], new Set([...path, entry]));
|
|
@@ -58,9 +61,9 @@ export class CapabilityRegistry {
|
|
|
58
61
|
disable(providerId) {
|
|
59
62
|
this.#descriptors = this.#descriptors.filter((entry) => entry.provider.id !== providerId);
|
|
60
63
|
}
|
|
61
|
-
|
|
64
|
+
#authorized(context) {
|
|
62
65
|
const visibility = this.authorize(context);
|
|
63
|
-
|
|
66
|
+
return this.#descriptors.filter((entry) => visible(entry.scope, visibility))
|
|
64
67
|
.filter((entry) => {
|
|
65
68
|
try {
|
|
66
69
|
this.authorize(context, entry);
|
|
@@ -70,25 +73,28 @@ export class CapabilityRegistry {
|
|
|
70
73
|
return false;
|
|
71
74
|
}
|
|
72
75
|
});
|
|
73
|
-
return authorized.filter((entry) => `${entry.name} ${entry.summary}`.toLowerCase().includes(query.toLowerCase()))
|
|
74
|
-
.map((entry) => {
|
|
75
|
-
const unavailable = this.#unavailable(entry, authorized, new Set());
|
|
76
|
-
return unavailable === undefined ? entry : Object.freeze({ ...entry, unavailable });
|
|
77
|
-
});
|
|
78
76
|
}
|
|
79
|
-
|
|
77
|
+
search(context, query = "") {
|
|
78
|
+
// Descriptor discovery must not acquire or inspect runtime Providers. These
|
|
79
|
+
// are templates: without input, no exact-target authorization is asserted.
|
|
80
|
+
return this.#authorized(context).filter((entry) => `${entry.name} ${entry.summary}`.toLowerCase().includes(query.toLowerCase()))
|
|
81
|
+
.map(entry => {
|
|
82
|
+
const access = this.assess?.(context, entry, undefined);
|
|
83
|
+
return access ? { ...entry, access } : entry;
|
|
84
|
+
}).filter(entry => !("access" in entry) || entry.access?.state !== "hidden");
|
|
85
|
+
}
|
|
86
|
+
#select(context, request, execution) {
|
|
80
87
|
try {
|
|
81
|
-
const
|
|
88
|
+
const authorized = this.#authorized(context);
|
|
89
|
+
const candidates = authorized.filter((entry) => entry.name === request.name
|
|
82
90
|
&& (request.contractVersion === undefined || request.contractVersion === entry.contractVersion)
|
|
83
91
|
&& (request.providerId === undefined || request.providerId === entry.provider.id));
|
|
84
92
|
if (!candidates.length)
|
|
85
93
|
return result("unavailable", "No authorized compatible Provider is available.");
|
|
86
|
-
const available = candidates.filter(
|
|
87
|
-
if (!available.length)
|
|
88
|
-
return {
|
|
89
|
-
|
|
90
|
-
candidates
|
|
91
|
-
};
|
|
94
|
+
const available = execution ? candidates.filter(entry => this.#unavailable(entry, authorized, new Set()) === undefined) : candidates;
|
|
95
|
+
if (!available.length) {
|
|
96
|
+
return { ...result("unavailable", candidates.map(entry => `${entry.provider.id}: ${this.#unavailable(entry, authorized, new Set())}`).join("; ")), candidates };
|
|
97
|
+
}
|
|
92
98
|
if (available.length > 1)
|
|
93
99
|
return { ...result("ambiguous", "Select an explicit Provider and contract version."), candidates };
|
|
94
100
|
const selected = available[0];
|
|
@@ -101,6 +107,32 @@ export class CapabilityRegistry {
|
|
|
101
107
|
return result("denied", "Current call authority is unavailable.");
|
|
102
108
|
}
|
|
103
109
|
}
|
|
110
|
+
describe(context, request) {
|
|
111
|
+
const selected = this.#select(context, request, false);
|
|
112
|
+
if (selected.kind !== "value")
|
|
113
|
+
return selected;
|
|
114
|
+
const descriptor = selected.value;
|
|
115
|
+
if (Object.hasOwn(request, "input")) {
|
|
116
|
+
const error = capabilitySchemaError(descriptor.inputSchema, request.input);
|
|
117
|
+
if (error)
|
|
118
|
+
return result("invalid", error);
|
|
119
|
+
try {
|
|
120
|
+
this.authorize(context, descriptor, request.input);
|
|
121
|
+
}
|
|
122
|
+
catch {
|
|
123
|
+
return result("denied", "Current call authority is unavailable.");
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
try {
|
|
127
|
+
const access = this.assess?.(context, descriptor, request.input);
|
|
128
|
+
if (access?.state === "hidden")
|
|
129
|
+
return result("unavailable", "No authorized compatible Provider is available.");
|
|
130
|
+
return access ? { ...selected, value: { ...descriptor, access } } : selected;
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
return result("failed", error instanceof Error ? error.message : "Authorization assessment failed.");
|
|
134
|
+
}
|
|
135
|
+
}
|
|
104
136
|
call(context, request) {
|
|
105
137
|
return this.#call(context, request, "external-operation");
|
|
106
138
|
}
|
|
@@ -112,7 +144,11 @@ export class CapabilityRegistry {
|
|
|
112
144
|
catch {
|
|
113
145
|
return result("invalid", "Capability request must be cloneable data.");
|
|
114
146
|
}
|
|
115
|
-
|
|
147
|
+
// Execution selects independently of discovery assessment and rechecks the
|
|
148
|
+
// exact input below. A requestable descriptor never grants admission.
|
|
149
|
+
const resolved = this.#select(context, {
|
|
150
|
+
name: request.name, contractVersion: request.contractVersion, providerId: request.providerId
|
|
151
|
+
}, true);
|
|
116
152
|
if (resolved.kind !== "value")
|
|
117
153
|
return resolved;
|
|
118
154
|
const descriptor = resolved.value;
|
|
@@ -135,6 +171,15 @@ export class CapabilityRegistry {
|
|
|
135
171
|
catch {
|
|
136
172
|
return { ...result("denied", "Current call authority was revoked."), ...origin };
|
|
137
173
|
}
|
|
174
|
+
try {
|
|
175
|
+
const access = this.assess?.(context, descriptor, request.input);
|
|
176
|
+
if (access && access.state !== "authorized") {
|
|
177
|
+
return { ...result("denied", "Exact target authorization is required."), ...origin };
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
return { ...result("failed", error instanceof Error ? error.message : "Authorization assessment failed."), ...origin };
|
|
182
|
+
}
|
|
138
183
|
const operations = [];
|
|
139
184
|
let effect = "none";
|
|
140
185
|
const observe = (operation) => {
|