@zq-silk/yui 0.16.0 → 0.99.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/ARCHITECTURE.md +12 -0
- package/ARCHITECTURE.zh-CN.md +8 -0
- package/README.md +3 -0
- package/dist/agent/managedRuntimeEnvironment.js +0 -4
- package/dist/agentRun/agentRun.js +7 -17
- package/dist/cli/agentConfigurationPicker.js +24 -21
- package/dist/cli/commandCatalog.js +18 -10
- package/dist/cli/roleWizard.js +41 -30
- package/dist/cli/updateCommand.js +35 -13
- package/dist/cli/updateOrchestrator.js +73 -33
- package/dist/cli/updatePorts.js +51 -20
- package/dist/cli.js +35 -26
- package/dist/commands/controllerCommands.js +1 -1
- package/dist/commands/executionAuditCommands.js +2 -2
- package/dist/commands/projectCommands.js +1 -1
- package/dist/commands/releaseCommands.js +9 -38
- package/dist/commands/taskCommands.js +84 -81
- package/dist/commands/taskCompletionGate.js +0 -117
- package/dist/commands/taskRoleRuntimeStatus.js +1 -1
- package/dist/commands/taskUpstreamCommands.js +93 -54
- package/dist/context/runContextPack.js +30 -54
- package/dist/context/runInputContract.js +9 -0
- package/dist/context/sessionBootstrapManifest.js +11 -19
- package/dist/controller/agentHostObservation.js +4 -2
- package/dist/controller/controller.js +8 -6
- package/dist/controller/fileSchedulerStoreAdapter.js +85 -18
- package/dist/controller/globalInputDelivery.js +28 -7
- package/dist/controller/globalRuntimeAttention.js +34 -0
- package/dist/controller/operatorNotification.js +31 -0
- package/dist/controller/providerRetryAdmission.js +3 -1
- package/dist/controller/providerRetryDelivery.js +10 -3
- package/dist/controller/runtime.js +1 -27
- package/dist/controller/sessionOwnerReconciliation.js +42 -24
- package/dist/controller/structuredProviderObservation.js +27 -0
- package/dist/controller/updateReconciliation.js +72 -20
- package/dist/doctor/doctor.js +2 -1
- package/dist/executor/agentAdapter.js +28 -96
- package/dist/executor/agentConfigurationCatalog.js +13 -68
- package/dist/executor/agentConfigurationFields.js +120 -0
- package/dist/executor/agentConfigurationProbe.js +55 -80
- package/dist/executor/agentExecutor.js +5 -2
- package/dist/executor/executorRegistry.js +0 -9
- package/dist/executor/fileRoleLaunchPlanner.js +12 -11
- package/dist/interaction/operatorPresentation.js +2 -1
- package/dist/message/messageContinuation.js +8 -9
- package/dist/observability/executionAudit.js +1 -10
- package/dist/observability/runtimeIdentity.js +0 -23
- package/dist/output/agentConfigurationPresentation.js +4 -2
- package/dist/release/releaseHandover.js +2 -2
- package/dist/release/releaseIdempotencyStore.js +0 -23
- package/dist/release/releaseWorkflowPorts.js +8 -4
- package/dist/repository/gitWorkspace.js +66 -19
- package/dist/repository/taskWorkspacePreparer.js +75 -68
- package/dist/resources/resourceRegistry.js +90 -44
- package/dist/resources/sqliteResourceRegistry.js +3 -5
- package/dist/runtime/acpSession.js +21 -5
- package/dist/runtime/agentEndpoint.js +4 -1
- package/dist/runtime/agentHost.js +22 -62
- package/dist/runtime/agentHostCleanup.js +85 -0
- package/dist/runtime/agentHostProtocol.js +1 -1
- package/dist/runtime/builtinAgentDrivers.js +46 -119
- package/dist/runtime/builtinTranscriptObserver.js +10 -6
- package/dist/runtime/codexAppServerRuntime.js +55 -89
- package/dist/runtime/jsonLineChannel.js +35 -7
- package/dist/runtime/providerContinuationReconciliationService.js +42 -3
- package/dist/runtime/providerRetry.js +15 -0
- package/dist/runtime/providerRuntimeReconciler.js +12 -2
- package/dist/runtime/structuredProviderHost.js +106 -81
- package/dist/scheduler/activeRoleRunDelivery.js +5 -1
- package/dist/scheduler/operatorInputNotificationProcessor.js +7 -17
- package/dist/storage/upgrade/upgradeOrchestrator.js +0 -10
- package/dist/task/nextAction.js +33 -23
- package/dist/task/task.js +2 -1
- package/dist/web/assets/assetManifest.js +2 -0
- package/dist/web/assets/client/app.js +72 -3
- package/dist/web/assets/client/components.js +2 -1
- package/dist/web/assets/client/i18n.js +2 -0
- package/dist/web/assets/client/taskSummary.js +345 -0
- package/dist/web/assets/client/taskSurface.js +44 -26
- package/dist/web/assets/client/view.js +33 -0
- package/dist/web/assets/shell.js +1 -0
- package/dist/web/assets/styles/cards.js +21 -0
- package/dist/web/webServer.js +39 -5
- package/dist/web/webSessions.js +165 -0
- package/dist/web/webSnapshot.js +23 -0
- package/dist/web/webTaskSurface.js +23 -0
- package/docs/managed-turn-and-session-runtime.md +31 -0
- package/docs/managed-turn-and-session-runtime.zh-CN.md +23 -0
- package/docs/observability/README.md +47 -0
- package/docs/observability/README.zh-CN.md +37 -0
- package/docs/project-refresh.md +9 -0
- package/docs/project-refresh.zh-CN.md +8 -0
- package/docs/provider-retry.md +35 -0
- package/docs/release-workflow.md +128 -20
- package/docs/release-workflow.zh-CN.md +107 -14
- package/docs/roles-and-configuration.md +30 -0
- package/docs/roles-and-configuration.zh-CN.md +20 -0
- package/docs/task-delivery.md +109 -0
- package/docs/task-delivery.zh-CN.md +80 -0
- package/docs/task-discovery.md +9 -0
- package/docs/task-discovery.zh-CN.md +6 -0
- package/docs/testing/verification-levels.md +33 -12
- package/docs/testing/verification-levels.zh-CN.md +28 -8
- package/i18n/README.zh-CN.md +2 -0
- package/package.json +1 -1
- package/skills/yui-leader/SKILL.md +11 -0
- package/skills/yui-leader/references/execution.md +22 -0
- package/skills/yui-leader/references/planning.md +6 -2
- package/skills/yui-operator/SKILL.md +45 -13
- package/skills/yui-operator/references/task-delivery.md +99 -0
- package/skills/yui-runtime/SKILL.md +5 -3
- package/skills/yui-runtime/references/publication.md +56 -5
- package/skills/yui-runtime/references/recovery.md +12 -2
- package/dist/runtime/agentHostCompatibility.js +0 -127
|
@@ -14,6 +14,33 @@ let deliveryFailure;
|
|
|
14
14
|
// Only live diagnostics for this process's immutable files, not another queue.
|
|
15
15
|
// No payload, retry, or authoritative delivery state is kept in this map.
|
|
16
16
|
const emittedEvents = new Map();
|
|
17
|
+
/** Retained in the existing Host Inbox; it never settles a Turn or a resource. */
|
|
18
|
+
export async function publishStructuredProviderDiagnostic(input) {
|
|
19
|
+
const diagnostic = input.diagnostic;
|
|
20
|
+
const adapterId = requireIdentity(input.environment.YUI_ADAPTER_ID, "Agent adapter id");
|
|
21
|
+
const owner = describeHostIdentity(input.environment, adapterId, diagnostic.nativeSessionId);
|
|
22
|
+
const failure = diagnostic.failure;
|
|
23
|
+
await persistAndApply(input.home, [observation({
|
|
24
|
+
kind: diagnostic.attemptId === undefined ? "conversation.observed" : "observer.health",
|
|
25
|
+
sequence: nextStructuredSequence(), ordinal: 0,
|
|
26
|
+
observedAt: new Date().toISOString(),
|
|
27
|
+
fence: {
|
|
28
|
+
...owner, driverId: builtinAgentDriverRegistry().requireByAdapterId(adapterId).id,
|
|
29
|
+
nativeSessionId: diagnostic.nativeSessionId, conversationId: diagnostic.nativeSessionId,
|
|
30
|
+
...(diagnostic.attemptId === undefined ? {} : { receiptId: diagnostic.attemptId }),
|
|
31
|
+
...(diagnostic.nativeTurnId === undefined ? {} : { nativeTurnId: diagnostic.nativeTurnId })
|
|
32
|
+
},
|
|
33
|
+
payload: {
|
|
34
|
+
...(diagnostic.attemptId === undefined ? { recoverability: "unknown" } : {}),
|
|
35
|
+
sourceId: failure.phase === "host-stop" ? "host-cleanup" : "provider-channel",
|
|
36
|
+
observerStatus: "unavailable", observerDetail: failure.detail,
|
|
37
|
+
failure: { error: standardAgentError({
|
|
38
|
+
source: "host", phase: failure.phase, message: failure.detail, raw: failure.raw ?? failure.detail,
|
|
39
|
+
inputDisposition: failure.inputDisposition, sessionDisposition: failure.sessionDisposition
|
|
40
|
+
}) }
|
|
41
|
+
}
|
|
42
|
+
})], input.environment);
|
|
43
|
+
}
|
|
17
44
|
/** A live transport observation, independent of Provider state and Run truth. */
|
|
18
45
|
export function structuredProviderEventDelivery(home, environment, nativeSessionId) {
|
|
19
46
|
try {
|
|
@@ -3,6 +3,14 @@ import { acquireHomeLifecycleLock } from "../core/controllerServer.js";
|
|
|
3
3
|
import { cleanControllerResource } from "./resourceCleanupLinux.js";
|
|
4
4
|
import { scanControllerResourceInventory } from "./resourceInventoryLinux.js";
|
|
5
5
|
const MAX_RECONCILIATION_PASSES = 4;
|
|
6
|
+
export class UpdateControllerReconciliationError extends Error {
|
|
7
|
+
result;
|
|
8
|
+
constructor(message, result, options) {
|
|
9
|
+
super(message, options);
|
|
10
|
+
this.result = result;
|
|
11
|
+
this.name = "UpdateControllerReconciliationError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
6
14
|
/**
|
|
7
15
|
* Reconcile only Controller-owned resources for the Home being updated.
|
|
8
16
|
*
|
|
@@ -12,21 +20,49 @@ const MAX_RECONCILIATION_PASSES = 4;
|
|
|
12
20
|
* and inode fingerprints. Agent, tmux, app, and foreign-Home resources are
|
|
13
21
|
* deliberately outside this operation.
|
|
14
22
|
*/
|
|
15
|
-
export async function reconcileControllerResourcesForUpdate(home, environment = process.env, tmuxBin
|
|
23
|
+
export async function reconcileControllerResourcesForUpdate(home, environment = process.env, tmuxBin, ports = {
|
|
24
|
+
scan: scanControllerResourceInventory,
|
|
25
|
+
clean: cleanControllerResource,
|
|
26
|
+
acquireLock: acquireHomeLifecycleLock
|
|
27
|
+
}) {
|
|
16
28
|
const resolvedHome = resolve(home);
|
|
17
|
-
const releaseLock = await acquireHomeLifecycleLock(resolvedHome, {
|
|
18
|
-
removeStaleOwner: true
|
|
19
|
-
});
|
|
20
29
|
const cleaned = new Set();
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
30
|
+
const attempts = [];
|
|
31
|
+
let remaining = [];
|
|
32
|
+
let observedAt;
|
|
33
|
+
let observationError;
|
|
34
|
+
let lockReleaseError;
|
|
35
|
+
const result = () => ({
|
|
36
|
+
home: resolvedHome, cleaned: [...cleaned], attempts: [...attempts], remaining: [...remaining],
|
|
37
|
+
...(observedAt === undefined ? {} : { observedAt }),
|
|
38
|
+
...(observationError === undefined ? {} : { observationError }),
|
|
39
|
+
...(lockReleaseError === undefined ? {} : { lockReleaseError })
|
|
40
|
+
});
|
|
41
|
+
const scan = async () => {
|
|
42
|
+
try {
|
|
43
|
+
const snapshot = await ports.scan({
|
|
44
|
+
currentHome: resolvedHome, scope: "current", environment,
|
|
27
45
|
...(tmuxBin === undefined ? {} : { tmuxBin })
|
|
28
46
|
});
|
|
47
|
+
if (snapshot.scope === "current" && resolve(snapshot.currentHome) === resolvedHome) {
|
|
48
|
+
remaining = controllerResources(snapshot, resolvedHome);
|
|
49
|
+
observedAt = snapshot.observedAt;
|
|
50
|
+
}
|
|
29
51
|
assertCertainSnapshot(snapshot, resolvedHome);
|
|
52
|
+
observationError = undefined;
|
|
53
|
+
return snapshot;
|
|
54
|
+
}
|
|
55
|
+
catch (error) {
|
|
56
|
+
observationError = messageOf(error);
|
|
57
|
+
throw error;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
let releaseLock;
|
|
61
|
+
let failure;
|
|
62
|
+
try {
|
|
63
|
+
releaseLock = await ports.acquireLock(resolvedHome, { removeStaleOwner: true });
|
|
64
|
+
for (let pass = 0; pass < MAX_RECONCILIATION_PASSES; pass += 1) {
|
|
65
|
+
const snapshot = await scan();
|
|
30
66
|
const resources = controllerResources(snapshot, resolvedHome);
|
|
31
67
|
const controllers = resources.filter(({ kind }) => kind === "controller");
|
|
32
68
|
const current = controllers.filter(({ state }) => state === "current");
|
|
@@ -50,36 +86,52 @@ export async function reconcileControllerResourcesForUpdate(home, environment =
|
|
|
50
86
|
}
|
|
51
87
|
const candidates = [...historicalCleanup, ...staleArtifactCleanup];
|
|
52
88
|
if (candidates.length === 0) {
|
|
53
|
-
|
|
89
|
+
break;
|
|
54
90
|
}
|
|
55
91
|
for (const candidate of candidates) {
|
|
56
92
|
try {
|
|
57
|
-
await
|
|
93
|
+
await ports.clean(candidate, { environment, ...(tmuxBin === undefined ? {} : { tmuxBin }) });
|
|
58
94
|
cleaned.add(candidate.id);
|
|
95
|
+
attempts.push({ resource: candidate, outcome: "cleaned" });
|
|
96
|
+
remaining = remaining.filter(({ id }) => id !== candidate.id);
|
|
59
97
|
}
|
|
60
98
|
catch (error) {
|
|
61
99
|
// A concurrent exact cleanup that already reached the desired state
|
|
62
100
|
// is harmless. Anything still present or reclassified is a real
|
|
63
101
|
// ownership change and must remain a user-visible blocker.
|
|
64
|
-
const
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
assertCertainSnapshot(afterFailure, resolvedHome);
|
|
102
|
+
const attempt = { resource: candidate, outcome: "unknown", error: messageOf(error) };
|
|
103
|
+
attempts.push(attempt);
|
|
104
|
+
// If this observation also fails, keep BOTH failures and the exact
|
|
105
|
+
// attempted identity. Failure to observe absence is never success.
|
|
106
|
+
const afterFailure = await scan();
|
|
70
107
|
if (!controllerResources(afterFailure, resolvedHome).some(({ id }) => id === candidate.id)) {
|
|
71
108
|
cleaned.add(candidate.id);
|
|
109
|
+
attempts[attempts.length - 1] = { ...attempt, outcome: "absent" };
|
|
72
110
|
continue;
|
|
73
111
|
}
|
|
74
112
|
throw reconciliationBlocked(`resource ${candidate.id} changed or could not be cleaned: ${messageOf(error)}`);
|
|
75
113
|
}
|
|
76
114
|
}
|
|
115
|
+
if (pass === MAX_RECONCILIATION_PASSES - 1) {
|
|
116
|
+
throw reconciliationBlocked("Controller resources did not converge after bounded cleanup");
|
|
117
|
+
}
|
|
77
118
|
}
|
|
78
|
-
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
failure = error;
|
|
79
122
|
}
|
|
80
123
|
finally {
|
|
81
|
-
|
|
124
|
+
try {
|
|
125
|
+
await releaseLock?.();
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
lockReleaseError = messageOf(error);
|
|
129
|
+
failure ??= error;
|
|
130
|
+
}
|
|
82
131
|
}
|
|
132
|
+
if (failure !== undefined)
|
|
133
|
+
throw new UpdateControllerReconciliationError(messageOf(failure), result(), { cause: failure });
|
|
134
|
+
return result();
|
|
83
135
|
}
|
|
84
136
|
function assertCertainSnapshot(snapshot, resolvedHome) {
|
|
85
137
|
if (snapshot.scope !== "current"
|
package/dist/doctor/doctor.js
CHANGED
|
@@ -694,7 +694,8 @@ function checkAgent(agent, executor, environment) {
|
|
|
694
694
|
detail: [
|
|
695
695
|
`start resume interrupt nativeSession=${snapshot.lifecycle.nativeSessionDiscovery}`,
|
|
696
696
|
`preInputReady=${snapshot.lifecycle.preInputReadiness.status}`,
|
|
697
|
-
`fields=${available}/${degraded}/${unavailable}`,
|
|
697
|
+
`fields(help-observed/static-or-unverified/unavailable)=${available}/${degraded}/${unavailable}`,
|
|
698
|
+
"installation/help inspection only; no model or account capability query",
|
|
698
699
|
...snapshot.warnings.map((warning) => `warning=${warning}`)
|
|
699
700
|
].join(" ")
|
|
700
701
|
}
|
|
@@ -6,11 +6,12 @@ import { ownedArgumentsForAdapter, validateAgentAdvancedArguments, validateAgent
|
|
|
6
6
|
import { writeTextFileAtomically } from "../storage/durableFile.js";
|
|
7
7
|
import { discoverAcpConfiguration, discoverClaudeConfiguration, discoverCodexConfiguration } from "./agentConfigurationProbe.js";
|
|
8
8
|
import { builtinAgentDriverRegistry } from "../runtime/builtinAgentDrivers.js";
|
|
9
|
-
|
|
10
|
-
const APPROVALS = ["untrusted", "on-request", "never"];
|
|
9
|
+
import { CODEX_SANDBOXES as SANDBOXES, CODEX_APPROVALS as APPROVALS, configurationFieldsFromHelp, configurationFieldWarnings, staticAgentConfigurationFields, STATIC_CONFIGURATION_NOTICE } from "./agentConfigurationFields.js";
|
|
11
10
|
const PROBE_TIMEOUT_MS = 2_000;
|
|
12
11
|
const PROBE_MAX_BYTES = 1024 * 1024;
|
|
13
|
-
|
|
12
|
+
// Offline producer/type evidence, not a claim of live account/model testing.
|
|
13
|
+
// Keep the existing minimum support floors separate: see provider-protocol-contracts.md.
|
|
14
|
+
const AUDITED_PRODUCER_VERSIONS = { codex: "0.153.4", claude: "2.1.270" };
|
|
14
15
|
class BaseAdapter {
|
|
15
16
|
launchContextArgs(_input) {
|
|
16
17
|
return [];
|
|
@@ -556,7 +557,7 @@ export function inspectAgentCapabilities(agent, optionsOrNow = {}) {
|
|
|
556
557
|
const at = now.toISOString();
|
|
557
558
|
const adapter = resolveAgentAdapter(agent.adapterId);
|
|
558
559
|
validateAgentBaseArguments(agent.adapterId, agent.baseArgs);
|
|
559
|
-
const versionRun = run(agent.command, ["--version"]);
|
|
560
|
+
const versionRun = run(agent.command, [...agent.baseArgs, "--version"]);
|
|
560
561
|
const failure = failed(versionRun);
|
|
561
562
|
if (failure !== undefined) {
|
|
562
563
|
const missing = versionRun.error?.code === "ENOENT";
|
|
@@ -583,7 +584,7 @@ export function inspectAgentCapabilities(agent, optionsOrNow = {}) {
|
|
|
583
584
|
probedAt: at
|
|
584
585
|
}, fields, at, [`Installed version ${version} is not supported by adapter ${adapter.id}.`]);
|
|
585
586
|
}
|
|
586
|
-
const help = run(agent.command, ["--help"]);
|
|
587
|
+
const help = run(agent.command, [...agent.baseArgs, "--help"]);
|
|
587
588
|
const helpFailure = failed(help);
|
|
588
589
|
if (helpFailure !== undefined) {
|
|
589
590
|
return snapshot(agent, adapter, {
|
|
@@ -593,114 +594,45 @@ export function inspectAgentCapabilities(agent, optionsOrNow = {}) {
|
|
|
593
594
|
}, fields, at);
|
|
594
595
|
}
|
|
595
596
|
const helpOutput = output(help.stdout, help.stderr);
|
|
596
|
-
|
|
597
|
+
const helpFields = configurationFieldsFromHelp(agent.adapterId, helpOutput);
|
|
598
|
+
fields = capabilityFields(helpFields);
|
|
599
|
+
warnings.push(...configurationFieldWarnings(helpFields));
|
|
597
600
|
const missing = missingRequiredCapabilities(adapter.id, helpOutput);
|
|
598
601
|
if (missing.length > 0) {
|
|
599
602
|
return snapshot(agent, adapter, {
|
|
600
603
|
status: "unsupported-version", command: agent.command, version,
|
|
601
604
|
reason: `${adapter.label} CLI is missing required capabilities: ${missing.join(", ")}.`,
|
|
602
605
|
probedAt: at
|
|
603
|
-
}, fields, at);
|
|
606
|
+
}, fields, at, warnings);
|
|
604
607
|
}
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
warnings.push(`Installed
|
|
608
|
-
+ `${
|
|
608
|
+
const audited = adapter.id === "acp" ? undefined : AUDITED_PRODUCER_VERSIONS[adapter.id];
|
|
609
|
+
if (audited !== undefined && compareVersions(version, audited) > 0) {
|
|
610
|
+
warnings.push(`Installed ${adapter.label} version ${version} is newer than the latest audited producer `
|
|
611
|
+
+ `${audited}; required CLI flags were detected, not live protocol compatibility.`);
|
|
609
612
|
}
|
|
610
613
|
return snapshot(agent, adapter, {
|
|
611
614
|
status: "installed", command: agent.command, version, probedAt: at
|
|
612
615
|
}, fields, at, warnings);
|
|
613
616
|
}
|
|
614
617
|
function baseline(id) {
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
field("permission.mode", "enum", "degraded", true),
|
|
628
|
-
// Configurable, but only reaches an Agent that advertises
|
|
629
|
-
// `sessionCapabilities.additionalDirectories` at `initialize`. A static
|
|
630
|
-
// baseline cannot see that handshake, so it reports the field as degraded
|
|
631
|
-
// rather than promising delivery it cannot guarantee.
|
|
632
|
-
field("additionalDirectories", "path-list", "degraded", true)
|
|
633
|
-
];
|
|
634
|
-
if (id === "codex")
|
|
635
|
-
return [
|
|
636
|
-
field("model", "enum", "degraded", true), field("effort", "enum", "unavailable", true),
|
|
637
|
-
field("permission.strategy", "enum", "available", false, ["default", "bypass", "configured"]),
|
|
638
|
-
field("permission.sandbox", "enum", "available", false, SANDBOXES),
|
|
639
|
-
field("permission.approval", "enum", "available", false, APPROVALS),
|
|
640
|
-
field("profile", "string", "available", true), field("search", "boolean", "available", false, ["true"]),
|
|
641
|
-
field("additionalDirectories", "path-list", "available", true)
|
|
642
|
-
];
|
|
643
|
-
return [
|
|
644
|
-
field("model", "enum", "degraded", true, ["fable", "opus", "sonnet"]),
|
|
645
|
-
field("effort", "enum", "unavailable", true),
|
|
646
|
-
field("permission.strategy", "enum", "available", false, ["default", "bypass", "configured"]),
|
|
647
|
-
field("permission.mode", "enum", "unavailable", true),
|
|
648
|
-
field("permission.allowedTools", "string-list", "available", true),
|
|
649
|
-
field("permission.disallowedTools", "string-list", "available", true),
|
|
650
|
-
field("additionalDirectories", "path-list", "available", true), field("settingsFile", "path", "available", true),
|
|
651
|
-
field("settingsSources", "string-list", "degraded", false, ["user", "project", "local"])
|
|
652
|
-
];
|
|
653
|
-
}
|
|
654
|
-
function fromHelp(id, help) {
|
|
655
|
-
const fields = baseline(id);
|
|
656
|
-
// Nothing in an ACP Agent's `--help` describes the protocol it speaks, so
|
|
657
|
-
// there is nothing here to refine: the baseline is already the whole truth.
|
|
658
|
-
if (id === "acp")
|
|
659
|
-
return fields;
|
|
660
|
-
const replacements = id === "codex"
|
|
661
|
-
? [permissionStrategyField(help, "--dangerously-bypass-approvals-and-sandbox"),
|
|
662
|
-
choiceField("permission.sandbox", help, "--sandbox", SANDBOXES),
|
|
663
|
-
choiceField("permission.approval", help, "--ask-for-approval", APPROVALS)]
|
|
664
|
-
: [choiceField("model", help, "--model", ["fable", "opus", "sonnet"], true),
|
|
665
|
-
choiceField("effort", help, "--effort", [], true),
|
|
666
|
-
permissionStrategyField(help, "--dangerously-skip-permissions"),
|
|
667
|
-
choiceField("permission.mode", help, "--permission-mode", [], true),
|
|
668
|
-
choiceField("settingsSources", help, "--setting-sources", ["user", "project", "local"])];
|
|
669
|
-
const byKey = new Map(replacements.map((value) => [value.key, value]));
|
|
670
|
-
return fields.map((value) => byKey.get(value.key) ?? value);
|
|
671
|
-
}
|
|
672
|
-
function permissionStrategyField(help, bypassFlag) {
|
|
673
|
-
const choices = [
|
|
674
|
-
"default",
|
|
675
|
-
...(help.includes(bypassFlag) ? ["bypass"] : []),
|
|
676
|
-
"configured"
|
|
677
|
-
];
|
|
678
|
-
return field("permission.strategy", "enum", choices.includes("bypass") ? "available" : "degraded", false, choices);
|
|
679
|
-
}
|
|
680
|
-
function choiceField(key, help, flag, fallback, custom = false) {
|
|
681
|
-
const choices = helpChoices(help, flag);
|
|
682
|
-
return field(key, key === "settingsSources" ? "string-list" : "enum", choices.length > 0 ? "available" : fallback.length > 0 ? "degraded" : "unavailable", custom, choices.length > 0 ? choices : fallback);
|
|
683
|
-
}
|
|
684
|
-
function field(key, kind, status, allowCustom, choices) {
|
|
685
|
-
return { key, kind, status, allowCustom, ...(choices === undefined ? {} : { choices: [...choices] }) };
|
|
686
|
-
}
|
|
687
|
-
function helpChoices(help, flag) {
|
|
688
|
-
const lines = help.replace(/\r\n/g, "\n").split("\n");
|
|
689
|
-
const start = lines.findIndex((line) => line.includes(flag));
|
|
690
|
-
if (start < 0)
|
|
691
|
-
return [];
|
|
692
|
-
let end = start + 1;
|
|
693
|
-
while (end < lines.length && !/^\s{2,}(?:-[A-Za-z](?:,\s*)?|--)[\w-]/.test(lines[end]))
|
|
694
|
-
end += 1;
|
|
695
|
-
const body = /(?:\[possible values:\s*([^\]]+)\]|\(choices:\s*([^)]*)\)|\(([^)]*)\))/i
|
|
696
|
-
.exec(lines.slice(start, end).join("\n"));
|
|
697
|
-
return [...new Set((body?.[1] ?? body?.[2] ?? body?.[3] ?? "").replace(/["']/g, "").split(",")
|
|
698
|
-
.map((value) => value.trim().replace(/\.$/, "")).filter((value) => /^[\w.+-]+$/.test(value)))];
|
|
618
|
+
return capabilityFields(staticAgentConfigurationFields(id));
|
|
619
|
+
}
|
|
620
|
+
function capabilityFields(fields) {
|
|
621
|
+
const kinds = {
|
|
622
|
+
profile: "string", search: "boolean", additionalDirectories: "path-list", settingsFile: "path",
|
|
623
|
+
settingsSources: "string-list", "permission.allowedTools": "string-list", "permission.disallowedTools": "string-list"
|
|
624
|
+
};
|
|
625
|
+
return fields.map(field => ({
|
|
626
|
+
key: field.key, kind: kinds[field.key] ?? "enum", allowCustom: field.allowCustom,
|
|
627
|
+
status: field.available === true ? "available" : field.available === false ? "unavailable" : "degraded",
|
|
628
|
+
choices: field.choices.map(choice => choice.value)
|
|
629
|
+
}));
|
|
699
630
|
}
|
|
700
631
|
function snapshot(agent, adapter, installation, fields, at, warnings = []) {
|
|
701
632
|
return { schemaVersion: 1, agentId: agent.id, adapterId: agent.adapterId, installation,
|
|
702
633
|
lifecycle: { start: true, resume: true, nativeSessionDiscovery: adapter.capabilities.nativeSessionDiscovery,
|
|
703
|
-
interrupt: true, preInputReadiness: adapter.capabilities.preInputReadiness }, fields,
|
|
634
|
+
interrupt: true, preInputReadiness: adapter.capabilities.preInputReadiness }, fields,
|
|
635
|
+
warnings: [...new Set([STATIC_CONFIGURATION_NOTICE, ...warnings])], refreshedAt: at };
|
|
704
636
|
}
|
|
705
637
|
function runProbe(command, args) {
|
|
706
638
|
const result = spawnSync(command, [...args], { encoding: "utf8", shell: false, timeout: PROBE_TIMEOUT_MS,
|
|
@@ -5,6 +5,7 @@ import { join } from "node:path";
|
|
|
5
5
|
import { writeTextFileAtomically } from "../storage/durableFile.js";
|
|
6
6
|
import { resolveAgentAdapter } from "./agentAdapter.js";
|
|
7
7
|
import { redactAgentErrorText } from "../runtime/agentError.js";
|
|
8
|
+
import { staticAgentConfigurationFields, STATIC_CONFIGURATION_NOTICE } from "./agentConfigurationFields.js";
|
|
8
9
|
const DEFAULT_TIMEOUT_MS = 8_000;
|
|
9
10
|
export class AgentConfigurationCatalogService {
|
|
10
11
|
yuiHome;
|
|
@@ -119,71 +120,14 @@ export class AgentConfigurationCatalogService {
|
|
|
119
120
|
}
|
|
120
121
|
}
|
|
121
122
|
export function fallbackAgentConfigurationCatalog(agent) {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
if (agent.adapterId === "acp") {
|
|
131
|
-
return {
|
|
132
|
-
schemaVersion: 1,
|
|
133
|
-
agentId: agent.id,
|
|
134
|
-
adapterId: "acp",
|
|
135
|
-
models: [],
|
|
136
|
-
fields: [
|
|
137
|
-
...common,
|
|
138
|
-
field("permission.strategy", [choice("default"), choice("bypass"), choice("configured")], false),
|
|
139
|
-
// The mode ids come from a live Session, so an offline catalog can only
|
|
140
|
-
// say the axis exists. Listing candidates here would invent an Agent's
|
|
141
|
-
// vocabulary from a build-time guess.
|
|
142
|
-
field("permission.mode", [], true)
|
|
143
|
-
],
|
|
144
|
-
warnings: ["Runtime configuration catalog is unavailable."]
|
|
145
|
-
};
|
|
146
|
-
}
|
|
147
|
-
return agent.adapterId === "codex"
|
|
148
|
-
? {
|
|
149
|
-
schemaVersion: 1,
|
|
150
|
-
agentId: agent.id,
|
|
151
|
-
adapterId: "codex",
|
|
152
|
-
models: [],
|
|
153
|
-
fields: [
|
|
154
|
-
...common,
|
|
155
|
-
field("permission.strategy", ["default", "bypass", "configured"].map(choice), false),
|
|
156
|
-
field("permission.sandbox", [
|
|
157
|
-
"read-only", "workspace-write", "danger-full-access"
|
|
158
|
-
].map(choice), false),
|
|
159
|
-
field("permission.approval", [
|
|
160
|
-
"untrusted", "on-request", "never"
|
|
161
|
-
].map(choice), false),
|
|
162
|
-
field("search", [choice("true")], false),
|
|
163
|
-
field("profile", [], true),
|
|
164
|
-
field("additionalDirectories", [], true)
|
|
165
|
-
],
|
|
166
|
-
warnings: ["Runtime configuration catalog is unavailable."]
|
|
167
|
-
}
|
|
168
|
-
: {
|
|
169
|
-
schemaVersion: 1,
|
|
170
|
-
agentId: agent.id,
|
|
171
|
-
adapterId: "claude",
|
|
172
|
-
models: [],
|
|
173
|
-
fields: [
|
|
174
|
-
...common,
|
|
175
|
-
field("permission.strategy", ["default", "bypass", "configured"].map(choice), false),
|
|
176
|
-
field("permission.mode", [
|
|
177
|
-
"acceptEdits", "auto", "bypassPermissions", "manual", "dontAsk", "plan"
|
|
178
|
-
].map(choice), true),
|
|
179
|
-
field("permission.allowedTools", [], true),
|
|
180
|
-
field("permission.disallowedTools", [], true),
|
|
181
|
-
field("settingsSources", ["user", "project", "local"].map(choice), false),
|
|
182
|
-
field("settingsFile", [], true),
|
|
183
|
-
field("additionalDirectories", [], true)
|
|
184
|
-
],
|
|
185
|
-
warnings: ["Runtime configuration catalog is unavailable."]
|
|
186
|
-
};
|
|
123
|
+
return {
|
|
124
|
+
schemaVersion: 1,
|
|
125
|
+
agentId: agent.id,
|
|
126
|
+
adapterId: agent.adapterId,
|
|
127
|
+
models: [],
|
|
128
|
+
fields: staticAgentConfigurationFields(agent.adapterId),
|
|
129
|
+
warnings: ["Runtime configuration catalog is unavailable.", STATIC_CONFIGURATION_NOTICE]
|
|
130
|
+
};
|
|
187
131
|
}
|
|
188
132
|
export function configurationField(catalog, key) {
|
|
189
133
|
return catalog.fields.find((candidate) => candidate.key === key);
|
|
@@ -232,9 +176,6 @@ export function validateAgentLaunchConfiguration(catalog, config) {
|
|
|
232
176
|
+ `Inspect current options: yui config agent capabilities ${catalog.agentId}.`);
|
|
233
177
|
}
|
|
234
178
|
}
|
|
235
|
-
function field(key, choices, allowCustom) {
|
|
236
|
-
return { key, choices, allowCustom };
|
|
237
|
-
}
|
|
238
179
|
function catalogFingerprint(input, environment) {
|
|
239
180
|
const bindings = input.agent.environment.map((binding) => ({
|
|
240
181
|
target: binding.target,
|
|
@@ -259,6 +200,10 @@ function catalogFingerprint(input, environment) {
|
|
|
259
200
|
}
|
|
260
201
|
: null;
|
|
261
202
|
return createHash("sha256").update(JSON.stringify({
|
|
203
|
+
// Old derived caches may contain guessed help values. They cannot establish
|
|
204
|
+
// the current discovery contract. No durable record or cache shape changes.
|
|
205
|
+
discoveryContract: "explicit-native-enumeration",
|
|
206
|
+
agentId: input.agent.id,
|
|
262
207
|
// The component, not just the plan: two ACP products answer the same
|
|
263
208
|
// handshake differently, so a cache keyed on the plan alone would serve one
|
|
264
209
|
// product's capabilities for the other.
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
// These are Yui's accepted configuration values, not a native capability probe.
|
|
2
|
+
// Keep existing explicit configuration legal; current native choices come from
|
|
3
|
+
// help and requirements, and may be a smaller set.
|
|
4
|
+
export const CODEX_SANDBOXES = ["read-only", "workspace-write", "danger-full-access"];
|
|
5
|
+
export const CODEX_APPROVALS = ["untrusted", "on-request", "never"];
|
|
6
|
+
export const STATIC_CONFIGURATION_NOTICE = "Static adapter configuration contracts are not native enumeration or proof of Provider acceptance; see field reasons.";
|
|
7
|
+
export function staticAgentConfigurationFields(id) {
|
|
8
|
+
const fields = [
|
|
9
|
+
staticField("model", [], true, "Model values require native metadata; custom IDs remain explicit input."),
|
|
10
|
+
staticField("effort", [], true, "Effort values depend on the selected native model."),
|
|
11
|
+
staticField("permission.strategy", ["default", "bypass", "configured"], false, "These are Yui strategies, not Provider modes.")
|
|
12
|
+
];
|
|
13
|
+
if (id === "codex")
|
|
14
|
+
return [
|
|
15
|
+
...fields,
|
|
16
|
+
staticField("permission.sandbox", CODEX_SANDBOXES, false),
|
|
17
|
+
staticField("permission.approval", CODEX_APPROVALS, false, "Native versions may retire values accepted by the adapter."),
|
|
18
|
+
staticField("search", ["true"], false),
|
|
19
|
+
staticField("profile", [], true),
|
|
20
|
+
staticField("additionalDirectories", [], true)
|
|
21
|
+
];
|
|
22
|
+
if (id === "acp")
|
|
23
|
+
return [
|
|
24
|
+
...fields,
|
|
25
|
+
staticField("permission.mode", [], true, "Mode IDs are enumerated per Session and checked at launch."),
|
|
26
|
+
staticField("additionalDirectories", [], true, "Delivery requires the Agent's initialize capability; it is unverified offline.")
|
|
27
|
+
];
|
|
28
|
+
return [
|
|
29
|
+
...fields,
|
|
30
|
+
staticField("permission.mode", [], true, "No native modes were enumerated."),
|
|
31
|
+
staticField("permission.allowedTools", [], true),
|
|
32
|
+
staticField("permission.disallowedTools", [], true),
|
|
33
|
+
staticField("settingsSources", ["user", "project", "local"], false),
|
|
34
|
+
staticField("settingsFile", [], true),
|
|
35
|
+
staticField("additionalDirectories", [], true)
|
|
36
|
+
];
|
|
37
|
+
}
|
|
38
|
+
function staticField(key, values, allowCustom, detail = "") {
|
|
39
|
+
return {
|
|
40
|
+
key, choices: values.map(value => ({ value, label: value })), allowCustom,
|
|
41
|
+
reason: `Static adapter configuration contract; native support is unverified.${detail ? ` ${detail}` : ""}`
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/** One help interpretation shared by installation inspection and live probes. */
|
|
45
|
+
export function configurationFieldsFromHelp(id, help) {
|
|
46
|
+
const fields = staticAgentConfigurationFields(id);
|
|
47
|
+
if (id === "acp")
|
|
48
|
+
return fields;
|
|
49
|
+
const bypassFlag = id === "codex"
|
|
50
|
+
? "--dangerously-bypass-approvals-and-sandbox" : "--dangerously-skip-permissions";
|
|
51
|
+
const replacements = [
|
|
52
|
+
{
|
|
53
|
+
...staticField("permission.strategy", [
|
|
54
|
+
"default", ...(configurationFlagAvailable(help, bypassFlag) ? ["bypass"] : []), "configured"
|
|
55
|
+
], false),
|
|
56
|
+
reason: `Static Yui strategies; ${bypassFlag} ${configurationFlagAvailable(help, bypassFlag) ? "reported" : "not reported"} by native --help. This does not confirm account policy.`
|
|
57
|
+
},
|
|
58
|
+
...(id === "codex"
|
|
59
|
+
? [
|
|
60
|
+
helpChoiceField("permission.sandbox", help, "--sandbox", false, CODEX_SANDBOXES),
|
|
61
|
+
helpChoiceField("permission.approval", help, "--ask-for-approval", false, CODEX_APPROVALS)
|
|
62
|
+
]
|
|
63
|
+
: [
|
|
64
|
+
helpChoiceField("permission.mode", help, "--permission-mode", true),
|
|
65
|
+
helpChoiceField("effort", help, "--effort", true),
|
|
66
|
+
// The CLI's description often names settings sources without an enum.
|
|
67
|
+
// Retain the explicit static contract, never relabel it as discovery.
|
|
68
|
+
...(configurationHelpChoices(help, "--setting-sources").length === 0 ? []
|
|
69
|
+
: [helpChoiceField("settingsSources", help, "--setting-sources", false)])
|
|
70
|
+
])
|
|
71
|
+
];
|
|
72
|
+
const byKey = new Map(replacements.map(value => [value.key, value]));
|
|
73
|
+
return fields.map(value => byKey.get(value.key) ?? value);
|
|
74
|
+
}
|
|
75
|
+
function helpChoiceField(key, help, flag, allowCustom, supported) {
|
|
76
|
+
const reported = configurationHelpChoices(help, flag);
|
|
77
|
+
const values = supported === undefined ? reported : reported.filter(value => supported.includes(value));
|
|
78
|
+
return {
|
|
79
|
+
key, choices: values.map(value => ({ value, label: value })), allowCustom,
|
|
80
|
+
available: values.length > 0,
|
|
81
|
+
reason: values.length > 0
|
|
82
|
+
? `Native --help ${flag} enumeration${supported === undefined ? "" : ", limited to adapter-supported values"}.`
|
|
83
|
+
: `Native enumeration unavailable: ${flag} values were not reported or are not supported by this adapter.`
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
export function configurationFieldWarnings(fields) {
|
|
87
|
+
return [
|
|
88
|
+
STATIC_CONFIGURATION_NOTICE,
|
|
89
|
+
...fields.filter(field => field.available === false)
|
|
90
|
+
.map(field => `${field.key}: ${field.reason ?? "Native capability unavailable."}`)
|
|
91
|
+
];
|
|
92
|
+
}
|
|
93
|
+
/** Read only an explicit native enum. Prose examples and missing fields are not choices. */
|
|
94
|
+
export function configurationHelpChoices(help, flag) {
|
|
95
|
+
const section = helpSection(help, flag);
|
|
96
|
+
if (section === undefined)
|
|
97
|
+
return [];
|
|
98
|
+
const inline = /(?:\[possible values:\s*([^\]]+)\]|\(choices:\s*([^)]*)\))/i.exec(section);
|
|
99
|
+
const declared = inline?.[1] ?? inline?.[2];
|
|
100
|
+
const bullets = declared === undefined && /Possible values:/i.test(section)
|
|
101
|
+
? [...section.matchAll(/^\s*-\s*([\w.+-]+)\s*:/gm)].map(match => match[1] ?? "")
|
|
102
|
+
: [];
|
|
103
|
+
return [...new Set((declared ?? bullets.join(",")).replace(/["']/g, "").split(",")
|
|
104
|
+
.map(value => value.trim().replace(/\.$/, "")).filter(value => /^[\w.+-]+$/.test(value)))];
|
|
105
|
+
}
|
|
106
|
+
export function configurationFlagAvailable(help, flag) {
|
|
107
|
+
return helpSection(help, flag) !== undefined;
|
|
108
|
+
}
|
|
109
|
+
function helpSection(help, flag) {
|
|
110
|
+
const lines = help.replace(/\r\n/g, "\n").split("\n");
|
|
111
|
+
const escaped = flag.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
112
|
+
const declaration = new RegExp(`^\\s*(?:-[A-Za-z],?\\s+)?${escaped}(?=[\\s=,<\\[]|$)`);
|
|
113
|
+
const start = lines.findIndex(line => declaration.test(line));
|
|
114
|
+
if (start < 0)
|
|
115
|
+
return undefined;
|
|
116
|
+
let end = start + 1;
|
|
117
|
+
while (end < lines.length && !/^\s*(?:-[A-Za-z],?\s+|--)[\w-]/.test(lines[end]))
|
|
118
|
+
end++;
|
|
119
|
+
return lines.slice(start, end).join("\n");
|
|
120
|
+
}
|