@zq-silk/yui 0.15.6 → 0.15.8
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 +192 -399
- package/README.md +127 -1149
- package/dist/agent/adapterCatalog.js +15 -2
- package/dist/agent/agent.js +23 -3
- package/dist/agent/argumentPolicy.js +7 -1
- package/dist/agent/connectionPlan.js +62 -0
- package/dist/agent/executionComponents.js +158 -0
- package/dist/agent/launchEnvironment.js +31 -3
- package/dist/agent/managedRuntimeEnvironment.js +3 -5
- package/dist/{turn/turn.js → agentRun/agentRun.js} +166 -109
- package/dist/{turn/turnIdentity.js → agentRun/runIdentity.js} +4 -4
- package/dist/brief/taskBrief.js +12 -0
- package/dist/cli/agentConfigurationPicker.js +13 -0
- package/dist/cli/commandCatalog.js +167 -70
- package/dist/cli/interactionCandidates.js +5 -5
- package/dist/cli/interactionPolicy.js +38 -8
- package/dist/cli/invocationRouter.js +1 -1
- package/dist/cli/managedDiagnostics.js +28 -0
- package/dist/cli/operatorWizard.js +1 -7
- package/dist/cli/roleOptionOrder.js +27 -0
- package/dist/cli/roleWizard.js +50 -14
- package/dist/cli/updateOrchestrator.js +1 -1
- package/dist/cli/updatePorts.js +3 -4
- package/dist/cli.js +188 -95
- package/dist/commands/agentCommands.js +72 -14
- package/dist/commands/capabilityCommands.js +57 -0
- package/dist/commands/configCommands.js +20 -20
- package/dist/commands/deliveryGuardPreflight.js +2 -2
- package/dist/commands/executionAuditCommands.js +24 -24
- package/dist/commands/globalRoleCommands.js +1 -1
- package/dist/commands/grantCommands.js +4 -4
- package/dist/commands/operatorCommands.js +1 -7
- package/dist/commands/projectCommands.js +4 -4
- package/dist/commands/resourcesCommands.js +2 -2
- package/dist/commands/roleConfiguration.js +25 -5
- package/dist/commands/roleRuntimeGuard.js +4 -5
- package/dist/commands/sessionCommands.js +3 -7
- package/dist/commands/taskActivationCommands.js +259 -0
- package/dist/commands/taskActor.js +28 -49
- package/dist/commands/taskCommands.js +1216 -730
- package/dist/commands/taskContextCommand.js +39 -583
- package/dist/commands/taskExecutionCommands.js +32 -32
- package/dist/commands/taskInputCommands.js +40 -104
- package/dist/commands/taskIntegrationCommands.js +3 -2
- package/dist/commands/taskIntegrationQueueCommands.js +1 -1
- package/dist/commands/taskNextActionCommand.js +8 -8
- package/dist/commands/taskOverviewCommand.js +33 -45
- package/dist/commands/taskRemoteDeliveryCommand.js +2 -2
- package/dist/commands/taskRoleRuntimeStatus.js +133 -102
- package/dist/commands/telemetryCommands.js +36 -38
- package/dist/config/configCatalog.js +4 -4
- package/dist/config/yuiConfig.js +8 -8
- package/dist/context/contextSnapshot.js +10 -10
- package/dist/context/dispatchContext.js +11 -11
- package/dist/context/roleSessionContext.js +6 -3
- package/dist/context/{turnContextPack.js → runContextPack.js} +146 -81
- package/dist/context/{turnInputContract.js → runInputContract.js} +73 -60
- package/dist/context/sessionBootstrapManifest.js +21 -2
- package/dist/context/sourceRunContext.js +30 -0
- package/dist/context/taskContext.js +458 -0
- package/dist/context/wakeNotification.js +27 -27
- package/dist/controller/agentRuntimeObserver.js +21 -24
- package/dist/controller/capabilityBridge.js +77 -0
- package/dist/controller/clientRuntime.js +65 -92
- package/dist/controller/controller.js +127 -188
- package/dist/controller/fileSchedulerStoreAdapter.js +787 -887
- package/dist/controller/jobControl.js +54 -85
- package/dist/controller/resourceInventory.js +8 -27
- package/dist/controller/resourceInventoryLinux.js +12 -13
- package/dist/controller/runtime.js +530 -476
- package/dist/controller/runtimeEventInbox.js +55 -25
- package/dist/controller/runtimeEventProcessor.js +22 -31
- package/dist/controller/{runtimeHookTurnFence.js → runtimeHookRunFence.js} +91 -115
- package/dist/controller/runtimeLaunchCoordinator.js +80 -426
- package/dist/controller/runtimeObservationHook.js +14 -18
- package/dist/controller/sessionNotify.js +16 -24
- package/dist/controller/sessionOwnerReconciliation.js +168 -50
- package/dist/controller/structuredProviderObservation.js +138 -99
- package/dist/coordination/workMailbox.js +3 -3
- package/dist/coordination/workMailboxQueue.js +36 -33
- package/dist/core/boundedRpc.js +8 -1
- package/dist/core/controllerClient.js +20 -1
- package/dist/core/controllerServer.js +4 -4
- package/dist/doctor/doctor.js +13 -2
- package/dist/domain/agentResultTransport.js +9 -9
- package/dist/execution/codexThreadNaming.js +2 -8
- package/dist/execution/executionHealth.js +51 -63
- package/dist/execution/reviewMainRun.js +137 -0
- package/dist/execution/workItemExecution.js +28 -29
- package/dist/execution/workItemExecutionProjection.js +99 -107
- package/dist/execution/workItemMainRun.js +141 -0
- package/dist/executor/agentAdapter.js +227 -20
- package/dist/executor/agentConfigurationCatalog.js +126 -4
- package/dist/executor/agentConfigurationProbe.js +162 -4
- package/dist/executor/agentExecutor.js +79 -78
- package/dist/executor/effectiveLaunch.js +105 -18
- package/dist/executor/executorRegistry.js +29 -44
- package/dist/executor/fileRoleLaunchPlanner.js +229 -154
- package/dist/executor/workspacePreflightClassification.js +16 -16
- package/dist/grant/capabilityGrant.js +6 -3
- package/dist/input/inputRequest.js +12 -10
- package/dist/integration/gitIntegrationService.js +4 -11
- package/dist/integration/integrationQueueService.js +4 -4
- package/dist/interaction/operatorPresentation.js +1 -1
- package/dist/kernel/builtinCapabilities.js +423 -0
- package/dist/kernel/capabilityRegistry.js +314 -0
- package/dist/kernel/capabilitySchema.js +91 -0
- package/dist/kernel/instanceHost.js +18 -1
- package/dist/kernel/kernelPorts.js +4 -1
- package/dist/lifecycle/canonicalLifecycleEvent.js +44 -49
- package/dist/lifecycle/exactRunTerminalization.js +449 -0
- package/dist/message/message.js +62 -6
- package/dist/message/messageContinuation.js +204 -0
- package/dist/observability/executionAudit.js +70 -72
- package/dist/observability/faultClassification.js +2 -2
- package/dist/observability/orchestrationMetrics.js +8 -8
- package/dist/operator/operatorSessionHistory.js +1 -7
- package/dist/output/agentConfigurationPresentation.js +8 -3
- package/dist/output/agentRunConfigurationPresentation.js +128 -0
- package/dist/output/rolePresentation.js +54 -3
- package/dist/plugins/pluginChild.js +104 -0
- package/dist/plugins/pluginIntent.js +26 -0
- package/dist/plugins/pluginInterpreter.js +43 -0
- package/dist/plugins/pluginPackage.js +101 -0
- package/dist/plugins/pluginProcess.js +112 -0
- package/dist/plugins/pluginService.js +380 -0
- package/dist/profile/agentProfile.js +1 -1
- package/dist/repository/gitWorkspace.js +26 -4
- package/dist/repository/project.js +19 -4
- package/dist/repository/taskBaseFreshness.js +13 -13
- package/dist/repository/taskWorkspaceCoordinator.js +20 -27
- package/dist/repository/taskWorkspacePreparer.js +344 -83
- package/dist/resources/autoResourceGc.js +3 -3
- package/dist/resources/liveReferences.js +3 -3
- package/dist/resources/projectResource.js +123 -0
- package/dist/resources/projectResourceService.js +421 -0
- package/dist/resources/resourceDiscovery.js +6 -6
- package/dist/resources/resourceGc.js +1 -1
- package/dist/resources/resourceRegistrar.js +1 -1
- package/dist/resources/resourceTypes.js +1 -1
- package/dist/review/deltaRecheck.js +3 -3
- package/dist/review/reviewAcceptance.js +16 -16
- package/dist/review/reviewDecision.js +7 -7
- package/dist/review/reviewRound.js +21 -20
- package/dist/review/reviewerAvailability.js +2 -2
- package/dist/role/role.js +51 -7
- package/dist/role/taskRoleUpdate.js +30 -0
- package/dist/runtime/acpProtocol.js +425 -0
- package/dist/runtime/acpSession.js +731 -0
- package/dist/runtime/acpSessionConfiguration.js +260 -0
- package/dist/runtime/agentDriver.js +30 -11
- package/dist/runtime/agentEndpoint.js +278 -0
- package/dist/runtime/agentEndpointIdentity.js +86 -0
- package/dist/runtime/agentEndpointOwnership.js +239 -0
- package/dist/runtime/agentError.js +2 -10
- package/dist/runtime/agentHost.js +565 -314
- package/dist/runtime/agentRunConfiguration.js +258 -0
- package/dist/runtime/builtinAgentDrivers.js +134 -18
- package/dist/runtime/builtinAgentErrorMappers.js +55 -3
- package/dist/runtime/builtinTranscriptUsage.js +1 -1
- package/dist/runtime/claude-process-owner +0 -0
- package/dist/runtime/codexAppServerRuntime.js +38 -30
- package/dist/runtime/codexInteractiveHost.js +41 -6
- package/dist/runtime/continuationManager.js +2 -6
- package/dist/runtime/executionEnvironment.js +30 -0
- package/dist/runtime/firstProgressAdvisory.js +11 -11
- package/dist/runtime/index.js +4 -3
- package/dist/runtime/jsonLineChannel.js +109 -0
- package/dist/runtime/launchBroker.js +91 -16
- package/dist/runtime/launchDiagnostics.js +2 -2
- package/dist/runtime/lifecycleReservation.js +10 -18
- package/dist/runtime/managedCaller.js +61 -17
- package/dist/runtime/nativeSessionControl.js +102 -0
- package/dist/runtime/ports.js +6 -21
- package/dist/runtime/processExitObservation.js +8 -7
- package/dist/runtime/promptEnvelope.js +17 -6
- package/dist/runtime/providerContinuation.js +3 -9
- package/dist/runtime/providerContinuationReconciliationService.js +4 -13
- package/dist/runtime/providerControl.js +2 -7
- package/dist/runtime/providerRuntimeIdentity.js +110 -222
- package/dist/runtime/providerRuntimeReconciler.js +5 -9
- package/dist/runtime/runtimeBinding.js +0 -1
- package/dist/runtime/runtimeContinuationProjection.js +4 -7
- package/dist/runtime/runtimeDeadlines.js +9 -0
- package/dist/runtime/runtimeHealthPolicy.js +1 -1
- package/dist/runtime/runtimeObservation.js +29 -65
- package/dist/runtime/runtimeProjection.js +43 -51
- package/dist/runtime/runtimeSessionCandidate.js +1 -3
- package/dist/runtime/sessionLaunchRequest.js +3 -7
- package/dist/runtime/sessionOwnerIdentity.js +7 -54
- package/dist/runtime/sessionOwnerRegistry.js +22 -17
- package/dist/runtime/sessionReconciliation.js +4 -8
- package/dist/runtime/sessionTerminationGuard.js +70 -259
- package/dist/runtime/sessionTokenMetrics.js +5 -16
- package/dist/runtime/structuredProviderHost.js +237 -117
- package/dist/runtime/taskRuntimeIsolation.js +39 -122
- package/dist/runtime/tmuxAdapters.js +39 -86
- package/dist/scheduler/activeRoleRunDelivery.js +354 -0
- package/dist/scheduler/leaderWakeupProcessor.js +75 -266
- package/dist/scheduler/operatorInputNotificationProcessor.js +1 -1
- package/dist/scheduler/ports.js +80 -9
- package/dist/scheduler/{roleTurnLiveness.js → roleRunLiveness.js} +26 -30
- package/dist/scheduler/{roleTurnStall.js → roleRunStall.js} +128 -139
- package/dist/scheduler/taskExecutionProjection.js +120 -124
- package/dist/scheduler/taskObservabilityProjection.js +29 -29
- package/dist/scheduler/taskWake.js +11 -4
- package/dist/scheduler/wakeReason.js +9 -1
- package/dist/setup/setupCommand.js +1 -0
- package/dist/storage/migrations/agentRunContract.js +159 -0
- package/dist/storage/migrations/removeRuntimeGeneration.js +207 -0
- package/dist/storage/sqliteSchema.js +431 -5
- package/dist/storage/sqliteStore.js +375 -220
- package/dist/storage/storageVersions.js +1 -1
- package/dist/storage/storeRpc.js +10 -5
- package/dist/storage/taskStore.js +13 -11
- package/dist/storage/upgrade/upgradeOrchestrator.js +5 -7
- package/dist/surface/surfaceContributions.js +102 -0
- package/dist/task/completionReadiness.js +32 -6
- package/dist/task/deliveryGuard.js +16 -16
- package/dist/task/draftPlan.js +72 -12
- package/dist/task/nextAction.js +144 -128
- package/dist/task/remoteDelivery.js +6 -6
- package/dist/task/task.js +184 -18
- package/dist/task/taskActivation.js +301 -0
- package/dist/task/taskActivationService.js +392 -0
- package/dist/task/taskRecordReference.js +5 -4
- package/dist/task/taskRecordRetirement.js +1 -1
- package/dist/telemetry/sqliteTelemetryStore.js +55 -68
- package/dist/telemetry/telemetryConfig.js +14 -14
- package/dist/telemetry/telemetryWiring.js +2 -2
- package/dist/web/assets/assetManifest.js +2 -0
- package/dist/web/assets/client/app.js +120 -20
- package/dist/web/assets/client/components.js +87 -54
- package/dist/web/assets/client/i18n.js +83 -41
- package/dist/web/assets/client/markdown.js +1 -1
- package/dist/web/assets/client/taskSurface.js +353 -0
- package/dist/web/assets/client/view.js +49 -44
- package/dist/web/assets/shell.js +1 -1
- package/dist/web/assets/styles/cards.js +22 -4
- package/dist/web/controllerWeb.js +60 -0
- package/dist/web/webMutation.js +28 -0
- package/dist/web/webServer.js +118 -8
- package/dist/web/webSnapshot.js +81 -74
- package/dist/web/webTaskSurface.js +64 -0
- package/dist/workItem/dependencyGate.js +1 -1
- package/dist/workItem/workItem.js +80 -48
- package/dist/workspace/workItemChangeSetManager.js +16 -9
- package/docs/agent-result-consumption.md +94 -0
- package/docs/agent-runtime-drivers.md +91 -0
- package/docs/architecture/README.md +38 -0
- package/docs/architecture/capabilities-and-resources.md +79 -0
- package/docs/managed-turn-and-session-runtime.md +222 -0
- package/docs/observability/README.md +81 -0
- package/docs/plugin-sdk.md +290 -0
- package/docs/provider-runtime.md +163 -0
- package/docs/release-workflow.md +303 -0
- package/docs/roles-and-configuration.md +113 -0
- package/docs/sqlite-control-plane-design.md +76 -0
- package/docs/task-dag-semantics.md +57 -0
- package/docs/task-delivery.md +103 -0
- package/docs/task-local-identity.md +6 -6
- package/docs/testing/verification-levels.md +86 -0
- package/i18n/README.zh-CN.md +98 -739
- package/package.json +5 -5
- package/skills/yui-leader/SKILL.md +130 -103
- package/skills/yui-leader/references/integration.md +39 -0
- package/skills/yui-leader/references/replicated-execution.md +42 -0
- package/skills/yui-leader/references/task-plugins.md +33 -0
- package/skills/yui-operator/SKILL.md +30 -59
- package/skills/yui-reviewer/SKILL.md +35 -36
- package/skills/yui-runtime/SKILL.md +88 -24
- package/skills/yui-runtime/references/publication.md +22 -0
- package/skills/yui-runtime/references/recovery.md +64 -0
- package/skills/yui-worker/SKILL.md +37 -39
- package/dist/cli/roleOptionCatalog.js +0 -68
- package/dist/context/sourceTurnContext.js +0 -30
- package/dist/execution/reviewMainTurn.js +0 -161
- package/dist/execution/workItemMainTurn.js +0 -164
- package/dist/lifecycle/exactTurnTerminalization.js +0 -407
- package/dist/runtime/preallocatedNativeSession.js +0 -13
- package/dist/runtime/runtimeStopReceipt.js +0 -42
- package/dist/scheduler/activeRoleTurnDelivery.js +0 -315
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isDeepStrictEqual } from "node:util";
|
|
2
|
-
import {
|
|
2
|
+
import { isCompletedReviewExecutionFromRuns } from "../review/reviewAcceptance.js";
|
|
3
3
|
import { projectFirstProgressAdvisory } from "../runtime/firstProgressAdvisory.js";
|
|
4
4
|
/** One Task's orchestration cost and advisory projection, with no writes. */
|
|
5
5
|
export function projectTaskOrchestration(facts) {
|
|
@@ -48,10 +48,10 @@ export function projectTaskOrchestration(facts) {
|
|
|
48
48
|
timeToFirstProjectCommitMs: firstCommitAt === undefined
|
|
49
49
|
? null
|
|
50
50
|
: Math.max(0, Date.parse(firstCommitAt) - Date.parse(facts.task.createdAt)),
|
|
51
|
-
|
|
52
|
-
total: facts.
|
|
53
|
-
byStatus: counts(facts.
|
|
54
|
-
byRole: counts(facts.
|
|
51
|
+
runs: {
|
|
52
|
+
total: facts.runs.length,
|
|
53
|
+
byStatus: counts(facts.runs.map(({ status }) => status)),
|
|
54
|
+
byRole: counts(facts.runs.map(({ roleName }) => roleName))
|
|
55
55
|
},
|
|
56
56
|
workItems: facts.workItems.length,
|
|
57
57
|
reviews: {
|
|
@@ -65,7 +65,7 @@ export function projectTaskOrchestration(facts) {
|
|
|
65
65
|
repeatedIdentities,
|
|
66
66
|
evidenceReuses: facts.integrations.filter((attempt) => ((attempt.checks ?? []).some(({ details }) => details?.startsWith("Reused successful check evidence from ")))).length
|
|
67
67
|
},
|
|
68
|
-
|
|
68
|
+
providerSessionsBeforeFirstProgress: firstProgress.sessionsBeforeFirstProgress,
|
|
69
69
|
publicationToCompletionMs: publicationAt === undefined || facts.task.completedAt === undefined
|
|
70
70
|
? null
|
|
71
71
|
: Math.max(0, Date.parse(facts.task.completedAt) - Date.parse(publicationAt)),
|
|
@@ -91,7 +91,7 @@ function projectAdvisories(facts, fullRounds, repeatedIdentities, firstProgressA
|
|
|
91
91
|
.map(({ id }) => `durable-job:${id}`)
|
|
92
92
|
});
|
|
93
93
|
}
|
|
94
|
-
const completedFull = fullRounds.filter((round) => (
|
|
94
|
+
const completedFull = fullRounds.filter((round) => (isCompletedReviewExecutionFromRuns(round, facts.runs)))
|
|
95
95
|
.sort((left, right) => left.createdAt.localeCompare(right.createdAt));
|
|
96
96
|
const recent = completedFull.slice(-3);
|
|
97
97
|
const sameCandidate = recent.length === 3
|
|
@@ -108,7 +108,7 @@ function projectAdvisories(facts, fullRounds, repeatedIdentities, firstProgressA
|
|
|
108
108
|
if (firstProgressAttention) {
|
|
109
109
|
result.push({
|
|
110
110
|
code: "provider-first-progress-advisory",
|
|
111
|
-
reason: "Two fresh Leader
|
|
111
|
+
reason: "Two fresh Leader Sessions produced no first durable progress; consider Operator attention before another Session.",
|
|
112
112
|
refs: [facts.task.id]
|
|
113
113
|
});
|
|
114
114
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { agentAdapterLabel as adapterLabel } from "../agent/adapterCatalog.js";
|
|
1
2
|
import { normalizeRoleAgentSessionText, roleAgentSessionRef, validateRoleSessionSet } from "../executor/agentExecutor.js";
|
|
2
3
|
/** Separates the one selected writer authority from retained conversations. */
|
|
3
4
|
export function projectOperatorStatus(sessions, activeAgentId, activeAdapterId) {
|
|
@@ -141,13 +142,6 @@ function optionalDisplayText(value) {
|
|
|
141
142
|
const normalized = normalizeRoleAgentSessionText(value);
|
|
142
143
|
return normalized.length === 0 ? undefined : normalized;
|
|
143
144
|
}
|
|
144
|
-
function adapterLabel(adapterId) {
|
|
145
|
-
return adapterId === "codex"
|
|
146
|
-
? "Codex"
|
|
147
|
-
: adapterId === "claude"
|
|
148
|
-
? "Claude"
|
|
149
|
-
: adapterId;
|
|
150
|
-
}
|
|
151
145
|
function requireText(value, label) {
|
|
152
146
|
if (typeof value !== "string" || value.includes("\0") || value.trim().length === 0) {
|
|
153
147
|
throw new Error(`${label} is required.`);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { renderAgentConfigurationResolutionNotice } from "../cli/agentConfigurationPicker.js";
|
|
2
|
+
import { agentHandshakeLine } from "./agentRunConfigurationPresentation.js";
|
|
2
3
|
import { defaultTableWidth, renderTable } from "./table.js";
|
|
3
4
|
export function renderAgentConfigurationCatalog(resolved) {
|
|
4
5
|
const { catalog } = resolved;
|
|
@@ -11,13 +12,17 @@ export function renderAgentConfigurationCatalog(resolved) {
|
|
|
11
12
|
model.serviceTiers?.map((tier) => tier.value).join(", ") || "none reported"
|
|
12
13
|
]);
|
|
13
14
|
const fieldRows = catalog.fields
|
|
14
|
-
.filter((field) => field.key !== "model" && field.key !== "effort")
|
|
15
|
+
.filter((field) => modelRows.length === 0 || (field.key !== "model" && field.key !== "effort"))
|
|
15
16
|
.map((field) => [
|
|
16
17
|
field.key,
|
|
17
18
|
field.choices.map((choice) => choice.value).join(", ") || "none reported",
|
|
19
|
+
// An available field can still carry a reason, and for ACP it always does:
|
|
20
|
+
// the values are enumerated per Session, so the probe deliberately lists
|
|
21
|
+
// none. Dropping that sentence left "none reported" looking like a failed
|
|
22
|
+
// query rather than a deliberate boundary, so it is shown either way.
|
|
18
23
|
field.available === false
|
|
19
24
|
? `unavailable${field.reason === undefined ? "" : `: ${field.reason}`}`
|
|
20
|
-
: "available"
|
|
25
|
+
: field.reason === undefined ? "available" : `available: ${field.reason}`,
|
|
21
26
|
field.allowCustom ? "yes" : "no"
|
|
22
27
|
]);
|
|
23
28
|
const sections = [
|
|
@@ -39,5 +44,5 @@ export function renderAgentConfigurationCatalog(resolved) {
|
|
|
39
44
|
{ header: "Custom", minWidth: 6, maxWidth: 8 }
|
|
40
45
|
], fieldRows, defaultTableWidth())])
|
|
41
46
|
];
|
|
42
|
-
return `${sections.join("\n\n")}\n${renderAgentConfigurationResolutionNotice(resolved)}`;
|
|
47
|
+
return `${sections.join("\n\n")}\n${agentHandshakeLine(catalog.handshake)}\n${renderAgentConfigurationResolutionNotice(resolved)}`;
|
|
43
48
|
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rendering what a live Agent reports about its own run configuration.
|
|
3
|
+
*
|
|
4
|
+
* The whole point of this display is to keep two things visibly apart that a
|
|
5
|
+
* reader would otherwise merge: what the launch asked for, and what the Agent
|
|
6
|
+
* says it is doing. So the confirmation strength is printed for every requested
|
|
7
|
+
* value rather than summarized into a checkmark, and a value Yui could not
|
|
8
|
+
* verify is labelled as such in the same column where a verified one would
|
|
9
|
+
* appear. There is no state in which this renders a requested value as though
|
|
10
|
+
* the Agent had agreed to it.
|
|
11
|
+
*/
|
|
12
|
+
import { renderTable, defaultTableWidth } from "./table.js";
|
|
13
|
+
/**
|
|
14
|
+
* The one handshake line, shared by every surface that has one to show.
|
|
15
|
+
*
|
|
16
|
+
* Both the capability catalog and a Session inspect display this same fact, and
|
|
17
|
+
* a reader comparing them is entitled to assume the wording means the same thing
|
|
18
|
+
* in both. One function is what guarantees that, in the same way one projection
|
|
19
|
+
* function guarantees the two produce the same observation to begin with.
|
|
20
|
+
*
|
|
21
|
+
* `undefined` is the catalog's third state — no connection was attempted — and
|
|
22
|
+
* is distinct from a plan that negotiates nothing.
|
|
23
|
+
*/
|
|
24
|
+
export function agentHandshakeLine(handshake) {
|
|
25
|
+
if (handshake === undefined) {
|
|
26
|
+
return "Agent handshake: not attempted (these values did not come from a live connection)";
|
|
27
|
+
}
|
|
28
|
+
if (handshake.status === "unsupported") {
|
|
29
|
+
return `Agent handshake: unsupported (${handshake.reason})`;
|
|
30
|
+
}
|
|
31
|
+
return [
|
|
32
|
+
`Agent handshake: protocol v${handshake.protocolVersion};`,
|
|
33
|
+
` reported as ${handshake.agentName} ${handshake.agentVersion};`,
|
|
34
|
+
` capabilities ${handshake.capabilities.join(", ") || "none"};`,
|
|
35
|
+
// Method ids only. Which methods an Agent offers is a capability; any
|
|
36
|
+
// credential or token that satisfies one is not Yui's to display.
|
|
37
|
+
` auth methods ${handshake.authMethods.join(", ") || "none"}`
|
|
38
|
+
].join("");
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The one-line form, for a status display that has a line to spare.
|
|
42
|
+
*
|
|
43
|
+
* Deliberately says which of the three "no value" answers applies instead of
|
|
44
|
+
* printing a blank: a reader who sees nothing cannot tell whether Yui failed to
|
|
45
|
+
* ask, the Agent has nothing to report, or the Agent reported a problem.
|
|
46
|
+
*/
|
|
47
|
+
export function agentRunConfigurationLabel(observation) {
|
|
48
|
+
if (observation === undefined)
|
|
49
|
+
return "not queried";
|
|
50
|
+
if (observation.status === "unknown")
|
|
51
|
+
return `unknown (${observation.reason})`;
|
|
52
|
+
if (observation.status === "unsupported")
|
|
53
|
+
return `unsupported (${observation.reason})`;
|
|
54
|
+
const summary = observation.requested.length === 0
|
|
55
|
+
? "no configuration requested; the Agent's own defaults stand"
|
|
56
|
+
: observation.requested
|
|
57
|
+
.map((request) => `${request.field}=${requestValueLabel(request)}`)
|
|
58
|
+
.join(", ");
|
|
59
|
+
return `${summary}; read at ${observation.observedAt}`;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* The full form: one table for what was asked and one for what the Agent offers.
|
|
63
|
+
*
|
|
64
|
+
* Returns an empty string when there is nothing an Agent reported, so a caller
|
|
65
|
+
* can append it unconditionally without producing an empty heading.
|
|
66
|
+
*/
|
|
67
|
+
export function renderAgentRunConfiguration(observation) {
|
|
68
|
+
if (observation === undefined || observation.status !== "observed")
|
|
69
|
+
return "";
|
|
70
|
+
const sections = [];
|
|
71
|
+
if (observation.requested.length > 0) {
|
|
72
|
+
sections.push(renderTable("Requested run configuration (launch-time confirmation)", [
|
|
73
|
+
{ header: "Field", minWidth: 10, maxWidth: 12 },
|
|
74
|
+
{ header: "Requested", minWidth: 12, maxWidth: 30 },
|
|
75
|
+
{ header: "Evidence", minWidth: 12, maxWidth: 14 },
|
|
76
|
+
{ header: "Agent reports now", minWidth: 16, maxWidth: 46 }
|
|
77
|
+
], observation.requested.map((request) => [
|
|
78
|
+
request.field,
|
|
79
|
+
request.value,
|
|
80
|
+
confirmationLabel(request.confirmation),
|
|
81
|
+
currentValueLabel(request.current, request.value)
|
|
82
|
+
]), defaultTableWidth()));
|
|
83
|
+
}
|
|
84
|
+
if (observation.axes.length > 0) {
|
|
85
|
+
sections.push(renderTable("Agent-reported configuration options (Yui-supported axes)", [
|
|
86
|
+
{ header: "Option", minWidth: 10, maxWidth: 22 },
|
|
87
|
+
{ header: "Axis", minWidth: 8, maxWidth: 14 },
|
|
88
|
+
{ header: "Current", minWidth: 12, maxWidth: 40 },
|
|
89
|
+
{ header: "Offered", minWidth: 18, maxWidth: 44 }
|
|
90
|
+
], observation.axes.map((axis) => [
|
|
91
|
+
axis.key,
|
|
92
|
+
axis.category ?? "unnamed",
|
|
93
|
+
axis.current.status === "observed"
|
|
94
|
+
? axis.current.value
|
|
95
|
+
: `unobserved: ${axis.current.reason}`,
|
|
96
|
+
axis.offered.join(", ") || "none reported"
|
|
97
|
+
]), defaultTableWidth()));
|
|
98
|
+
}
|
|
99
|
+
sections.push(agentHandshakeLine(observation.handshake));
|
|
100
|
+
sections.push(`Read from the live Session at ${observation.observedAt}. `
|
|
101
|
+
+ "Requests describe this connection's launch, not subsequent Role edits; "
|
|
102
|
+
+ "current values are the latest peer reports, not independent execution verification.");
|
|
103
|
+
return sections.join("\n\n");
|
|
104
|
+
}
|
|
105
|
+
/** Spell out what each confirmation actually proves, in the table cell. */
|
|
106
|
+
function confirmationLabel(confirmation) {
|
|
107
|
+
if (confirmation === "already")
|
|
108
|
+
return "already set";
|
|
109
|
+
if (confirmation === "observed")
|
|
110
|
+
return "observed";
|
|
111
|
+
return "ack only";
|
|
112
|
+
}
|
|
113
|
+
function requestValueLabel(request) {
|
|
114
|
+
if (request.current.status === "unobserved")
|
|
115
|
+
return `${request.value} (unverified)`;
|
|
116
|
+
return request.current.value === request.value
|
|
117
|
+
? request.value
|
|
118
|
+
// Drift is the case this whole path exists to make visible, so it is stated
|
|
119
|
+
// rather than resolved in favour of either value.
|
|
120
|
+
: `${request.current.value} (requested ${request.value})`;
|
|
121
|
+
}
|
|
122
|
+
function currentValueLabel(current, requested) {
|
|
123
|
+
if (current.status === "unobserved")
|
|
124
|
+
return `unobserved: ${current.reason}`;
|
|
125
|
+
return current.value === requested
|
|
126
|
+
? current.value
|
|
127
|
+
: `${current.value} — differs from the requested ${requested}`;
|
|
128
|
+
}
|
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
import { activeRoleAgentSession } from "../executor/agentExecutor.js";
|
|
2
|
+
import { effectiveRoleForLaunch } from "../executor/effectiveLaunch.js";
|
|
2
3
|
import { defaultTableWidth, renderTable } from "./table.js";
|
|
4
|
+
/** Desired configuration and the fixed Session request are not Provider evidence. */
|
|
5
|
+
export function renderRoleLaunchComparison(role, effective) {
|
|
6
|
+
const pinned = effectiveRoleForLaunch(role, effective);
|
|
7
|
+
return renderTable("Configuration intent (not observed Provider state)", [
|
|
8
|
+
{ header: "Source", minWidth: 14, maxWidth: 20 },
|
|
9
|
+
{ header: "Agent / component", minWidth: 18, maxWidth: 32 },
|
|
10
|
+
{ header: "Model", minWidth: 10, maxWidth: 20 },
|
|
11
|
+
{ header: "Effort", minWidth: 8, maxWidth: 14 },
|
|
12
|
+
{ header: "Permission", minWidth: 12, maxWidth: 34 },
|
|
13
|
+
{ header: "Profile intent", minWidth: 14, maxWidth: 14 }
|
|
14
|
+
], [[`Role desired r${role.launchRevision}`, role],
|
|
15
|
+
[`Session pinned r${effective.sourceDesiredRevision}`, pinned]].map(([source, current]) => {
|
|
16
|
+
const binding = current.agentBindings[current.activeAgentId];
|
|
17
|
+
return [
|
|
18
|
+
source,
|
|
19
|
+
`${binding.agentId}/${binding.component}`,
|
|
20
|
+
binding.config.model ?? "Agent default",
|
|
21
|
+
binding.config.effort ?? "Agent default",
|
|
22
|
+
permission(binding),
|
|
23
|
+
current.defaultAccess
|
|
24
|
+
];
|
|
25
|
+
}), defaultTableWidth());
|
|
26
|
+
}
|
|
3
27
|
export function activeRoleSummary(role) {
|
|
4
28
|
const binding = role.agentBindings[role.activeAgentId];
|
|
5
29
|
return {
|
|
@@ -24,10 +48,20 @@ export function renderRoleDetails(title, role, input) {
|
|
|
24
48
|
` Kind ${input.kind}`,
|
|
25
49
|
` Active Agent ${role.activeAgentId}`,
|
|
26
50
|
` Workspace ${role.workspace}`,
|
|
51
|
+
...("taskId" in role ? [
|
|
52
|
+
` Desired environment ${role.executionEnvironment === undefined
|
|
53
|
+
? "managed workspace"
|
|
54
|
+
: `${role.executionEnvironment.environmentRef}; ${role.executionEnvironment.directory.path}; ${role.executionEnvironment.access}`}`,
|
|
55
|
+
` Session environment ${effective === undefined ? "not started"
|
|
56
|
+
: effective.executionEnvironment === undefined ? "managed workspace"
|
|
57
|
+
: `${effective.executionEnvironment.environmentRef}; ${effective.executionEnvironment.directory.path}; ${effective.executionEnvironment.access}`}`
|
|
58
|
+
] : []),
|
|
27
59
|
` Desired launch r${role.launchRevision}; Profile intent=${role.defaultAccess}`,
|
|
60
|
+
// The component is what the Session is pinned to; its plan follows from it.
|
|
61
|
+
// Naming the plan alone would show two different ACP products identically.
|
|
28
62
|
` Effective launch ${effective === undefined
|
|
29
63
|
? "not started"
|
|
30
|
-
: `${effective.agentId}/${effective.
|
|
64
|
+
: `${effective.agentId}/${effective.component}; r${effective.sourceDesiredRevision}; Profile intent=${effective.profileAccess}; permission=${effective.permission.strategy}`}`,
|
|
31
65
|
` Desired drift ${effective === undefined
|
|
32
66
|
? "-"
|
|
33
67
|
: effective.sourceDesiredRevision === role.launchRevision
|
|
@@ -46,7 +80,10 @@ export function renderRoleDetails(title, role, input) {
|
|
|
46
80
|
renderTable("Agent settings", [
|
|
47
81
|
{ header: "Agent", minWidth: 5, maxWidth: 20 },
|
|
48
82
|
{ header: "Active", minWidth: 6, maxWidth: 6 },
|
|
49
|
-
|
|
83
|
+
// Wide enough to hold the longest component id whole: it is an
|
|
84
|
+
// identifier, so splitting it mid-token reads worse than letting the
|
|
85
|
+
// prose in Permission wrap, which it does naturally.
|
|
86
|
+
{ header: "Component", minWidth: 17, maxWidth: 18 },
|
|
50
87
|
{ header: "Model", minWidth: 8, maxWidth: 24 },
|
|
51
88
|
{ header: "Effort", minWidth: 8, maxWidth: 16 },
|
|
52
89
|
{ header: "Permission", minWidth: 10, maxWidth: 34 },
|
|
@@ -59,7 +96,7 @@ function bindingRow(binding, role, sessions) {
|
|
|
59
96
|
return [
|
|
60
97
|
binding.agentId,
|
|
61
98
|
binding.agentId === role.activeAgentId ? "yes" : "",
|
|
62
|
-
binding.
|
|
99
|
+
binding.component,
|
|
63
100
|
binding.config.model ?? "CLI default",
|
|
64
101
|
binding.config.effort ?? "CLI default",
|
|
65
102
|
permission(binding),
|
|
@@ -81,6 +118,20 @@ function permission(binding) {
|
|
|
81
118
|
permission.approval === undefined ? undefined : `approval=${permission.approval}`
|
|
82
119
|
].filter((value) => value !== undefined).join("; ");
|
|
83
120
|
}
|
|
121
|
+
// ACP's strategy decides the Session mode Yui requests over the protocol. It
|
|
122
|
+
// is stated separately from how Yui answers `session/request_permission`,
|
|
123
|
+
// which is always a decline on this transport: those are two different
|
|
124
|
+
// questions, and collapsing them would make a configured Session mode read as
|
|
125
|
+
// interactive consent Yui does not hold.
|
|
126
|
+
if (binding.config.adapterId === "acp") {
|
|
127
|
+
const permission = binding.config.permission;
|
|
128
|
+
const requests = "requests declined";
|
|
129
|
+
if (permission.strategy === "default")
|
|
130
|
+
return `Agent default; ${requests}`;
|
|
131
|
+
if (permission.strategy === "bypass")
|
|
132
|
+
return `bypass; ${requests}`;
|
|
133
|
+
return `mode=${permission.mode}; ${requests}`;
|
|
134
|
+
}
|
|
84
135
|
const permission = binding.config.permission;
|
|
85
136
|
if (permission.strategy === "default")
|
|
86
137
|
return "CLI default";
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/** Dedicated trusted-local process. Module linking fixes the dependency closure
|
|
2
|
+
* to captured package bytes; node:vm is NOT a security boundary. The explicit
|
|
3
|
+
* trusted-local execution grant, not this loader, authorizes author code. */
|
|
4
|
+
import { createContext, SourceTextModule } from "node:vm";
|
|
5
|
+
import { posix } from "node:path";
|
|
6
|
+
const send = (message) => process.send?.(message);
|
|
7
|
+
const pending = new Map();
|
|
8
|
+
let nextCall = 0;
|
|
9
|
+
let instance;
|
|
10
|
+
let selfTest;
|
|
11
|
+
const context = createContext(Object.create(null), { codeGeneration: { strings: false, wasm: false } });
|
|
12
|
+
process.on("message", (raw) => {
|
|
13
|
+
if (raw.type === "nested-result") {
|
|
14
|
+
const entry = pending.get(raw.id);
|
|
15
|
+
if (!entry)
|
|
16
|
+
return;
|
|
17
|
+
pending.delete(raw.id);
|
|
18
|
+
if (raw.error)
|
|
19
|
+
entry.reject(new Error(String(raw.error)));
|
|
20
|
+
else
|
|
21
|
+
entry.resolve(raw.value);
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
void execute(raw).then((value) => send({ type: "result", id: raw.id, value }), (error) => send({ type: "result", id: raw.id, error: error instanceof Error ? error.message : String(error) }));
|
|
25
|
+
});
|
|
26
|
+
async function execute(message) {
|
|
27
|
+
if (message.type === "load") {
|
|
28
|
+
const files = message.files;
|
|
29
|
+
const modules = new Map();
|
|
30
|
+
const load = (name) => {
|
|
31
|
+
const previous = modules.get(name);
|
|
32
|
+
if (previous)
|
|
33
|
+
return previous;
|
|
34
|
+
if (!Object.hasOwn(files, name))
|
|
35
|
+
throw new Error(`Package module missing: ${name}`);
|
|
36
|
+
const module = new SourceTextModule(files[name], {
|
|
37
|
+
context, identifier: name,
|
|
38
|
+
importModuleDynamically: async () => { throw new Error("Dynamic imports are not supported; bundle package dependencies."); }
|
|
39
|
+
});
|
|
40
|
+
modules.set(name, module);
|
|
41
|
+
return module;
|
|
42
|
+
};
|
|
43
|
+
const root = load(message.entry);
|
|
44
|
+
await root.link((specifier, parent) => {
|
|
45
|
+
if (!specifier.startsWith("./") && !specifier.startsWith("../"))
|
|
46
|
+
throw new Error("Only package-relative module imports are supported.");
|
|
47
|
+
const path = posix.normalize(posix.join(posix.dirname(parent.identifier), specifier));
|
|
48
|
+
if (path.startsWith("../") || path.startsWith("/"))
|
|
49
|
+
throw new Error("Module import escapes the captured package.");
|
|
50
|
+
return load(path);
|
|
51
|
+
});
|
|
52
|
+
await root.evaluate();
|
|
53
|
+
const exports = root.namespace;
|
|
54
|
+
if (typeof exports.initialize !== "function")
|
|
55
|
+
throw new Error("Executable entry must export initialize().");
|
|
56
|
+
// No host invocation/identity/resource port exists during initialization.
|
|
57
|
+
instance = await exports.initialize();
|
|
58
|
+
if (!instance || typeof instance.handlers !== "object" || !instance.handlers
|
|
59
|
+
|| Object.values(instance.handlers).some((handler) => typeof handler !== "function")
|
|
60
|
+
|| (instance.dispose !== undefined && typeof instance.dispose !== "function")) {
|
|
61
|
+
throw new Error("initialize() must return handlers and an optional dispose function.");
|
|
62
|
+
}
|
|
63
|
+
selfTest = exports.selfTest;
|
|
64
|
+
return Object.keys(instance.handlers).sort();
|
|
65
|
+
}
|
|
66
|
+
if (message.type === "test") {
|
|
67
|
+
if (typeof selfTest !== "function")
|
|
68
|
+
throw new Error("Executable validation requires exported selfTest().");
|
|
69
|
+
if (await selfTest() !== true)
|
|
70
|
+
throw new Error("selfTest() must return true.");
|
|
71
|
+
return true;
|
|
72
|
+
}
|
|
73
|
+
if (message.type === "dispose") {
|
|
74
|
+
await instance?.dispose?.();
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
if (message.type !== "invoke")
|
|
78
|
+
throw new Error("Unknown child request.");
|
|
79
|
+
let open = true;
|
|
80
|
+
try {
|
|
81
|
+
const value = await instance.handlers[message.name](message.input, Object.freeze({
|
|
82
|
+
context: Object.freeze(message.context), requestId: message.requestId,
|
|
83
|
+
call: (request) => {
|
|
84
|
+
if (!open)
|
|
85
|
+
return Promise.reject(new Error("Plugin invocation ended."));
|
|
86
|
+
const id = `nested-${++nextCall}`;
|
|
87
|
+
return new Promise((resolve, reject) => {
|
|
88
|
+
pending.set(id, { resolve, reject });
|
|
89
|
+
send({ type: "nested", id, parentId: message.id, request });
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
}));
|
|
93
|
+
// IPC's JSON projection must not hide bad output from the parent registry.
|
|
94
|
+
return JSON.parse(JSON.stringify(value, (_key, item) => {
|
|
95
|
+
if (typeof item === "bigint" || typeof item === "function" || typeof item === "symbol"
|
|
96
|
+
|| (typeof item === "number" && !Number.isFinite(item)))
|
|
97
|
+
throw new Error("Plugin output is not JSON data.");
|
|
98
|
+
return item;
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
101
|
+
finally {
|
|
102
|
+
open = false;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { requireIdentity, requireText, requireTimestamp } from "../domain/validation.js";
|
|
2
|
+
export function validatePluginId(id) {
|
|
3
|
+
if (typeof id !== "string" || !/^[a-z][a-z0-9-]*$/u.test(id))
|
|
4
|
+
throw new Error("Invalid plugin id.");
|
|
5
|
+
return id;
|
|
6
|
+
}
|
|
7
|
+
export function validatePluginIntentFailure(failure) {
|
|
8
|
+
requireText(failure.message, "Plugin failure");
|
|
9
|
+
requireTimestamp(failure.occurredAt, "Plugin failure time");
|
|
10
|
+
return failure;
|
|
11
|
+
}
|
|
12
|
+
export function validatePluginIntent(intent) {
|
|
13
|
+
if (intent.schemaVersion !== 1 || typeof intent.enabled !== "boolean"
|
|
14
|
+
|| !Number.isSafeInteger(intent.revision) || intent.revision < 1)
|
|
15
|
+
throw new Error("Invalid plugin intent.");
|
|
16
|
+
requireIdentity(intent.taskId, "Plugin intent Task");
|
|
17
|
+
validatePluginId(intent.pluginId);
|
|
18
|
+
requireTimestamp(intent.updatedAt, "Plugin intent time");
|
|
19
|
+
if (intent.validationId !== undefined)
|
|
20
|
+
requireIdentity(intent.validationId, "Plugin validation reference");
|
|
21
|
+
if (intent.enabled && intent.validationId === undefined)
|
|
22
|
+
throw new Error("Enabled intent requires a fixed validation reference.");
|
|
23
|
+
if (intent.lastFailure !== undefined)
|
|
24
|
+
validatePluginIntentFailure(intent.lastFailure);
|
|
25
|
+
return intent;
|
|
26
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { capabilitySchemaError } from "../kernel/capabilitySchema.js";
|
|
2
|
+
/** Bounded data interpreter, not JavaScript evaluation or a workflow engine. */
|
|
3
|
+
export function interpretPlugin(pkg) {
|
|
4
|
+
const handlers = JSON.parse(pkg.files[pkg.manifest.entry]);
|
|
5
|
+
const text = { type: "string", minLength: 1 };
|
|
6
|
+
const handlerSchema = {
|
|
7
|
+
anyOf: [
|
|
8
|
+
{ type: "object", properties: { type: { const: "echo" } }, required: ["type"], additionalProperties: false },
|
|
9
|
+
{ type: "object", properties: { type: { const: "constant" }, value: {} }, required: ["type", "value"], additionalProperties: false },
|
|
10
|
+
{ type: "object", properties: { type: { const: "call" }, name: text, contractVersion: text, providerId: text },
|
|
11
|
+
required: ["type", "name", "contractVersion"], additionalProperties: false }
|
|
12
|
+
]
|
|
13
|
+
};
|
|
14
|
+
const error = capabilitySchemaError({
|
|
15
|
+
type: "object", required: pkg.manifest.capabilities.map((capability) => capability.name),
|
|
16
|
+
properties: Object.fromEntries(pkg.manifest.capabilities.map((capability) => [capability.name, handlerSchema])),
|
|
17
|
+
additionalProperties: false
|
|
18
|
+
}, handlers);
|
|
19
|
+
if (error)
|
|
20
|
+
throw new Error(`Declarative entry: ${error}`);
|
|
21
|
+
const entries = handlers;
|
|
22
|
+
for (const handler of Object.values(entries)) {
|
|
23
|
+
if (handler.type === "call" && !pkg.manifest.required.some((dependency) => dependency.name === handler.name && dependency.contractVersion === handler.contractVersion)) {
|
|
24
|
+
throw new Error("Declarative call must name a required dependency.");
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return {
|
|
28
|
+
async invoke(name, input, invocation) {
|
|
29
|
+
const handler = entries[name];
|
|
30
|
+
if (handler.type === "echo")
|
|
31
|
+
return input;
|
|
32
|
+
if (handler.type === "constant")
|
|
33
|
+
return structuredClone(handler.value);
|
|
34
|
+
const result = await invocation.call({
|
|
35
|
+
name: handler.name, input, contractVersion: handler.contractVersion,
|
|
36
|
+
providerId: handler.providerId, requestId: invocation.requestId
|
|
37
|
+
});
|
|
38
|
+
if (!["value", "operation"].includes(result.kind))
|
|
39
|
+
throw new Error(result.detail ?? result.kind);
|
|
40
|
+
return result.value;
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { lstatSync, readFileSync, readdirSync, realpathSync } from "node:fs";
|
|
3
|
+
import { join, posix } from "node:path";
|
|
4
|
+
import { capabilitySchemaError, checkCapabilitySchema } from "../kernel/capabilitySchema.js";
|
|
5
|
+
const text = { type: "string", minLength: 1 };
|
|
6
|
+
const texts = { type: "array", items: text };
|
|
7
|
+
const object = (properties, required = Object.keys(properties)) => ({ type: "object", properties, required, additionalProperties: false });
|
|
8
|
+
const manifestSchema = object({
|
|
9
|
+
id: text, version: text, apiVersion: { const: "1" }, entry: text,
|
|
10
|
+
kind: { enum: ["declarative", "trusted-local"] }, reloadMode: { const: "manual" },
|
|
11
|
+
permissions: texts,
|
|
12
|
+
required: { type: "array", items: object({ name: text, contractVersion: text }) },
|
|
13
|
+
capabilities: { type: "array", minItems: 1, items: object({
|
|
14
|
+
name: text, contractVersion: text, summary: text,
|
|
15
|
+
inputSchema: { type: "object" }, outputSchema: { type: "object" },
|
|
16
|
+
effect: { enum: ["query", "local-mutation", "external-operation"] }, requiredPermissions: texts
|
|
17
|
+
}) },
|
|
18
|
+
build: { type: "array", minItems: 1, items: text }
|
|
19
|
+
}, ["id", "version", "apiVersion", "entry", "kind", "reloadMode", "permissions", "required", "capabilities"]);
|
|
20
|
+
export function parsePluginManifest(value) {
|
|
21
|
+
const error = capabilitySchemaError(manifestSchema, value);
|
|
22
|
+
if (error)
|
|
23
|
+
throw new Error(`Plugin manifest: ${error}`);
|
|
24
|
+
const manifest = value;
|
|
25
|
+
if (!/^[a-z][a-z0-9-]*$/u.test(manifest.id))
|
|
26
|
+
throw new Error("Plugin id must be a lowercase unqualified name.");
|
|
27
|
+
packagePath(manifest.entry);
|
|
28
|
+
if (manifest.kind === "declarative" && manifest.build !== undefined)
|
|
29
|
+
throw new Error("Declarative packages do not execute builds.");
|
|
30
|
+
if (new Set(manifest.permissions).size !== manifest.permissions.length)
|
|
31
|
+
throw new Error("Duplicate plugin permissions.");
|
|
32
|
+
if (manifest.permissions.includes("plugin:manage"))
|
|
33
|
+
throw new Error("Task-local plugins cannot manage their own Host or other plugins.");
|
|
34
|
+
for (const capability of manifest.capabilities) {
|
|
35
|
+
checkCapabilitySchema(capability.inputSchema);
|
|
36
|
+
checkCapabilitySchema(capability.outputSchema);
|
|
37
|
+
if (capability.requiredPermissions.some((permission) => !manifest.permissions.includes(permission))) {
|
|
38
|
+
throw new Error("Capability permission exceeds the manifest permission set.");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return manifest;
|
|
42
|
+
}
|
|
43
|
+
export function packagePath(value) {
|
|
44
|
+
if (!value || value.startsWith("/") || value.includes("\\") || value.includes("\0")
|
|
45
|
+
|| value.split("/").some((part) => !part || part === "." || part === "..")
|
|
46
|
+
|| posix.normalize(value) !== value)
|
|
47
|
+
throw new Error("Package paths must be normalized relative paths.");
|
|
48
|
+
return value;
|
|
49
|
+
}
|
|
50
|
+
/** Data-only scan. Symlinks, special files and non-UTF8 dependencies are not a
|
|
51
|
+
* supported package. Capture every byte, including build inputs and dependencies. */
|
|
52
|
+
export function readPluginPackage(directory, requireEntry = true) {
|
|
53
|
+
if (realpathSync(directory) !== directory)
|
|
54
|
+
throw new Error("Plugin directory must be canonical.");
|
|
55
|
+
const files = Object.create(null);
|
|
56
|
+
let bytes = 0;
|
|
57
|
+
const walk = (prefix) => {
|
|
58
|
+
for (const name of readdirSync(join(directory, prefix)).sort()) {
|
|
59
|
+
const path = prefix ? `${prefix}/${name}` : name;
|
|
60
|
+
packagePath(path);
|
|
61
|
+
const stat = lstatSync(join(directory, path));
|
|
62
|
+
if (stat.isDirectory())
|
|
63
|
+
walk(path);
|
|
64
|
+
else {
|
|
65
|
+
if (!stat.isFile() || stat.isSymbolicLink())
|
|
66
|
+
throw new Error("Plugin package contains a non-regular file.");
|
|
67
|
+
bytes += stat.size;
|
|
68
|
+
if (bytes > 4 * 1024 * 1024 || Object.keys(files).length >= 256)
|
|
69
|
+
throw new Error("Plugin package exceeds 4 MiB/256 files.");
|
|
70
|
+
const buffer = readFileSync(join(directory, path));
|
|
71
|
+
const content = buffer.toString("utf8");
|
|
72
|
+
if (!Buffer.from(content).equals(buffer))
|
|
73
|
+
throw new Error("Plugin package files must be UTF-8.");
|
|
74
|
+
files[path] = content;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
walk("");
|
|
79
|
+
const manifest = parsePluginManifest(JSON.parse(files["plugin.json"] ?? "null"));
|
|
80
|
+
if (requireEntry && !Object.hasOwn(files, manifest.entry))
|
|
81
|
+
throw new Error("Plugin entry is missing.");
|
|
82
|
+
return { manifest, files, digest: packageDigest(files) };
|
|
83
|
+
}
|
|
84
|
+
export function packageDigest(files) {
|
|
85
|
+
return createHash("sha256").update(JSON.stringify(Object.entries(files).sort(([a], [b]) => a.localeCompare(b)))).digest("hex");
|
|
86
|
+
}
|
|
87
|
+
export function validatePluginValidation(record) {
|
|
88
|
+
if (record.schemaVersion !== 1 || !record.id?.startsWith("validation-") || !record.taskId || !record.directory || !record.preparationId
|
|
89
|
+
|| !record.environment?.path || record.environment.isolation !== "trusted-local"
|
|
90
|
+
|| !record.environment.node || !record.environment.device || !record.environment.inode
|
|
91
|
+
|| !/^[0-9a-f]{64}$/u.test(record.sourceDigest) || !Number.isFinite(Date.parse(record.createdAt))
|
|
92
|
+
|| !Array.isArray(record.checks) || !record.checks.every((value) => typeof value === "string")) {
|
|
93
|
+
throw new Error("Invalid plugin validation record.");
|
|
94
|
+
}
|
|
95
|
+
const manifest = parsePluginManifest(JSON.parse(record.package.files["plugin.json"]));
|
|
96
|
+
if (JSON.stringify(manifest) !== JSON.stringify(record.package.manifest)
|
|
97
|
+
|| packageDigest(record.package.files) !== record.package.digest
|
|
98
|
+
|| !Object.hasOwn(record.package.files, manifest.entry))
|
|
99
|
+
throw new Error("Plugin validation content mismatch.");
|
|
100
|
+
return record;
|
|
101
|
+
}
|