mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-21 10:16:44 +00:00
feat(setup): rename Crestodian to OpenClaw system agent
User-facing name is now OpenClaw (the system speaks); internal code name is system-agent. Gateway methods crestodian.* -> openclaw.chat/openclaw.setup.*, agent tool -> openclaw, reserved agent ids openclaw + retired crestodian. openclaw setup routes: onboarding flags -> onboard, -m/--yes -> system agent, bare configured interactive -> OpenClaw chat, unconfigured -> onboarding. Hidden crestodian CLI and /crestodian TUI aliases kept; docs moved to docs/cli/openclaw.md with redirect stub. macOS/Android strings in lockstep. Refs #107237
This commit is contained in:
@@ -135,10 +135,10 @@ enum class GatewayMethod(
|
||||
PluginApprovalResolve("plugin.approval.resolve"),
|
||||
PluginsUiDescriptors("plugins.uiDescriptors"),
|
||||
PluginsSessionAction("plugins.sessionAction"),
|
||||
CrestodianChat("crestodian.chat"),
|
||||
CrestodianSetupDetect("crestodian.setup.detect"),
|
||||
CrestodianSetupActivate("crestodian.setup.activate"),
|
||||
CrestodianSetupAuthStart("crestodian.setup.auth.start"),
|
||||
SystemAgentChat("openclaw.chat"),
|
||||
SystemAgentSetupDetect("openclaw.setup.detect"),
|
||||
SystemAgentSetupActivate("openclaw.setup.activate"),
|
||||
SystemAgentSetupAuthStart("openclaw.setup.auth.start"),
|
||||
WizardStart("wizard.start"),
|
||||
WizardNext("wizard.next"),
|
||||
WizardCancel("wizard.cancel"),
|
||||
@@ -360,7 +360,7 @@ enum class GatewayMethod(
|
||||
GatewaySuspendResume("gateway.suspend.resume"),
|
||||
ChatToolTitles("chat.toolTitles"),
|
||||
SessionsDiff("sessions.diff"),
|
||||
CrestodianSetupVerify("crestodian.setup.verify"),
|
||||
SystemAgentSetupVerify("openclaw.setup.verify"),
|
||||
EnvironmentsCreate("environments.create"),
|
||||
EnvironmentsDestroy("environments.destroy"),
|
||||
SessionsCatalogList("sessions.catalog.list"),
|
||||
|
||||
@@ -383,7 +383,7 @@ internal val nativeStringResourceIds: Map<String, Int> =
|
||||
"Create" to R.string.native_4759498ac2a719c6,
|
||||
"Create Goal" to R.string.native_1d5e35075a1017e0,
|
||||
"Created" to R.string.native_d70b9e24bca26b40,
|
||||
"Crestodian" to R.string.native_8c28828d13dbb711,
|
||||
"OpenClaw" to R.string.native_8c28828d13dbb711,
|
||||
"Cron" to R.string.native_dd9d24965dbedc02,
|
||||
"Cron action failed." to R.string.native_8cac4b77b9c54695,
|
||||
"Cron changes require operator.admin access." to R.string.native_a1bca979bc69befb,
|
||||
|
||||
@@ -790,7 +790,7 @@
|
||||
<string name="native_8b8fc46055e32971" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Open Screen"</string>
|
||||
<string name="native_8bb7ec15b1e350a5" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Inspecting"</string>
|
||||
<string name="native_8bf253a125f66985" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Scan or paste a setup code to add another gateway."</string>
|
||||
<string name="native_8c28828d13dbb711" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Crestodian"</string>
|
||||
<string name="native_8c28828d13dbb711" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"OpenClaw"</string>
|
||||
<string name="native_8c4a3896e8f06d27" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"TLS timed out"</string>
|
||||
<string name="native_8c93c68056cde54d" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Gateway paired. Checking node capability approval."</string>
|
||||
<string name="native_8ca344247c18317e" formatted="false" tools:ignore="Typos,TypographyDashes,TypographyEllipsis">"Motion"</string>
|
||||
|
||||
@@ -6,7 +6,7 @@ let launchdLabel = "ai.openclaw.mac"
|
||||
let gatewayLaunchdLabel = "ai.openclaw.gateway"
|
||||
let onboardingVersionKey = "openclaw.onboardingVersion"
|
||||
let onboardingSeenKey = "openclaw.onboardingSeen"
|
||||
let onboardingCrestodianPendingKey = "openclaw.onboardingCrestodianPending"
|
||||
let onboardingSystemAgentPendingKey = "openclaw.onboardingSystemAgentPending"
|
||||
let currentOnboardingVersion = 8
|
||||
let pauseDefaultsKey = "openclaw.pauseEnabled"
|
||||
let iconAnimationsEnabledKey = "openclaw.iconAnimationsEnabled"
|
||||
|
||||
@@ -61,7 +61,7 @@ enum GatewayDiscoveryPreferences {
|
||||
} else {
|
||||
defaultRemotePort
|
||||
}
|
||||
return OnboardingCrestodianResumeStore.routeIdentity(
|
||||
return OnboardingSystemAgentResumeStore.routeIdentity(
|
||||
connectionMode: .remote,
|
||||
preferredGatewayID: nil,
|
||||
remoteTransport: remoteTransport,
|
||||
@@ -86,7 +86,7 @@ enum GatewayDiscoveryPreferences {
|
||||
remoteURL: remoteURL,
|
||||
remoteTarget: remoteTarget)
|
||||
}
|
||||
return OnboardingCrestodianResumeStore.routeIdentity(
|
||||
return OnboardingSystemAgentResumeStore.routeIdentity(
|
||||
connectionMode: connectionMode,
|
||||
preferredGatewayID: nil,
|
||||
remoteTransport: remoteTransport,
|
||||
|
||||
@@ -552,14 +552,14 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
static func shouldOpenDashboardInsteadOfOnboarding(
|
||||
connectionMode: AppState.ConnectionMode,
|
||||
onboardingSeen: Bool,
|
||||
crestodianResumePending: Bool,
|
||||
systemAgentResumePending: Bool,
|
||||
gatewayConnected: Bool,
|
||||
configuredInferenceModel: String?) -> Bool
|
||||
{
|
||||
let model = configuredInferenceModel?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
return connectionMode != .unconfigured &&
|
||||
!onboardingSeen &&
|
||||
!crestodianResumePending &&
|
||||
!systemAgentResumePending &&
|
||||
gatewayConnected &&
|
||||
model?.isEmpty == false
|
||||
}
|
||||
@@ -592,7 +592,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
|
||||
private func scheduleFirstRunOnboardingIfNeeded(gatewayConnected: Bool) async {
|
||||
let connectionMode = AppStateStore.shared.connectionMode
|
||||
let expectedRouteIdentity = OnboardingCrestodianResumeStore.selectedRouteIdentity()
|
||||
let expectedRouteIdentity = OnboardingSystemAgentResumeStore.selectedRouteIdentity()
|
||||
var configuredInferenceModel: String?
|
||||
if connectionMode != .unconfigured,
|
||||
!AppStateStore.shared.onboardingSeen,
|
||||
@@ -603,7 +603,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
return
|
||||
}
|
||||
// Bind inference discovery to the connected route. A socket without a
|
||||
// default-agent model cannot run Crestodian and must stay in onboarding.
|
||||
// default-agent model cannot run OpenClaw and must stay in onboarding.
|
||||
do {
|
||||
configuredInferenceModel = try await GatewayConnection.shared.configuredInferenceModel(
|
||||
ifCurrentRoute: route)
|
||||
@@ -614,7 +614,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
return
|
||||
}
|
||||
let gatewayRouteIsCurrent = await GatewayConnection.shared.isCurrentRoute(route)
|
||||
let currentRouteIdentity = OnboardingCrestodianResumeStore.selectedRouteIdentity()
|
||||
let currentRouteIdentity = OnboardingSystemAgentResumeStore.selectedRouteIdentity()
|
||||
guard Self.isCurrentFirstRunInferenceProbe(
|
||||
expectedConnectionMode: connectionMode,
|
||||
currentConnectionMode: AppStateStore.shared.connectionMode,
|
||||
@@ -627,11 +627,11 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
}
|
||||
}
|
||||
let onboardingSeen = AppStateStore.shared.onboardingSeen
|
||||
let crestodianResumePending = OnboardingCrestodianResumeStore.isPending(for: expectedRouteIdentity)
|
||||
let systemAgentResumePending = OnboardingSystemAgentResumeStore.isPending(for: expectedRouteIdentity)
|
||||
let shouldOpenDashboard = Self.shouldOpenDashboardInsteadOfOnboarding(
|
||||
connectionMode: connectionMode,
|
||||
onboardingSeen: onboardingSeen,
|
||||
crestodianResumePending: crestodianResumePending,
|
||||
systemAgentResumePending: systemAgentResumePending,
|
||||
gatewayConnected: gatewayConnected,
|
||||
configuredInferenceModel: configuredInferenceModel)
|
||||
if connectionMode != .unconfigured, onboardingSeen || shouldOpenDashboard {
|
||||
@@ -650,7 +650,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
private func scheduleFirstRunOnboardingRecovery() {
|
||||
self.scheduleFirstRunOnboardingPresentation(
|
||||
expectedConnectionMode: AppStateStore.shared.connectionMode,
|
||||
expectedRouteIdentity: OnboardingCrestodianResumeStore.selectedRouteIdentity())
|
||||
expectedRouteIdentity: OnboardingSystemAgentResumeStore.selectedRouteIdentity())
|
||||
}
|
||||
|
||||
private func scheduleFirstRunOnboardingPresentation(
|
||||
@@ -661,7 +661,7 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
|
||||
let shouldShow = seenVersion < currentOnboardingVersion || !AppStateStore.shared.onboardingSeen
|
||||
guard shouldShow else { return }
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.6) {
|
||||
let currentRouteIdentity = OnboardingCrestodianResumeStore.selectedRouteIdentity()
|
||||
let currentRouteIdentity = OnboardingSystemAgentResumeStore.selectedRouteIdentity()
|
||||
guard Self.shouldPresentScheduledFirstRunOnboarding(
|
||||
expectedConnectionMode: expectedConnectionMode,
|
||||
currentConnectionMode: AppStateStore.shared.connectionMode,
|
||||
|
||||
@@ -15,7 +15,7 @@ enum RemoteOnboardingProbeState: Equatable {
|
||||
case failed(String)
|
||||
}
|
||||
|
||||
enum OnboardingCrestodianResumeStore {
|
||||
enum OnboardingSystemAgentResumeStore {
|
||||
struct ActivationOwner: Equatable {
|
||||
let id: String
|
||||
let routeFingerprint: String
|
||||
@@ -130,7 +130,7 @@ enum OnboardingCrestodianResumeStore {
|
||||
static func markPending(
|
||||
routeIdentity: String?,
|
||||
activationOwner: ActivationOwner? = nil,
|
||||
activationTimeoutMs: Double = OnboardingCrestodianResumeStore.maximumActivationTimeoutMs,
|
||||
activationTimeoutMs: Double = OnboardingSystemAgentResumeStore.maximumActivationTimeoutMs,
|
||||
defaults: UserDefaults = .standard,
|
||||
now: Date = Date())
|
||||
-> Date?
|
||||
@@ -264,14 +264,14 @@ enum OnboardingCrestodianResumeStore {
|
||||
}
|
||||
|
||||
static func clear(defaults: UserDefaults = .standard) {
|
||||
defaults.removeObject(forKey: onboardingCrestodianPendingKey)
|
||||
defaults.removeObject(forKey: onboardingSystemAgentPendingKey)
|
||||
}
|
||||
|
||||
private static func loadRecords(
|
||||
defaults: UserDefaults,
|
||||
now: Date = Date()) -> [String: Record]
|
||||
{
|
||||
guard let stored = defaults.object(forKey: onboardingCrestodianPendingKey) else { return [:] }
|
||||
guard let stored = defaults.object(forKey: onboardingSystemAgentPendingKey) else { return [:] }
|
||||
if let legacyRoute = normalized(stored as? String) {
|
||||
let records = [legacyRoute: conservativeLegacyRecord(now: now)]
|
||||
self.writeRecords(records, defaults: defaults)
|
||||
@@ -400,7 +400,7 @@ enum OnboardingCrestodianResumeStore {
|
||||
}
|
||||
defaults.set(
|
||||
["version": self.recordVersion, "records": payload],
|
||||
forKey: onboardingCrestodianPendingKey)
|
||||
forKey: onboardingSystemAgentPendingKey)
|
||||
}
|
||||
|
||||
private static func date(_ value: Any?) -> Date? {
|
||||
@@ -609,7 +609,7 @@ struct OnboardingView: View {
|
||||
@State var suppressRemoteProbeReset = false
|
||||
@State var gatewayDiscovery: GatewayDiscoveryModel
|
||||
@State var onboardingSkillsModel = SkillsSettingsModel()
|
||||
@State var crestodianState = OnboardingCrestodianChatState()
|
||||
@State var systemAgentState = OnboardingSystemAgentChatState()
|
||||
@State var aiSetup = OnboardingAISetupModel()
|
||||
@State var configuredGatewayProbe = OnboardingConfiguredGatewayProbe()
|
||||
@State var didLoadOnboardingSkills = false
|
||||
@@ -617,7 +617,7 @@ struct OnboardingView: View {
|
||||
@State var defaultsToLocalGateway: Bool
|
||||
@Bindable var state: AppState
|
||||
var permissionMonitor: PermissionMonitor
|
||||
let crestodianDefaults: UserDefaults
|
||||
let systemAgentDefaults: UserDefaults
|
||||
let aiSetupRouteIdentityProvider: @MainActor () -> String?
|
||||
let gatewaySelectionPersister: @MainActor () -> Bool
|
||||
|
||||
@@ -755,16 +755,16 @@ struct OnboardingView: View {
|
||||
localDisplayName: InstanceIdentity.displayName,
|
||||
filterLocalGateways: false),
|
||||
aiSetupGateway: GatewayConnection = .shared,
|
||||
crestodianDefaults: UserDefaults = .standard,
|
||||
systemAgentDefaults: UserDefaults = .standard,
|
||||
aiSetupRouteIdentityProvider: (@MainActor () -> String?)? = nil,
|
||||
configuredGatewayProbeTimeoutMs: Double = 15000,
|
||||
gatewaySelectionPersister: (@MainActor () -> Bool)? = nil)
|
||||
{
|
||||
self.state = state
|
||||
self.permissionMonitor = permissionMonitor
|
||||
self.crestodianDefaults = crestodianDefaults
|
||||
self.systemAgentDefaults = systemAgentDefaults
|
||||
let routeIdentityProvider = aiSetupRouteIdentityProvider ?? {
|
||||
OnboardingCrestodianResumeStore.selectedRouteIdentity(state: state)
|
||||
OnboardingSystemAgentResumeStore.selectedRouteIdentity(state: state)
|
||||
}
|
||||
self.aiSetupRouteIdentityProvider = routeIdentityProvider
|
||||
self.gatewaySelectionPersister = gatewaySelectionPersister ?? {
|
||||
@@ -775,7 +775,7 @@ struct OnboardingView: View {
|
||||
_gatewayDiscovery = State(initialValue: discoveryModel)
|
||||
_aiSetup = State(initialValue: OnboardingAISetupModel(
|
||||
gateway: aiSetupGateway,
|
||||
defaults: crestodianDefaults,
|
||||
defaults: systemAgentDefaults,
|
||||
routeIdentityProvider: routeIdentityProvider))
|
||||
_configuredGatewayProbe = State(
|
||||
initialValue: OnboardingConfiguredGatewayProbe(
|
||||
|
||||
@@ -6,7 +6,7 @@ import OpenClawProtocol
|
||||
|
||||
/// Structured "Connect your AI" onboarding step.
|
||||
///
|
||||
/// Drives the gateway's `crestodian.setup.detect` / `crestodian.setup.activate`
|
||||
/// Drives the gateway's `openclaw.setup.detect` / `openclaw.setup.activate`
|
||||
/// RPCs: detect reusable AI access (Claude Code, Codex, Gemini logins, API
|
||||
/// keys), live-test candidates in the detected order, and automatically fall
|
||||
/// through when one fails. Config is only written server-side after a
|
||||
@@ -85,7 +85,7 @@ final class OnboardingAISetupModel {
|
||||
}
|
||||
|
||||
/// Once setup starts changing inference, its successful result belongs to
|
||||
/// Crestodian rather than the existing-Gateway onboarding bypass.
|
||||
/// OpenClaw rather than the existing-Gateway onboarding bypass.
|
||||
var ownsInferenceTransition: Bool {
|
||||
(self.phase == .detecting && !self.configuredGatewayProbeUnavailable) ||
|
||||
self.phase == .testing || self.manualTesting || self.authBusy || self.connected ||
|
||||
@@ -104,7 +104,7 @@ final class OnboardingAISetupModel {
|
||||
private var started = false
|
||||
private var attemptToken = UUID()
|
||||
@ObservationIgnored private var pendingVerification: PendingVerification?
|
||||
@ObservationIgnored private var pendingActivationOwner: OnboardingCrestodianResumeStore.ActivationOwner?
|
||||
@ObservationIgnored private var pendingActivationOwner: OnboardingSystemAgentResumeStore.ActivationOwner?
|
||||
@ObservationIgnored private var completedHandoff: CompletedHandoff?
|
||||
@ObservationIgnored private var pendingActivationRequiresFreshActivation = false
|
||||
@ObservationIgnored private var serverLease: GatewayConnection.ServerLease?
|
||||
@@ -131,14 +131,14 @@ final class OnboardingAISetupModel {
|
||||
|
||||
private struct CompletedHandoff {
|
||||
let routeIdentity: String
|
||||
let activationOwner: OnboardingCrestodianResumeStore.ActivationOwner?
|
||||
let activationOwner: OnboardingSystemAgentResumeStore.ActivationOwner?
|
||||
}
|
||||
|
||||
init(
|
||||
gateway: GatewayConnection = .shared,
|
||||
defaults: UserDefaults = .standard,
|
||||
routeIdentityProvider: @escaping @MainActor () -> String? = {
|
||||
OnboardingCrestodianResumeStore.selectedRouteIdentity()
|
||||
OnboardingSystemAgentResumeStore.selectedRouteIdentity()
|
||||
})
|
||||
{
|
||||
self.gateway = gateway
|
||||
@@ -254,11 +254,11 @@ final class OnboardingAISetupModel {
|
||||
// repeatedly. Keep the first attempt and let every caller await it.
|
||||
guard !self.ownsInferenceTransition else { return }
|
||||
let routeIdentity = self.routeIdentityProvider()
|
||||
let pendingState = OnboardingCrestodianResumeStore.pendingState(
|
||||
let pendingState = OnboardingSystemAgentResumeStore.pendingState(
|
||||
for: routeIdentity,
|
||||
defaults: self.defaults)
|
||||
let inMemoryOwner = self.pendingActivationOwner
|
||||
let restoredOwner = OnboardingCrestodianResumeStore.activationOwner(
|
||||
let restoredOwner = OnboardingSystemAgentResumeStore.activationOwner(
|
||||
for: routeIdentity,
|
||||
defaults: self.defaults)
|
||||
let activationOwner = inMemoryOwner ?? restoredOwner
|
||||
@@ -338,7 +338,7 @@ final class OnboardingAISetupModel {
|
||||
return .notConnected
|
||||
}
|
||||
guard activationOwner.routeFingerprint == currentFingerprint else {
|
||||
switch OnboardingCrestodianResumeStore.pendingState(
|
||||
switch OnboardingSystemAgentResumeStore.pendingState(
|
||||
for: context.routeIdentity,
|
||||
defaults: self.defaults)
|
||||
{
|
||||
@@ -353,7 +353,7 @@ final class OnboardingAISetupModel {
|
||||
case .activationExpired, .completed, .none:
|
||||
// No live mutation remains to overlap. Retire only this
|
||||
// owner, then let the replacement credentials start fresh.
|
||||
OnboardingCrestodianResumeStore.clear(
|
||||
OnboardingSystemAgentResumeStore.clear(
|
||||
ifOwnedBy: context.routeIdentity,
|
||||
activationOwner: activationOwner,
|
||||
defaults: self.defaults)
|
||||
@@ -367,7 +367,7 @@ final class OnboardingAISetupModel {
|
||||
}
|
||||
do {
|
||||
let data = try await gateway.request(
|
||||
method: "crestodian.setup.verify",
|
||||
method: "openclaw.setup.verify",
|
||||
params: [:],
|
||||
timeoutMs: 150_000,
|
||||
ifCurrentServerLease: lease)
|
||||
@@ -377,14 +377,14 @@ final class OnboardingAISetupModel {
|
||||
else { return .superseded }
|
||||
let result = try JSONDecoder().decode(ActivateResult.self, from: data)
|
||||
if result.ok, let modelRef = result.modelRef {
|
||||
let pendingState = OnboardingCrestodianResumeStore.pendingState(
|
||||
let pendingState = OnboardingSystemAgentResumeStore.pendingState(
|
||||
for: context.routeIdentity,
|
||||
defaults: self.defaults)
|
||||
switch pendingState {
|
||||
case let .activating(deadline), let .verified(deadline):
|
||||
// This proves inference works, but not that the dropped
|
||||
// activation stopped mutating. Preserve its deadline.
|
||||
OnboardingCrestodianResumeStore.markVerified(
|
||||
OnboardingSystemAgentResumeStore.markVerified(
|
||||
ifOwnedBy: context.routeIdentity,
|
||||
activationOwner: self.pendingActivationOwner,
|
||||
defaults: self.defaults)
|
||||
@@ -438,7 +438,7 @@ final class OnboardingAISetupModel {
|
||||
private func pendingVerificationFailureOutcome(
|
||||
context: AttemptContext) -> PendingVerificationOutcome
|
||||
{
|
||||
switch OnboardingCrestodianResumeStore.pendingState(
|
||||
switch OnboardingSystemAgentResumeStore.pendingState(
|
||||
for: context.routeIdentity,
|
||||
defaults: self.defaults)
|
||||
{
|
||||
@@ -446,7 +446,7 @@ final class OnboardingAISetupModel {
|
||||
// The dropped activation may still be writing config or credentials.
|
||||
// Verification may repeat, but mutation stays blocked until its lease ends.
|
||||
if let activationOwner = pendingActivationOwner,
|
||||
!OnboardingCrestodianResumeStore.isOwned(
|
||||
!OnboardingSystemAgentResumeStore.isOwned(
|
||||
by: activationOwner,
|
||||
for: context.routeIdentity,
|
||||
defaults: defaults)
|
||||
@@ -472,7 +472,7 @@ final class OnboardingAISetupModel {
|
||||
}
|
||||
|
||||
private func retainCompletedReceiptForRetry(context: AttemptContext) {
|
||||
self.pendingActivationOwner = OnboardingCrestodianResumeStore.activationOwner(
|
||||
self.pendingActivationOwner = OnboardingSystemAgentResumeStore.activationOwner(
|
||||
for: context.routeIdentity,
|
||||
defaults: self.defaults)
|
||||
self.pendingActivationRequiresFreshActivation = true
|
||||
@@ -480,7 +480,7 @@ final class OnboardingAISetupModel {
|
||||
}
|
||||
|
||||
private func activePendingActivationDeadline(for routeIdentity: String) -> Date? {
|
||||
switch OnboardingCrestodianResumeStore.pendingState(
|
||||
switch OnboardingSystemAgentResumeStore.pendingState(
|
||||
for: routeIdentity,
|
||||
defaults: self.defaults)
|
||||
{
|
||||
@@ -502,17 +502,17 @@ final class OnboardingAISetupModel {
|
||||
|
||||
private func retainAmbiguousActivation(
|
||||
ifOwnedBy context: AttemptContext,
|
||||
activationOwner: OnboardingCrestodianResumeStore.ActivationOwner,
|
||||
activationOwner: OnboardingSystemAgentResumeStore.ActivationOwner,
|
||||
activationDeadline: Date)
|
||||
{
|
||||
guard isCurrentAttempt(context) else { return }
|
||||
self.pendingActivationVerification = true
|
||||
switch OnboardingCrestodianResumeStore.pendingState(
|
||||
switch OnboardingSystemAgentResumeStore.pendingState(
|
||||
for: context.routeIdentity,
|
||||
defaults: self.defaults)
|
||||
{
|
||||
case let .activating(deadline), let .verified(deadline):
|
||||
guard OnboardingCrestodianResumeStore.isOwned(
|
||||
guard OnboardingSystemAgentResumeStore.isOwned(
|
||||
by: activationOwner,
|
||||
for: context.routeIdentity,
|
||||
defaults: self.defaults)
|
||||
@@ -532,7 +532,7 @@ final class OnboardingAISetupModel {
|
||||
// A concurrent read-only probe can clear the marker while the
|
||||
// dispatched handler is still returning. Restore route ownership
|
||||
// before probing so failure or relaunch cannot start a duplicate.
|
||||
OnboardingCrestodianResumeStore.restorePending(
|
||||
OnboardingSystemAgentResumeStore.restorePending(
|
||||
routeIdentity: context.routeIdentity,
|
||||
activationOwner: activationOwner,
|
||||
deadline: activationDeadline,
|
||||
@@ -571,7 +571,7 @@ final class OnboardingAISetupModel {
|
||||
/// A replacement activation on the same route retains its own receipt.
|
||||
func clearCompletedHandoffIfOwned() {
|
||||
guard let completedHandoff else { return }
|
||||
OnboardingCrestodianResumeStore.clear(
|
||||
OnboardingSystemAgentResumeStore.clear(
|
||||
ifOwnedBy: completedHandoff.routeIdentity,
|
||||
activationOwner: completedHandoff.activationOwner,
|
||||
defaults: self.defaults)
|
||||
@@ -583,7 +583,7 @@ final class OnboardingAISetupModel {
|
||||
let authSessionToCancel = self.authSessionID
|
||||
let authServerLease = self.serverLease
|
||||
if clearPendingHandoff, let routeIdentity = routeIdentityProvider() {
|
||||
OnboardingCrestodianResumeStore.clear(
|
||||
OnboardingSystemAgentResumeStore.clear(
|
||||
ifOwnedBy: routeIdentity,
|
||||
activationOwner: self.pendingActivationOwner,
|
||||
defaults: self.defaults)
|
||||
@@ -662,7 +662,7 @@ extension OnboardingAISetupModel {
|
||||
let lease = try await gateway.acquireServerLease()
|
||||
guard self.isCurrentAttempt(context), !Task.isCancelled else { return }
|
||||
let data = try await gateway.request(
|
||||
method: "crestodian.setup.detect",
|
||||
method: "openclaw.setup.detect",
|
||||
params: [:],
|
||||
timeoutMs: 20000,
|
||||
ifCurrentServerLease: lease)
|
||||
@@ -748,10 +748,10 @@ extension OnboardingAISetupModel {
|
||||
|
||||
private func clearPendingHandoff(
|
||||
ifOwnedBy context: AttemptContext,
|
||||
activationOwner: OnboardingCrestodianResumeStore.ActivationOwner? = nil)
|
||||
activationOwner: OnboardingSystemAgentResumeStore.ActivationOwner? = nil)
|
||||
{
|
||||
guard self.isCurrentAttempt(context) else { return }
|
||||
OnboardingCrestodianResumeStore.clear(
|
||||
OnboardingSystemAgentResumeStore.clear(
|
||||
ifOwnedBy: context.routeIdentity,
|
||||
activationOwner: activationOwner ?? self.pendingActivationOwner,
|
||||
defaults: self.defaults)
|
||||
@@ -822,7 +822,7 @@ extension OnboardingAISetupModel {
|
||||
self.phase = .testing
|
||||
self.statuses[kind] = .testing
|
||||
guard let supportsExactModel = await gateway.supportsServerCapability(
|
||||
.crestodianSetupModelRef,
|
||||
.systemAgentSetupModelRef,
|
||||
ifCurrentServerLease: lease),
|
||||
isCurrentAttempt(context),
|
||||
!Task.isCancelled
|
||||
@@ -844,14 +844,14 @@ extension OnboardingAISetupModel {
|
||||
kind: kind,
|
||||
modelRef: candidate.modelRef,
|
||||
supportsExactModel: supportsExactModel)
|
||||
let activationOwner = OnboardingCrestodianResumeStore.ActivationOwner(
|
||||
let activationOwner = OnboardingSystemAgentResumeStore.ActivationOwner(
|
||||
id: UUID().uuidString,
|
||||
routeFingerprint: routeFingerprint)
|
||||
self.pendingActivationOwner = activationOwner
|
||||
self.pendingActivationRequiresFreshActivation = true
|
||||
// Activation can persist before the response reaches the app. Cover the
|
||||
// whole ambiguous window so relaunch can inspect the actual Gateway state.
|
||||
guard let activationDeadline = OnboardingCrestodianResumeStore.markPending(
|
||||
guard let activationDeadline = OnboardingSystemAgentResumeStore.markPending(
|
||||
routeIdentity: context.routeIdentity,
|
||||
activationOwner: activationOwner,
|
||||
activationTimeoutMs: requestTimeoutMs,
|
||||
@@ -869,7 +869,7 @@ extension OnboardingAISetupModel {
|
||||
}
|
||||
do {
|
||||
let data = try await gateway.request(
|
||||
method: "crestodian.setup.activate",
|
||||
method: "openclaw.setup.activate",
|
||||
params: params,
|
||||
timeoutMs: requestTimeoutMs,
|
||||
ifCurrentServerLease: lease)
|
||||
@@ -877,7 +877,7 @@ extension OnboardingAISetupModel {
|
||||
guard self.isCurrentAttempt(context), !Task.isCancelled else { return }
|
||||
guard await self.gateway.isCurrentServerLease(lease) else {
|
||||
if result.ok,
|
||||
OnboardingCrestodianResumeStore.markCompleted(
|
||||
OnboardingSystemAgentResumeStore.markCompleted(
|
||||
ifOwnedBy: context.routeIdentity,
|
||||
activationOwner: activationOwner,
|
||||
defaults: self.defaults)
|
||||
@@ -948,7 +948,7 @@ extension OnboardingAISetupModel {
|
||||
private func reconcileActivationAfterGatewayRestart(
|
||||
kind: String,
|
||||
context: AttemptContext,
|
||||
activationOwner: OnboardingCrestodianResumeStore.ActivationOwner,
|
||||
activationOwner: OnboardingSystemAgentResumeStore.ActivationOwner,
|
||||
before: PersistedActivationState?,
|
||||
originalServerLease: GatewayConnection.ServerLease) async -> Bool
|
||||
{
|
||||
@@ -997,7 +997,7 @@ extension OnboardingAISetupModel {
|
||||
private func reconcilePersistedActivation(
|
||||
kind: String,
|
||||
context: AttemptContext,
|
||||
activationOwner: OnboardingCrestodianResumeStore.ActivationOwner,
|
||||
activationOwner: OnboardingSystemAgentResumeStore.ActivationOwner,
|
||||
before: PersistedActivationState?,
|
||||
serverLease: GatewayConnection.ServerLease,
|
||||
timeoutMs: Int) async -> Bool
|
||||
@@ -1006,7 +1006,7 @@ extension OnboardingAISetupModel {
|
||||
let expectedModel = candidates.first(where: { $0.kind == kind })?.modelRef,
|
||||
isCurrentAttempt(context),
|
||||
!Task.isCancelled,
|
||||
OnboardingCrestodianResumeStore.isOwned(
|
||||
OnboardingSystemAgentResumeStore.isOwned(
|
||||
by: activationOwner,
|
||||
for: context.routeIdentity,
|
||||
defaults: defaults),
|
||||
@@ -1014,7 +1014,7 @@ extension OnboardingAISetupModel {
|
||||
activationOwner.routeFingerprint
|
||||
else { return false }
|
||||
guard let detectData = try? await gateway.request(
|
||||
method: "crestodian.setup.detect",
|
||||
method: "openclaw.setup.detect",
|
||||
params: [:],
|
||||
timeoutMs: Double(timeoutMs),
|
||||
ifCurrentServerLease: serverLease),
|
||||
@@ -1028,7 +1028,7 @@ extension OnboardingAISetupModel {
|
||||
after: detection.persistedActivationState)
|
||||
else { return false }
|
||||
guard let verifyData = try? await gateway.request(
|
||||
method: "crestodian.setup.verify",
|
||||
method: "openclaw.setup.verify",
|
||||
params: [:],
|
||||
timeoutMs: Double(timeoutMs),
|
||||
ifCurrentServerLease: serverLease),
|
||||
@@ -1074,7 +1074,7 @@ extension OnboardingAISetupModel {
|
||||
Task {
|
||||
do {
|
||||
let data = try await self.gateway.request(
|
||||
method: "crestodian.setup.auth.start",
|
||||
method: "openclaw.setup.auth.start",
|
||||
params: [
|
||||
"sessionId": AnyCodable(authSessionID),
|
||||
"authChoice": AnyCodable(option.id),
|
||||
@@ -1298,7 +1298,7 @@ extension OnboardingAISetupModel {
|
||||
lease = replacement
|
||||
}
|
||||
guard let data = try? await gateway.request(
|
||||
method: "crestodian.setup.detect",
|
||||
method: "openclaw.setup.detect",
|
||||
params: [:],
|
||||
timeoutMs: 10000,
|
||||
ifCurrentServerLease: lease),
|
||||
@@ -1394,14 +1394,14 @@ extension OnboardingAISetupModel {
|
||||
}
|
||||
guard self.isCurrentAttempt(context), !Task.isCancelled else { return }
|
||||
let requestTimeoutMs = Self.activationRequestTimeoutMs(for: "api-key")
|
||||
let activationOwner = OnboardingCrestodianResumeStore.ActivationOwner(
|
||||
let activationOwner = OnboardingSystemAgentResumeStore.ActivationOwner(
|
||||
id: UUID().uuidString,
|
||||
routeFingerprint: routeFingerprint)
|
||||
self.pendingActivationOwner = activationOwner
|
||||
self.pendingActivationRequiresFreshActivation = true
|
||||
// Manual activation has the same persist-before-response ambiguity as
|
||||
// detected candidates, so relaunch must inspect exact Gateway truth.
|
||||
guard let activationDeadline = OnboardingCrestodianResumeStore.markPending(
|
||||
guard let activationDeadline = OnboardingSystemAgentResumeStore.markPending(
|
||||
routeIdentity: context.routeIdentity,
|
||||
activationOwner: activationOwner,
|
||||
activationTimeoutMs: requestTimeoutMs,
|
||||
@@ -1417,7 +1417,7 @@ extension OnboardingAISetupModel {
|
||||
}
|
||||
do {
|
||||
let data = try await gateway.request(
|
||||
method: "crestodian.setup.activate",
|
||||
method: "openclaw.setup.activate",
|
||||
params: [
|
||||
"kind": AnyCodable("api-key"),
|
||||
"authChoice": AnyCodable(provider.id),
|
||||
@@ -1429,7 +1429,7 @@ extension OnboardingAISetupModel {
|
||||
guard self.isCurrentAttempt(context), !Task.isCancelled else { return }
|
||||
guard await self.gateway.isCurrentServerLease(lease) else {
|
||||
if result.ok,
|
||||
OnboardingCrestodianResumeStore.markCompleted(
|
||||
OnboardingSystemAgentResumeStore.markCompleted(
|
||||
ifOwnedBy: context.routeIdentity,
|
||||
activationOwner: activationOwner,
|
||||
defaults: self.defaults)
|
||||
@@ -1493,11 +1493,11 @@ extension OnboardingAISetupModel {
|
||||
private func finishConnected(
|
||||
kind: String,
|
||||
result: ActivateResult,
|
||||
activationOwner: OnboardingCrestodianResumeStore.ActivationOwner? = nil,
|
||||
activationOwner: OnboardingSystemAgentResumeStore.ActivationOwner? = nil,
|
||||
requireExistingReceipt: Bool = false)
|
||||
{
|
||||
let routeIdentity = self.routeIdentityProvider()?.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let completedReceipt = OnboardingCrestodianResumeStore.markCompleted(
|
||||
let completedReceipt = OnboardingSystemAgentResumeStore.markCompleted(
|
||||
ifOwnedBy: routeIdentity,
|
||||
activationOwner: activationOwner,
|
||||
defaults: self.defaults)
|
||||
|
||||
@@ -84,7 +84,7 @@ extension OnboardingAISetupModel {
|
||||
// Codex can spend 305s installing its runtime plugin before the 90s live probe.
|
||||
// Keep a bounded client deadline with room for registry refresh and finalization.
|
||||
kind == "codex-cli"
|
||||
? OnboardingCrestodianResumeStore.maximumActivationTimeoutMs
|
||||
? OnboardingSystemAgentResumeStore.maximumActivationTimeoutMs
|
||||
: 150_000
|
||||
}
|
||||
|
||||
@@ -97,7 +97,7 @@ extension OnboardingAISetupModel {
|
||||
return code == "UNKNOWN_METHOD" ||
|
||||
(code == "INVALID_REQUEST" &&
|
||||
(message.contains("unknown method") ||
|
||||
message.contains("invalid crestodian.setup.activate params")))
|
||||
message.contains("invalid openclaw.setup.activate params")))
|
||||
}
|
||||
return error is GatewayConnectAuthError ||
|
||||
error is GatewayTLSValidationError ||
|
||||
|
||||
@@ -69,8 +69,8 @@ enum OnboardingProviderAuthLink {
|
||||
|
||||
struct OnboardingAISetupView: View {
|
||||
@Bindable var model: OnboardingAISetupModel
|
||||
var crestodianChat: CrestodianOnboardingChatModel
|
||||
@Binding var showCrestodianChat: Bool
|
||||
var systemAgentChat: SystemAgentOnboardingChatModel
|
||||
@Binding var showSystemAgentChat: Bool
|
||||
var retryConfiguredGatewayProbe: () -> Void
|
||||
@State private var openedProviderAuthURL: URL?
|
||||
|
||||
@@ -84,8 +84,8 @@ struct OnboardingAISetupView: View {
|
||||
}
|
||||
}
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
.sheet(isPresented: self.$showCrestodianChat) {
|
||||
self.crestodianSheet
|
||||
.sheet(isPresented: self.$showSystemAgentChat) {
|
||||
self.systemAgentSheet
|
||||
}
|
||||
.sheet(isPresented: Binding(
|
||||
get: { self.model.activeAuthOption != nil },
|
||||
@@ -185,13 +185,13 @@ struct OnboardingAISetupView: View {
|
||||
self.manualSection
|
||||
}
|
||||
|
||||
if CrestodianAvailability.shouldShow(configuredModel: self.model.connectedModelRef) {
|
||||
if SystemAgentAvailability.shouldShow(configuredModel: self.model.connectedModelRef) {
|
||||
HStack {
|
||||
Spacer(minLength: 0)
|
||||
Button {
|
||||
self.showCrestodianChat = true
|
||||
self.showSystemAgentChat = true
|
||||
} label: {
|
||||
Label("Need help? Chat with Crestodian", systemImage: "questionmark.bubble")
|
||||
Label("Need help? Chat with OpenClaw", systemImage: "questionmark.bubble")
|
||||
.font(.caption)
|
||||
}
|
||||
.buttonStyle(.link)
|
||||
@@ -704,19 +704,19 @@ struct OnboardingAISetupView: View {
|
||||
return "\(hint). Paste it here, and OpenClaw checks it with a real test question."
|
||||
}
|
||||
|
||||
private var crestodianSheet: some View {
|
||||
private var systemAgentSheet: some View {
|
||||
VStack(spacing: 8) {
|
||||
HStack {
|
||||
Label("Crestodian — setup helper", systemImage: "lifepreserver")
|
||||
Label("OpenClaw — setup helper", systemImage: "lifepreserver")
|
||||
.font(.headline)
|
||||
Spacer(minLength: 0)
|
||||
Button("Done") {
|
||||
self.showCrestodianChat = false
|
||||
self.showSystemAgentChat = false
|
||||
}
|
||||
}
|
||||
.padding([.top, .horizontal], 14)
|
||||
CrestodianOnboardingChatView(model: self.crestodianChat)
|
||||
.task { await self.crestodianChat.startIfNeeded() }
|
||||
SystemAgentOnboardingChatView(model: self.systemAgentChat)
|
||||
.task { await self.systemAgentChat.startIfNeeded() }
|
||||
}
|
||||
.frame(width: 520, height: 480)
|
||||
}
|
||||
|
||||
+19
-19
@@ -4,8 +4,8 @@ import SwiftUI
|
||||
|
||||
@MainActor
|
||||
@Observable
|
||||
final class OnboardingCrestodianChatState {
|
||||
var chat = CrestodianOnboardingChatModel()
|
||||
final class OnboardingSystemAgentChatState {
|
||||
var chat = SystemAgentOnboardingChatModel()
|
||||
var isPresented = false
|
||||
@ObservationIgnored private var startTask: Task<Void, Never>?
|
||||
|
||||
@@ -31,16 +31,16 @@ final class OnboardingCrestodianChatState {
|
||||
self.startTask?.cancel()
|
||||
self.startTask = nil
|
||||
self.chat.invalidate()
|
||||
self.chat = CrestodianOnboardingChatModel()
|
||||
self.chat = SystemAgentOnboardingChatModel()
|
||||
}
|
||||
}
|
||||
|
||||
/// Onboarding talks to Crestodian over the gateway `crestodian.chat` RPC.
|
||||
/// Onboarding talks to OpenClaw over the gateway `openclaw.chat` RPC.
|
||||
/// The conversation is available after structured setup establishes working
|
||||
/// inference, so the model-backed helper can answer reliably.
|
||||
@MainActor
|
||||
@Observable
|
||||
final class CrestodianOnboardingChatModel {
|
||||
final class SystemAgentOnboardingChatModel {
|
||||
struct Message: Identifiable, Equatable {
|
||||
enum Role {
|
||||
case assistant
|
||||
@@ -57,7 +57,7 @@ final class CrestodianOnboardingChatModel {
|
||||
private(set) var errorMessage: String?
|
||||
private(set) var expectsSensitiveReply = false
|
||||
var input = ""
|
||||
/// Set when Crestodian hands off to the normal agent ("talk to agent").
|
||||
/// Set when OpenClaw hands off to the normal agent ("talk to agent").
|
||||
var onAgentHandoff: (() -> Void)?
|
||||
/// Called after every assistant reply (setup may have applied config).
|
||||
var onReplyReceived: (() -> Void)?
|
||||
@@ -66,7 +66,7 @@ final class CrestodianOnboardingChatModel {
|
||||
private let sessionPrefix: String
|
||||
private let gateway: GatewayConnection
|
||||
/// "onboarding" seeds the first-run setup proposal; nil gets the
|
||||
/// status/repair greeting (used by Settings → Crestodian).
|
||||
/// status/repair greeting (used by Settings → OpenClaw).
|
||||
private let welcomeVariant: String?
|
||||
private var started = false
|
||||
private var requestGeneration: UInt64? = 0
|
||||
@@ -100,7 +100,7 @@ final class CrestodianOnboardingChatModel {
|
||||
await self.requestReply(message: nil, generation: generation)
|
||||
if Task.isCancelled, self.requestGeneration == generation {
|
||||
self.started = false
|
||||
self.errorMessage = "Crestodian was interrupted. Restart to try again."
|
||||
self.errorMessage = "OpenClaw was interrupted. Restart to try again."
|
||||
}
|
||||
}
|
||||
|
||||
@@ -195,7 +195,7 @@ final class CrestodianOnboardingChatModel {
|
||||
let route = try await self.sessionRoute(for: generation)
|
||||
guard self.isCurrentRequest(generation) else { return }
|
||||
let data = try await self.gateway.request(
|
||||
method: "crestodian.chat",
|
||||
method: "openclaw.chat",
|
||||
params: params,
|
||||
timeoutMs: 190_000,
|
||||
ifCurrentRoute: route)
|
||||
@@ -214,8 +214,8 @@ final class CrestodianOnboardingChatModel {
|
||||
if error is CancellationError || Task.isCancelled {
|
||||
self.started = false
|
||||
self.errorMessage = Task.isCancelled
|
||||
? "Crestodian was interrupted. Restart to try again."
|
||||
: "The Gateway connection changed. Restart Crestodian to reconnect."
|
||||
? "OpenClaw was interrupted. Restart to try again."
|
||||
: "The Gateway connection changed. Restart OpenClaw to reconnect."
|
||||
return
|
||||
}
|
||||
self.errorMessage = error.localizedDescription
|
||||
@@ -223,8 +223,8 @@ final class CrestodianOnboardingChatModel {
|
||||
}
|
||||
}
|
||||
|
||||
struct CrestodianOnboardingChatView: View {
|
||||
@Bindable var model: CrestodianOnboardingChatModel
|
||||
struct SystemAgentOnboardingChatView: View {
|
||||
@Bindable var model: SystemAgentOnboardingChatModel
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 8) {
|
||||
@@ -232,14 +232,14 @@ struct CrestodianOnboardingChatView: View {
|
||||
ScrollView {
|
||||
LazyVStack(alignment: .leading, spacing: 10) {
|
||||
ForEach(self.model.messages) { message in
|
||||
CrestodianChatBubble(message: message)
|
||||
SystemAgentChatBubble(message: message)
|
||||
.id(message.id)
|
||||
}
|
||||
if self.model.isSending {
|
||||
HStack(spacing: 8) {
|
||||
ProgressView()
|
||||
.controlSize(.small)
|
||||
Text("Crestodian is working…")
|
||||
Text("OpenClaw is working…")
|
||||
.font(.caption)
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
@@ -278,7 +278,7 @@ struct CrestodianOnboardingChatView: View {
|
||||
SecureField("Enter secret…", text: self.$model.input)
|
||||
} else {
|
||||
TextField(
|
||||
"Reply to Crestodian… (yes sets everything up)",
|
||||
"Reply to OpenClaw… (yes sets everything up)",
|
||||
text: self.$model.input)
|
||||
}
|
||||
}
|
||||
@@ -301,8 +301,8 @@ struct CrestodianOnboardingChatView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private struct CrestodianChatBubble: View {
|
||||
let message: CrestodianOnboardingChatModel.Message
|
||||
private struct SystemAgentChatBubble: View {
|
||||
let message: SystemAgentOnboardingChatModel.Message
|
||||
|
||||
var body: some View {
|
||||
HStack {
|
||||
@@ -326,7 +326,7 @@ private struct CrestodianChatBubble: View {
|
||||
}
|
||||
|
||||
private var attributedText: AttributedString {
|
||||
// Crestodian replies use light markdown (headings, bold, backticks).
|
||||
// OpenClaw replies use light markdown (headings, bold, backticks).
|
||||
// Parse per line so multi-line replies keep their structure.
|
||||
var result = AttributedString()
|
||||
let lines = self.message.text.split(separator: "\n", omittingEmptySubsequences: false)
|
||||
@@ -3,7 +3,7 @@ import SwiftUI
|
||||
extension OnboardingView {
|
||||
/// Structured AI setup: detect what's already on this machine, test the
|
||||
/// best option live, fall through automatically, offer an API-key form
|
||||
/// when nothing works. Crestodian becomes available only after inference
|
||||
/// when nothing works. OpenClaw becomes available only after inference
|
||||
/// has completed a live round-trip.
|
||||
func aiSetupPage(contentHeight: CGFloat) -> some View {
|
||||
VStack(spacing: 12) {
|
||||
@@ -19,8 +19,8 @@ extension OnboardingView {
|
||||
ScrollView {
|
||||
OnboardingAISetupView(
|
||||
model: self.aiSetup,
|
||||
crestodianChat: self.crestodianState.chat,
|
||||
showCrestodianChat: self.$crestodianState.isPresented,
|
||||
systemAgentChat: self.systemAgentState.chat,
|
||||
showSystemAgentChat: self.$systemAgentState.isPresented,
|
||||
retryConfiguredGatewayProbe: { self.retryConfiguredGatewayProbe() })
|
||||
.padding(.vertical, 4)
|
||||
.padding(.trailing, 12)
|
||||
@@ -46,14 +46,14 @@ extension OnboardingView {
|
||||
// Local mode reaches this page only after the CLI/gateway install page,
|
||||
// so the gateway is up before the first RPC.
|
||||
guard state.connectionMode != .local || cliInstalled else { return }
|
||||
self.prepareCrestodianHandoff()
|
||||
self.prepareSystemAgentHandoff()
|
||||
// A selected/reconnected Gateway may already have a configured default
|
||||
// agent. Check that route before setup tries to author inference.
|
||||
probeConfiguredGatewayForDashboard(startAISetupWhenMissing: true)
|
||||
}
|
||||
|
||||
func prepareCrestodianHandoff() {
|
||||
crestodianState.chat.onAgentHandoff = { [self] in self.finish() }
|
||||
func prepareSystemAgentHandoff() {
|
||||
systemAgentState.chat.onAgentHandoff = { [self] in self.finish() }
|
||||
aiSetup.onPendingActivationDeadline = { [self] deadline, routeIdentity in
|
||||
let currentRouteIdentity = self.aiSetupRouteIdentityProvider()
|
||||
guard currentRouteIdentity == routeIdentity else { return }
|
||||
@@ -65,14 +65,14 @@ extension OnboardingView {
|
||||
aiSetup.onConnected = { [self] in
|
||||
// Activation already persisted the resume marker before its RPC.
|
||||
self.configuredGatewayProbe.cancelPendingActivationRecheck()
|
||||
self.crestodianState.presentAndStart()
|
||||
self.systemAgentState.presentAndStart()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
func resumePendingCrestodian(modelRef: String) -> Task<Void, Never> {
|
||||
self.prepareCrestodianHandoff()
|
||||
func resumePendingSystemAgent(modelRef: String) -> Task<Void, Never> {
|
||||
self.prepareSystemAgentHandoff()
|
||||
let expectedRouteIdentity = self.aiSetupRouteIdentityProvider()
|
||||
aiSetup.resumeConfiguredInference(modelRef: modelRef)
|
||||
if let page = pageOrder.firstIndex(of: aiPageIndex) {
|
||||
@@ -91,12 +91,12 @@ extension OnboardingView {
|
||||
self.configuredGatewayProbe.cancelPendingActivationRecheck()
|
||||
// `onConnected` already owns presentation. Await that exact start
|
||||
// task without starting a replacement route's chat after suspension.
|
||||
await self.crestodianState.waitForStartIfNeeded()
|
||||
await self.systemAgentState.waitForStartIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
func waitForPendingInferenceSetup() {
|
||||
self.prepareCrestodianHandoff()
|
||||
self.prepareSystemAgentHandoff()
|
||||
if let page = pageOrder.firstIndex(of: aiPageIndex) {
|
||||
currentPage = page
|
||||
}
|
||||
@@ -115,7 +115,7 @@ extension OnboardingView {
|
||||
}
|
||||
|
||||
func resumePendingInferenceSetup() {
|
||||
self.prepareCrestodianHandoff()
|
||||
self.prepareSystemAgentHandoff()
|
||||
if let page = pageOrder.firstIndex(of: aiPageIndex) {
|
||||
currentPage = page
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import AppKit
|
||||
import SwiftUI
|
||||
|
||||
extension OnboardingView {
|
||||
/// The inference-first flow has no full-page chat; Crestodian opens in its own sheet.
|
||||
/// The inference-first flow has no full-page chat; OpenClaw opens in its own sheet.
|
||||
var usesCompactHero: Bool {
|
||||
false
|
||||
}
|
||||
@@ -90,7 +90,7 @@ extension OnboardingView {
|
||||
// Queued detection can otherwise proceed into a mutating activation
|
||||
// after the window or its selected route has gone away.
|
||||
aiSetup.resetForGatewayChange(clearPendingHandoff: false)
|
||||
crestodianState.resetForGatewayChange()
|
||||
systemAgentState.resetForGatewayChange()
|
||||
stopPermissionMonitoring()
|
||||
stopDiscovery()
|
||||
}
|
||||
@@ -136,9 +136,9 @@ extension OnboardingView {
|
||||
// The UI attempt belongs to one route, but its durable activation lease
|
||||
// must survive A -> B -> A while the old Gateway can still be mutating.
|
||||
aiSetup.resetForGatewayChange(clearPendingHandoff: false)
|
||||
// Crestodian sessions belong to one Gateway. Dismiss and replace the chat so
|
||||
// OpenClaw sessions belong to one Gateway. Dismiss and replace the chat so
|
||||
// changing routes cannot send an old session ID to the new endpoint.
|
||||
crestodianState.resetForGatewayChange()
|
||||
systemAgentState.resetForGatewayChange()
|
||||
}
|
||||
|
||||
@discardableResult
|
||||
@@ -158,12 +158,12 @@ extension OnboardingView {
|
||||
guard gatewaySelectionPersister() else { return nil }
|
||||
let expectedMode = state.connectionMode
|
||||
let expectedRouteIdentity = self.aiSetupRouteIdentityProvider()
|
||||
let expectedPendingState = OnboardingCrestodianResumeStore.pendingState(
|
||||
let expectedPendingState = OnboardingSystemAgentResumeStore.pendingState(
|
||||
for: expectedRouteIdentity,
|
||||
defaults: crestodianDefaults)
|
||||
let expectedActivationOwner = OnboardingCrestodianResumeStore.activationOwner(
|
||||
defaults: systemAgentDefaults)
|
||||
let expectedActivationOwner = OnboardingSystemAgentResumeStore.activationOwner(
|
||||
for: expectedRouteIdentity,
|
||||
defaults: crestodianDefaults)
|
||||
defaults: systemAgentDefaults)
|
||||
let probeAttempt = configuredGatewayProbe.beginProbe()
|
||||
return Task { @MainActor in
|
||||
let outcome = await self.configuredGatewayProbe.probe(
|
||||
@@ -177,10 +177,10 @@ extension OnboardingView {
|
||||
expectedRouteIdentity: expectedRouteIdentity,
|
||||
knownVisible: knownVisible)
|
||||
else { return }
|
||||
let pendingState = OnboardingCrestodianResumeStore.pendingState(
|
||||
let pendingState = OnboardingSystemAgentResumeStore.pendingState(
|
||||
for: expectedRouteIdentity,
|
||||
defaults: self.crestodianDefaults)
|
||||
let crestodianResumePending = pendingState != .none
|
||||
defaults: self.systemAgentDefaults)
|
||||
let systemAgentResumePending = pendingState != .none
|
||||
self.schedulePendingActivationRecheckIfNeeded(pendingState)
|
||||
|
||||
switch outcome {
|
||||
@@ -191,7 +191,7 @@ extension OnboardingView {
|
||||
// reconnect must not downgrade connected state or fork a
|
||||
// second resume operation.
|
||||
guard !self.aiSetup.connected else { return }
|
||||
self.resumePendingCrestodian(modelRef: modelRef)
|
||||
self.resumePendingSystemAgent(modelRef: modelRef)
|
||||
return
|
||||
case .verified:
|
||||
// Inference was observed, but the dropped activation can
|
||||
@@ -203,7 +203,7 @@ extension OnboardingView {
|
||||
// the dispatched activation is still returning. Keep the
|
||||
// setup-owned handoff, and prove inference on this route.
|
||||
if self.aiSetup.pendingActivationVerification {
|
||||
self.resumePendingCrestodian(modelRef: modelRef)
|
||||
self.resumePendingSystemAgent(modelRef: modelRef)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -211,7 +211,7 @@ extension OnboardingView {
|
||||
onboardingVisible: self.onboardingVisible,
|
||||
expectedMode: expectedMode,
|
||||
currentMode: self.state.connectionMode,
|
||||
crestodianResumePending: crestodianResumePending,
|
||||
systemAgentResumePending: systemAgentResumePending,
|
||||
setupOwnsInferenceTransition: self.aiSetup.ownsInferenceTransition)
|
||||
else { return }
|
||||
self.onboardingVisible = false
|
||||
@@ -235,10 +235,10 @@ extension OnboardingView {
|
||||
// at probe start. A replacement attempt owns its own retry.
|
||||
guard expectedPendingState != .none,
|
||||
let expectedRouteIdentity,
|
||||
OnboardingCrestodianResumeStore.clear(
|
||||
OnboardingSystemAgentResumeStore.clear(
|
||||
ifOwnedBy: expectedRouteIdentity,
|
||||
activationOwner: expectedActivationOwner,
|
||||
defaults: self.crestodianDefaults)
|
||||
defaults: self.systemAgentDefaults)
|
||||
else { return }
|
||||
self.resumePendingInferenceSetup()
|
||||
return
|
||||
@@ -282,7 +282,7 @@ extension OnboardingView {
|
||||
}
|
||||
|
||||
private func schedulePendingActivationRecheckIfNeeded(
|
||||
_ pendingState: OnboardingCrestodianResumeStore.PendingState)
|
||||
_ pendingState: OnboardingSystemAgentResumeStore.PendingState)
|
||||
{
|
||||
switch pendingState {
|
||||
case let .activating(deadline), let .verified(deadline):
|
||||
@@ -298,14 +298,14 @@ extension OnboardingView {
|
||||
onboardingVisible: Bool,
|
||||
expectedMode: AppState.ConnectionMode,
|
||||
currentMode: AppState.ConnectionMode,
|
||||
crestodianResumePending: Bool,
|
||||
systemAgentResumePending: Bool,
|
||||
setupOwnsInferenceTransition: Bool) -> Bool
|
||||
{
|
||||
self.isCurrentConfiguredGatewayProbe(
|
||||
onboardingVisible: onboardingVisible,
|
||||
expectedMode: expectedMode,
|
||||
currentMode: currentMode) &&
|
||||
!crestodianResumePending &&
|
||||
!systemAgentResumePending &&
|
||||
!setupOwnsInferenceTransition
|
||||
}
|
||||
|
||||
|
||||
@@ -147,7 +147,7 @@ extension OnboardingView {
|
||||
.disabled(self.installingCLI)
|
||||
.onChange(of: self.state.connectionMode) { _, newValue in
|
||||
// The root view's mode observer calls handleConnectionModeChange(), which
|
||||
// retires route-owned AI/Crestodian state. This nested observer owns probe copy only.
|
||||
// retires route-owned AI/OpenClaw state. This nested observer owns probe copy only.
|
||||
guard Self.shouldResetRemoteProbeFeedback(
|
||||
for: newValue,
|
||||
suppressReset: self.suppressRemoteProbeReset)
|
||||
|
||||
@@ -11,7 +11,7 @@ struct SettingsRootView: View {
|
||||
@State private var inferenceConfiguration: InferenceConfiguration
|
||||
@State private var trackedInferenceGatewayID: String?
|
||||
@State private var inferenceRefreshTrigger = InferenceRefreshTrigger.invalidate(UUID())
|
||||
@State private var crestodianChatIdentity = UUID()
|
||||
@State private var systemAgentChatIdentity = UUID()
|
||||
@State private var deferredTab: SettingsTab?
|
||||
@State private var columnVisibility: NavigationSplitViewVisibility = .all
|
||||
@State private var snapshotPaths: (configPath: String?, stateDir: String?) = (nil, nil)
|
||||
@@ -80,8 +80,8 @@ struct SettingsRootView: View {
|
||||
}
|
||||
}
|
||||
.onChange(of: self.inferenceConfiguration) { _, configuration in
|
||||
if !CrestodianAvailability.shouldShow(configuredModel: configuration.configuredModel),
|
||||
self.selectedTab == .crestodian
|
||||
if !SystemAgentAvailability.shouldShow(configuredModel: configuration.configuredModel),
|
||||
self.selectedTab == .systemAgent
|
||||
{
|
||||
self.selectedTab = .general
|
||||
}
|
||||
@@ -105,7 +105,7 @@ struct SettingsRootView: View {
|
||||
self.trackedInferenceGatewayID = gatewayID
|
||||
self.scheduleInferenceRefresh(
|
||||
clearPrevious: plan.clearsPrevious,
|
||||
resetCrestodian: plan.resetsCrestodian)
|
||||
resetSystemAgent: plan.resetsSystemAgent)
|
||||
}
|
||||
.onDisappear { self.stopPermissionMonitoring() }
|
||||
.task {
|
||||
@@ -114,7 +114,7 @@ struct SettingsRootView: View {
|
||||
}
|
||||
.onChange(of: self.state.connectionMode) { _, _ in
|
||||
self.trackedInferenceGatewayID = MacChatTranscriptCache.currentGatewayID()
|
||||
self.scheduleInferenceRefresh(clearPrevious: true, resetCrestodian: true)
|
||||
self.scheduleInferenceRefresh(clearPrevious: true, resetSystemAgent: true)
|
||||
}
|
||||
.task(id: self.inferenceRefreshTrigger) {
|
||||
guard !self.isPreview else { return }
|
||||
@@ -127,7 +127,7 @@ struct SettingsRootView: View {
|
||||
private var visibleGroups: [SettingsTabGroup] {
|
||||
SettingsTabGroup.defaultGroups(
|
||||
showDebug: self.state.debugPaneEnabled,
|
||||
showCrestodian: CrestodianAvailability.shouldShow(
|
||||
showSystemAgent: SystemAgentAvailability.shouldShow(
|
||||
configuredModel: self.inferenceConfiguration.configuredModel))
|
||||
}
|
||||
|
||||
@@ -224,13 +224,13 @@ struct SettingsRootView: View {
|
||||
showOnboarding: { DebugActions.restartOnboarding() }))
|
||||
case .voiceWake:
|
||||
AnyView(VoiceWakeSettings(state: self.state, isActive: self.selectedTab == .voiceWake))
|
||||
case .crestodian:
|
||||
AnyView(CrestodianSettings(
|
||||
case .systemAgent:
|
||||
AnyView(SystemAgentSettings(
|
||||
isActive: self.selectedTab == tab,
|
||||
onReplyReceived: {
|
||||
self.scheduleInferenceRefresh(clearPrevious: false)
|
||||
})
|
||||
.id(self.crestodianChatIdentity))
|
||||
.id(self.systemAgentChatIdentity))
|
||||
case .channels:
|
||||
AnyView(ChannelsSettings(isActive: self.selectedTab == tab))
|
||||
case .skills:
|
||||
@@ -271,28 +271,28 @@ struct SettingsRootView: View {
|
||||
showDebug: Bool,
|
||||
inferenceConfiguration: InferenceConfiguration) -> TabSelection
|
||||
{
|
||||
let showCrestodian = CrestodianAvailability.shouldShow(
|
||||
let showSystemAgent = SystemAgentAvailability.shouldShow(
|
||||
configuredModel: inferenceConfiguration.configuredModel)
|
||||
let deferred = requested == .crestodian && !showCrestodian && !inferenceConfiguration.isLoaded
|
||||
let deferred = requested == .systemAgent && !showSystemAgent && !inferenceConfiguration.isLoaded
|
||||
? requested
|
||||
: nil
|
||||
return TabSelection(
|
||||
selected: Self.normalizedTab(
|
||||
requested,
|
||||
showDebug: showDebug,
|
||||
showCrestodian: showCrestodian),
|
||||
showSystemAgent: showSystemAgent),
|
||||
deferred: deferred)
|
||||
}
|
||||
|
||||
static func normalizedTab(
|
||||
_ requested: SettingsTab,
|
||||
showDebug: Bool,
|
||||
showCrestodian: Bool) -> SettingsTab
|
||||
showSystemAgent: Bool) -> SettingsTab
|
||||
{
|
||||
if requested == .debug, !showDebug {
|
||||
return .general
|
||||
}
|
||||
if requested == .crestodian, !showCrestodian {
|
||||
if requested == .systemAgent, !showSystemAgent {
|
||||
return .general
|
||||
}
|
||||
return requested
|
||||
@@ -375,7 +375,7 @@ struct SettingsRootView: View {
|
||||
|
||||
struct ConfigRefreshPlan: Equatable {
|
||||
let clearsPrevious: Bool
|
||||
let resetsCrestodian: Bool
|
||||
let resetsSystemAgent: Bool
|
||||
}
|
||||
|
||||
static func configRefreshPlan(
|
||||
@@ -385,8 +385,8 @@ struct SettingsRootView: View {
|
||||
{
|
||||
let routeChanged = previousGatewayID != currentGatewayID
|
||||
return ConfigRefreshPlan(
|
||||
clearsPrevious: routeChanged || selectedTab != .crestodian,
|
||||
resetsCrestodian: routeChanged)
|
||||
clearsPrevious: routeChanged || selectedTab != .systemAgent,
|
||||
resetsSystemAgent: routeChanged)
|
||||
}
|
||||
|
||||
static func configurationAfterInferenceRefresh(
|
||||
@@ -399,14 +399,14 @@ struct SettingsRootView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private func scheduleInferenceRefresh(clearPrevious: Bool, resetCrestodian: Bool = false) {
|
||||
if resetCrestodian {
|
||||
// Crestodian sessions are gateway-owned. Re-key the cached detail so a route
|
||||
private func scheduleInferenceRefresh(clearPrevious: Bool, resetSystemAgent: Bool = false) {
|
||||
if resetSystemAgent {
|
||||
// OpenClaw sessions are gateway-owned. Re-key the cached detail so a route
|
||||
// change cannot send old conversation state to a new endpoint.
|
||||
self.crestodianChatIdentity = UUID()
|
||||
self.systemAgentChatIdentity = UUID()
|
||||
}
|
||||
if clearPrevious {
|
||||
// Preserve an active or pending Crestodian request while config truth is revalidated.
|
||||
// Preserve an active or pending OpenClaw request while config truth is revalidated.
|
||||
// A confirmed model restores it; a confirmed missing model leaves General selected.
|
||||
let requestedTab = self.deferredTab ?? self.selectedTab
|
||||
self.inferenceConfiguration = .loading
|
||||
@@ -439,9 +439,9 @@ struct SettingsTabGroup: Identifiable {
|
||||
self.title
|
||||
}
|
||||
|
||||
static func defaultGroups(showDebug: Bool, showCrestodian: Bool) -> [SettingsTabGroup] {
|
||||
let basicTabs: [SettingsTab] = showCrestodian
|
||||
? [.general, .connection, .permissions, .voiceWake, .crestodian]
|
||||
static func defaultGroups(showDebug: Bool, showSystemAgent: Bool) -> [SettingsTabGroup] {
|
||||
let basicTabs: [SettingsTab] = showSystemAgent
|
||||
? [.general, .connection, .permissions, .voiceWake, .systemAgent]
|
||||
: [.general, .connection, .permissions, .voiceWake]
|
||||
var groups = [
|
||||
SettingsTabGroup(title: "Basics", tabs: basicTabs),
|
||||
@@ -460,7 +460,7 @@ struct SettingsTabGroup: Identifiable {
|
||||
}
|
||||
|
||||
enum SettingsTab: CaseIterable, Identifiable, Hashable {
|
||||
case general, connection, permissions, voiceWake, crestodian, channels, skills, cron
|
||||
case general, connection, permissions, voiceWake, systemAgent, channels, skills, cron
|
||||
case execApprovals, sessions, instances, config, debug, about
|
||||
static let windowWidth: CGFloat = 1120
|
||||
static let windowHeight: CGFloat = 790
|
||||
@@ -475,7 +475,7 @@ enum SettingsTab: CaseIterable, Identifiable, Hashable {
|
||||
case .connection: "Connection"
|
||||
case .permissions: "Permissions"
|
||||
case .voiceWake: "Voice & Talk"
|
||||
case .crestodian: "Crestodian"
|
||||
case .systemAgent: "OpenClaw"
|
||||
case .channels: "Channels"
|
||||
case .skills: "Skills"
|
||||
case .cron: "Cron Jobs"
|
||||
@@ -494,7 +494,7 @@ enum SettingsTab: CaseIterable, Identifiable, Hashable {
|
||||
case .connection: "point.3.connected.trianglepath.dotted"
|
||||
case .permissions: "lock.shield"
|
||||
case .voiceWake: "waveform.circle"
|
||||
case .crestodian: "lifepreserver"
|
||||
case .systemAgent: "lifepreserver"
|
||||
case .channels: "link"
|
||||
case .skills: "sparkles"
|
||||
case .cron: "calendar.badge.clock"
|
||||
@@ -534,7 +534,7 @@ struct SettingsRootView_Previews: PreviewProvider {
|
||||
state: .preview,
|
||||
updater: DisabledUpdaterController(),
|
||||
initialTab: tab,
|
||||
configuredInferenceModel: tab == .crestodian ? "openai/gpt-5.6-sol" : nil)
|
||||
configuredInferenceModel: tab == .systemAgent ? "openai/gpt-5.6-sol" : nil)
|
||||
.previewDisplayName(tab.title)
|
||||
.frame(width: SettingsTab.windowWidth, height: SettingsTab.windowHeight)
|
||||
}
|
||||
|
||||
+8
-8
@@ -1,30 +1,30 @@
|
||||
import SwiftUI
|
||||
|
||||
enum CrestodianAvailability {
|
||||
enum SystemAgentAvailability {
|
||||
static func shouldShow(configuredModel: String?) -> Bool {
|
||||
!(configuredModel?.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ?? true)
|
||||
}
|
||||
}
|
||||
|
||||
/// Settings pane hosting the Crestodian setup/repair chat.
|
||||
/// Settings pane hosting the OpenClaw setup/repair chat.
|
||||
///
|
||||
/// The parent settings view exposes this pane only after inference is configured.
|
||||
struct CrestodianSettings: View {
|
||||
struct SystemAgentSettings: View {
|
||||
let isActive: Bool
|
||||
let onReplyReceived: () -> Void
|
||||
@State private var chat = CrestodianOnboardingChatModel(
|
||||
@State private var chat = SystemAgentOnboardingChatModel(
|
||||
welcomeVariant: nil,
|
||||
sessionPrefix: "mac-settings-crestodian")
|
||||
sessionPrefix: "mac-settings-openclaw")
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 20) {
|
||||
SettingsPageHeader(
|
||||
title: "Crestodian",
|
||||
title: "OpenClaw",
|
||||
subtitle: "Your AI-powered setup helper. It can check status, fix config, " +
|
||||
"switch models, and connect channels.")
|
||||
|
||||
SettingsCardGroup("Chat") {
|
||||
CrestodianOnboardingChatView(model: self.chat)
|
||||
SystemAgentOnboardingChatView(model: self.chat)
|
||||
.frame(maxWidth: .infinity, minHeight: 320, maxHeight: .infinity)
|
||||
}
|
||||
.frame(maxHeight: .infinity)
|
||||
@@ -45,7 +45,7 @@ struct CrestodianSettings: View {
|
||||
|
||||
@MainActor
|
||||
static func configureChatCallbacks(
|
||||
for chat: CrestodianOnboardingChatModel,
|
||||
for chat: SystemAgentOnboardingChatModel,
|
||||
onReplyReceived: @escaping () -> Void)
|
||||
{
|
||||
chat.onAgentHandoff = {
|
||||
@@ -229,13 +229,13 @@ struct AppStateRemoteConfigTests {
|
||||
@Test
|
||||
func `config watcher endpoint replacement clears and ignores stale discovery identity`() {
|
||||
let previousGatewayPreference = captureGatewayPreference()
|
||||
let previousPending = UserDefaults.standard.object(forKey: onboardingCrestodianPendingKey)
|
||||
let previousPending = UserDefaults.standard.object(forKey: onboardingSystemAgentPendingKey)
|
||||
defer {
|
||||
restoreGatewayPreference(previousGatewayPreference)
|
||||
if let previousPending {
|
||||
UserDefaults.standard.set(previousPending, forKey: onboardingCrestodianPendingKey)
|
||||
UserDefaults.standard.set(previousPending, forKey: onboardingSystemAgentPendingKey)
|
||||
} else {
|
||||
OnboardingCrestodianResumeStore.clear()
|
||||
OnboardingSystemAgentResumeStore.clear()
|
||||
}
|
||||
}
|
||||
let state = AppState(preview: true)
|
||||
@@ -243,7 +243,7 @@ struct AppStateRemoteConfigTests {
|
||||
state.remoteTransport = .direct
|
||||
state.remoteUrl = "wss://gateway-a.example.test"
|
||||
GatewayDiscoveryPreferences.setPreferredStableID("gateway-a")
|
||||
OnboardingCrestodianResumeStore.markPending(routeIdentity: "remote:id:gateway-a")
|
||||
OnboardingSystemAgentResumeStore.markPending(routeIdentity: "remote:id:gateway-a")
|
||||
let view = OnboardingView(state: state)
|
||||
view.preferredGatewayID = "gateway-a"
|
||||
|
||||
@@ -260,13 +260,13 @@ struct AppStateRemoteConfigTests {
|
||||
#expect(state.remoteUrl == "wss://gateway-b.example.test")
|
||||
#expect(GatewayDiscoveryPreferences.preferredStableID() == nil)
|
||||
#expect(view.effectivePreferredGatewayID == nil)
|
||||
let routeIdentity = OnboardingCrestodianResumeStore.selectedRouteIdentity(
|
||||
let routeIdentity = OnboardingSystemAgentResumeStore.selectedRouteIdentity(
|
||||
state: state,
|
||||
preferredGatewayID: view.effectivePreferredGatewayID)
|
||||
#expect(routeIdentity?.hasPrefix("remote:direct:") == true)
|
||||
#expect(routeIdentity != "remote:id:gateway-a")
|
||||
#expect(!OnboardingCrestodianResumeStore.isPending(for: routeIdentity))
|
||||
#expect(OnboardingCrestodianResumeStore.isPending(for: "remote:id:gateway-a"))
|
||||
#expect(!OnboardingSystemAgentResumeStore.isPending(for: routeIdentity))
|
||||
#expect(OnboardingSystemAgentResumeStore.isPending(for: "remote:id:gateway-a"))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -89,13 +89,13 @@ struct GatewayConnectionTests {
|
||||
}
|
||||
|
||||
@Test func `first connection admits hello capabilities before lease readiness`() async throws {
|
||||
let session = self.makeSession(serverCapabilities: ["crestodian-setup-model-ref"])
|
||||
let session = self.makeSession(serverCapabilities: ["openclaw-setup-model-ref"])
|
||||
let (conn, _) = try makeConnection(session: session)
|
||||
|
||||
let lease = try await conn.acquireServerLease()
|
||||
|
||||
#expect(await conn.supportsServerCapability(
|
||||
.crestodianSetupModelRef,
|
||||
.systemAgentSetupModelRef,
|
||||
ifCurrentServerLease: lease) == true)
|
||||
#expect(await conn.cachedGatewayVersion() == "test")
|
||||
#expect(session.snapshotMakeCount() == 1)
|
||||
@@ -105,14 +105,14 @@ struct GatewayConnectionTests {
|
||||
}
|
||||
|
||||
@Test func `disconnected server lease rejects before dispatch`() async throws {
|
||||
let session = self.makeSession(serverCapabilities: ["crestodian-setup-model-ref"])
|
||||
let session = self.makeSession(serverCapabilities: ["openclaw-setup-model-ref"])
|
||||
let (conn, _) = try makeConnection(session: session)
|
||||
let lease = try await conn.acquireServerLease()
|
||||
|
||||
await conn._test_handleDisconnect(socketGeneration: 1)
|
||||
do {
|
||||
_ = try await conn.request(
|
||||
method: "crestodian.setup.detect",
|
||||
method: "openclaw.setup.detect",
|
||||
params: [:],
|
||||
ifCurrentServerLease: lease)
|
||||
Issue.record("expected disconnected server lease rejection")
|
||||
@@ -146,14 +146,14 @@ struct GatewayConnectionTests {
|
||||
let id = task.snapshotConnectRequestID() ?? "connect"
|
||||
return .data(Self.connectOkData(
|
||||
id: id,
|
||||
capabilities: ["crestodian-setup-model-ref"]))
|
||||
capabilities: ["openclaw-setup-model-ref"]))
|
||||
})
|
||||
})
|
||||
let (conn, _) = try makeConnection(session: session)
|
||||
let lease = try await conn.acquireServerLease()
|
||||
let request = Task {
|
||||
try await conn.request(
|
||||
method: "crestodian.setup.activate",
|
||||
method: "openclaw.setup.activate",
|
||||
params: [:],
|
||||
timeoutMs: 5000,
|
||||
ifCurrentServerLease: lease)
|
||||
|
||||
@@ -161,7 +161,7 @@ struct MenuContentSmokeTests {
|
||||
let shouldOpen = AppDelegate.shouldOpenDashboardInsteadOfOnboarding(
|
||||
connectionMode: mode,
|
||||
onboardingSeen: false,
|
||||
crestodianResumePending: false,
|
||||
systemAgentResumePending: false,
|
||||
gatewayConnected: true,
|
||||
configuredInferenceModel: " openai/gpt-5.5 ")
|
||||
|
||||
@@ -174,7 +174,7 @@ struct MenuContentSmokeTests {
|
||||
let shouldOpen = AppDelegate.shouldOpenDashboardInsteadOfOnboarding(
|
||||
connectionMode: .remote,
|
||||
onboardingSeen: false,
|
||||
crestodianResumePending: false,
|
||||
systemAgentResumePending: false,
|
||||
gatewayConnected: true,
|
||||
configuredInferenceModel: model)
|
||||
|
||||
@@ -186,7 +186,7 @@ struct MenuContentSmokeTests {
|
||||
let shouldOpen = AppDelegate.shouldOpenDashboardInsteadOfOnboarding(
|
||||
connectionMode: .remote,
|
||||
onboardingSeen: false,
|
||||
crestodianResumePending: false,
|
||||
systemAgentResumePending: false,
|
||||
gatewayConnected: false,
|
||||
configuredInferenceModel: "openai/gpt-5.5")
|
||||
|
||||
@@ -197,18 +197,18 @@ struct MenuContentSmokeTests {
|
||||
let shouldOpen = AppDelegate.shouldOpenDashboardInsteadOfOnboarding(
|
||||
connectionMode: .local,
|
||||
onboardingSeen: false,
|
||||
crestodianResumePending: false,
|
||||
systemAgentResumePending: false,
|
||||
gatewayConnected: true,
|
||||
configuredInferenceModel: "openai/gpt-5.5")
|
||||
|
||||
#expect(shouldOpen)
|
||||
}
|
||||
|
||||
@Test func `pending Crestodian handoff survives relaunch and keeps onboarding`() {
|
||||
@Test func `pending OpenClaw handoff survives relaunch and keeps onboarding`() {
|
||||
let shouldOpen = AppDelegate.shouldOpenDashboardInsteadOfOnboarding(
|
||||
connectionMode: .local,
|
||||
onboardingSeen: false,
|
||||
crestodianResumePending: true,
|
||||
systemAgentResumePending: true,
|
||||
gatewayConnected: true,
|
||||
configuredInferenceModel: "openai/gpt-5.5")
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+100
-100
@@ -3,7 +3,7 @@ import OpenClawKit
|
||||
import Testing
|
||||
@testable import OpenClaw
|
||||
|
||||
private actor CrestodianGatewayConfig {
|
||||
private actor SystemAgentGatewayConfig {
|
||||
private var token = "a"
|
||||
|
||||
func snapshotToken() -> String {
|
||||
@@ -15,7 +15,7 @@ private actor CrestodianGatewayConfig {
|
||||
}
|
||||
}
|
||||
|
||||
private actor CrestodianSessionRecorder {
|
||||
private actor SystemAgentSessionRecorder {
|
||||
private var sessionIDs: [String] = []
|
||||
|
||||
func record(_ sessionID: String) {
|
||||
@@ -27,7 +27,7 @@ private actor CrestodianSessionRecorder {
|
||||
}
|
||||
}
|
||||
|
||||
private actor CrestodianMethodRecorder {
|
||||
private actor SystemAgentMethodRecorder {
|
||||
private var methods: [String] = []
|
||||
|
||||
func record(_ method: String) {
|
||||
@@ -39,7 +39,7 @@ private actor CrestodianMethodRecorder {
|
||||
}
|
||||
}
|
||||
|
||||
private actor CrestodianRequestGate {
|
||||
private actor SystemAgentRequestGate {
|
||||
private var consumed = false
|
||||
private var released = false
|
||||
private var continuation: CheckedContinuation<Void, Never>?
|
||||
@@ -62,7 +62,7 @@ private actor CrestodianRequestGate {
|
||||
}
|
||||
}
|
||||
|
||||
private func crestodianSessionID(from message: URLSessionWebSocketTask.Message) -> String? {
|
||||
private func systemAgentSessionID(from message: URLSessionWebSocketTask.Message) -> String? {
|
||||
let data: Data? = switch message {
|
||||
case let .data(data): data
|
||||
case let .string(string): string.data(using: .utf8)
|
||||
@@ -70,13 +70,13 @@ private func crestodianSessionID(from message: URLSessionWebSocketTask.Message)
|
||||
}
|
||||
guard let data,
|
||||
let object = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
|
||||
object["method"] as? String == "crestodian.chat",
|
||||
object["method"] as? String == "openclaw.chat",
|
||||
let params = object["params"] as? [String: Any]
|
||||
else { return nil }
|
||||
return params["sessionId"] as? String
|
||||
}
|
||||
|
||||
private func crestodianRequestMethod(from message: URLSessionWebSocketTask.Message) -> String? {
|
||||
private func systemAgentRequestMethod(from message: URLSessionWebSocketTask.Message) -> String? {
|
||||
let data: Data? = switch message {
|
||||
case let .data(data): data
|
||||
case let .string(string): string.data(using: .utf8)
|
||||
@@ -88,7 +88,7 @@ private func crestodianRequestMethod(from message: URLSessionWebSocketTask.Messa
|
||||
return object["method"] as? String
|
||||
}
|
||||
|
||||
private func respondToCrestodianHealth(
|
||||
private func respondToSystemAgentHealth(
|
||||
task: GatewayTestWebSocketTask,
|
||||
id: String,
|
||||
method: String?) -> Bool
|
||||
@@ -98,7 +98,7 @@ private func respondToCrestodianHealth(
|
||||
return true
|
||||
}
|
||||
|
||||
private func crestodianResponse(id: String, action: String = "none") -> Data {
|
||||
private func systemAgentResponse(id: String, action: String = "none") -> Data {
|
||||
Data(
|
||||
"""
|
||||
{
|
||||
@@ -165,37 +165,37 @@ private func transientVerificationErrorResponse(id: String) -> Data {
|
||||
|
||||
@Suite(.serialized)
|
||||
@MainActor
|
||||
struct OnboardingCrestodianChatTests {
|
||||
@Test func `onboarding wires Crestodian agent handoff`() {
|
||||
struct OnboardingSystemAgentChatTests {
|
||||
@Test func `onboarding wires OpenClaw agent handoff`() {
|
||||
let state = AppState(preview: true)
|
||||
state.connectionMode = .local
|
||||
let view = OnboardingView(state: state)
|
||||
|
||||
view.prepareCrestodianHandoff()
|
||||
view.prepareSystemAgentHandoff()
|
||||
|
||||
#expect(view.crestodianState.chat.onAgentHandoff != nil)
|
||||
#expect(view.systemAgentState.chat.onAgentHandoff != nil)
|
||||
}
|
||||
|
||||
@Test func `relaunch with pending inference resumes Crestodian`() async throws {
|
||||
@Test func `relaunch with pending inference resumes OpenClaw`() async throws {
|
||||
let suiteName = "OnboardingPendingInferenceResumeTests-\(UUID().uuidString)"
|
||||
let defaults = try #require(UserDefaults(suiteName: suiteName))
|
||||
defer { defaults.removePersistentDomain(forName: suiteName) }
|
||||
let methods = CrestodianMethodRecorder()
|
||||
let methods = SystemAgentMethodRecorder()
|
||||
let session = GatewayTestWebSocketSession(taskFactory: {
|
||||
GatewayTestWebSocketTask(sendHook: { task, message, sendIndex in
|
||||
guard sendIndex > 0,
|
||||
let id = GatewayWebSocketTestSupport.requestID(from: message)
|
||||
else { return }
|
||||
let method = crestodianRequestMethod(from: message)
|
||||
let method = systemAgentRequestMethod(from: message)
|
||||
if let method {
|
||||
await methods.record(method)
|
||||
}
|
||||
if respondToCrestodianHealth(task: task, id: id, method: method) { return }
|
||||
if respondToSystemAgentHealth(task: task, id: id, method: method) { return }
|
||||
switch method {
|
||||
case "crestodian.setup.verify":
|
||||
case "openclaw.setup.verify":
|
||||
task.emitReceiveSuccess(.data(verifiedInferenceResponse(id: id)))
|
||||
case "crestodian.chat":
|
||||
task.emitReceiveSuccess(.data(crestodianResponse(id: id)))
|
||||
case "openclaw.chat":
|
||||
task.emitReceiveSuccess(.data(systemAgentResponse(id: id)))
|
||||
default:
|
||||
break
|
||||
}
|
||||
@@ -212,25 +212,25 @@ struct OnboardingCrestodianChatTests {
|
||||
let view = OnboardingView(
|
||||
state: appState,
|
||||
aiSetupGateway: gateway,
|
||||
crestodianDefaults: defaults,
|
||||
systemAgentDefaults: defaults,
|
||||
aiSetupRouteIdentityProvider: { "remote:direct:example.invalid" })
|
||||
view.crestodianState.chat = CrestodianOnboardingChatModel(gateway: gateway)
|
||||
view.systemAgentState.chat = SystemAgentOnboardingChatModel(gateway: gateway)
|
||||
|
||||
let task = view.resumePendingCrestodian(modelRef: "openai/gpt-5.5")
|
||||
let task = view.resumePendingSystemAgent(modelRef: "openai/gpt-5.5")
|
||||
await task.value
|
||||
|
||||
#expect(view.aiSetup.connectedModelRef == "openai/gpt-5.5")
|
||||
#expect(view.crestodianState.isPresented)
|
||||
#expect(view.crestodianState.chat.messages.map(\.text) == ["ready"])
|
||||
#expect(view.systemAgentState.isPresented)
|
||||
#expect(view.systemAgentState.chat.messages.map(\.text) == ["ready"])
|
||||
|
||||
let repeatedResume = view.resumePendingCrestodian(modelRef: "openai/gpt-5.5")
|
||||
let repeatedResume = view.resumePendingSystemAgent(modelRef: "openai/gpt-5.5")
|
||||
await repeatedResume.value
|
||||
|
||||
#expect(view.aiSetup.connectedModelRef == "openai/gpt-5.5")
|
||||
#expect(await methods.snapshot() == [
|
||||
"health",
|
||||
"crestodian.setup.verify",
|
||||
"crestodian.chat",
|
||||
"openclaw.setup.verify",
|
||||
"openclaw.chat",
|
||||
])
|
||||
}
|
||||
|
||||
@@ -238,26 +238,26 @@ struct OnboardingCrestodianChatTests {
|
||||
let suiteName = "OnboardingPendingVerificationRetryTests-\(UUID().uuidString)"
|
||||
let defaults = try #require(UserDefaults(suiteName: suiteName))
|
||||
defer { defaults.removePersistentDomain(forName: suiteName) }
|
||||
let methods = CrestodianMethodRecorder()
|
||||
let methods = SystemAgentMethodRecorder()
|
||||
let session = GatewayTestWebSocketSession(taskFactory: {
|
||||
GatewayTestWebSocketTask(sendHook: { task, message, sendIndex in
|
||||
guard sendIndex > 0,
|
||||
let id = GatewayWebSocketTestSupport.requestID(from: message),
|
||||
let method = crestodianRequestMethod(from: message)
|
||||
let method = systemAgentRequestMethod(from: message)
|
||||
else { return }
|
||||
await methods.record(method)
|
||||
if respondToCrestodianHealth(task: task, id: id, method: method) { return }
|
||||
if respondToSystemAgentHealth(task: task, id: id, method: method) { return }
|
||||
switch method {
|
||||
case "crestodian.setup.verify":
|
||||
case "openclaw.setup.verify":
|
||||
let priorVerifications = await methods.snapshot().filter {
|
||||
$0 == "crestodian.setup.verify"
|
||||
$0 == "openclaw.setup.verify"
|
||||
}.count
|
||||
let response = priorVerifications == 1
|
||||
? transientVerificationErrorResponse(id: id)
|
||||
: verifiedInferenceResponse(id: id)
|
||||
task.emitReceiveSuccess(.data(response))
|
||||
case "crestodian.chat":
|
||||
task.emitReceiveSuccess(.data(crestodianResponse(id: id)))
|
||||
case "openclaw.chat":
|
||||
task.emitReceiveSuccess(.data(systemAgentResponse(id: id)))
|
||||
default:
|
||||
break
|
||||
}
|
||||
@@ -269,18 +269,18 @@ struct OnboardingCrestodianChatTests {
|
||||
sessionBox: WebSocketSessionBox(session: session))
|
||||
let appState = AppState(preview: true)
|
||||
appState.connectionMode = .local
|
||||
OnboardingCrestodianResumeStore.markPending(
|
||||
OnboardingSystemAgentResumeStore.markPending(
|
||||
routeIdentity: "local",
|
||||
defaults: defaults)
|
||||
let view = OnboardingView(
|
||||
state: appState,
|
||||
aiSetupGateway: gateway,
|
||||
crestodianDefaults: defaults,
|
||||
systemAgentDefaults: defaults,
|
||||
aiSetupRouteIdentityProvider: { "local" })
|
||||
view.crestodianState.chat = CrestodianOnboardingChatModel(gateway: gateway)
|
||||
view.systemAgentState.chat = SystemAgentOnboardingChatModel(gateway: gateway)
|
||||
|
||||
await view.resumePendingCrestodian(modelRef: "openai/gpt-5.5").value
|
||||
#expect(!view.crestodianState.isPresented)
|
||||
await view.resumePendingSystemAgent(modelRef: "openai/gpt-5.5").value
|
||||
#expect(!view.systemAgentState.isPresented)
|
||||
|
||||
var scheduledDeadlines: [(deadline: Date, routeIdentity: String)] = []
|
||||
view.aiSetup.onPendingActivationDeadline = { deadline, routeIdentity in
|
||||
@@ -288,7 +288,7 @@ struct OnboardingCrestodianChatTests {
|
||||
}
|
||||
view.aiSetup.retryFromScratch()
|
||||
for _ in 0..<200 {
|
||||
if case .verified = OnboardingCrestodianResumeStore.pendingState(
|
||||
if case .verified = OnboardingSystemAgentResumeStore.pendingState(
|
||||
for: "local",
|
||||
defaults: defaults)
|
||||
{
|
||||
@@ -299,11 +299,11 @@ struct OnboardingCrestodianChatTests {
|
||||
|
||||
#expect(!view.aiSetup.connected)
|
||||
#expect(view.aiSetup.waitingForPendingActivationDeadline)
|
||||
#expect(!view.crestodianState.isPresented)
|
||||
#expect(view.crestodianState.chat.messages.isEmpty)
|
||||
#expect(!view.systemAgentState.isPresented)
|
||||
#expect(view.systemAgentState.chat.messages.isEmpty)
|
||||
#expect(scheduledDeadlines.count == 1)
|
||||
#expect(scheduledDeadlines.first?.routeIdentity == "local")
|
||||
if case let .verified(deadline) = OnboardingCrestodianResumeStore.pendingState(
|
||||
if case let .verified(deadline) = OnboardingSystemAgentResumeStore.pendingState(
|
||||
for: "local",
|
||||
defaults: defaults)
|
||||
{
|
||||
@@ -315,9 +315,9 @@ struct OnboardingCrestodianChatTests {
|
||||
#expect(scheduledDeadlines.count == 1)
|
||||
#expect(await methods.snapshot() == [
|
||||
"health",
|
||||
"crestodian.setup.verify",
|
||||
"openclaw.setup.verify",
|
||||
"health",
|
||||
"crestodian.setup.verify",
|
||||
"openclaw.setup.verify",
|
||||
])
|
||||
}
|
||||
|
||||
@@ -325,17 +325,17 @@ struct OnboardingCrestodianChatTests {
|
||||
let suiteName = "OnboardingSupersededResumeTests-\(UUID().uuidString)"
|
||||
let defaults = try #require(UserDefaults(suiteName: suiteName))
|
||||
defer { defaults.removePersistentDomain(forName: suiteName) }
|
||||
let gate = CrestodianRequestGate()
|
||||
let methods = CrestodianMethodRecorder()
|
||||
let gate = SystemAgentRequestGate()
|
||||
let methods = SystemAgentMethodRecorder()
|
||||
let session = GatewayTestWebSocketSession(taskFactory: {
|
||||
GatewayTestWebSocketTask(sendHook: { task, message, sendIndex in
|
||||
guard sendIndex > 0,
|
||||
let id = GatewayWebSocketTestSupport.requestID(from: message),
|
||||
let method = crestodianRequestMethod(from: message)
|
||||
let method = systemAgentRequestMethod(from: message)
|
||||
else { return }
|
||||
await methods.record(method)
|
||||
if respondToCrestodianHealth(task: task, id: id, method: method) { return }
|
||||
guard method == "crestodian.setup.verify" else { return }
|
||||
if respondToSystemAgentHealth(task: task, id: id, method: method) { return }
|
||||
guard method == "openclaw.setup.verify" else { return }
|
||||
_ = await gate.waitIfFirst()
|
||||
task.emitReceiveSuccess(.data(verifiedInferenceResponse(id: id)))
|
||||
})
|
||||
@@ -351,13 +351,13 @@ struct OnboardingCrestodianChatTests {
|
||||
let view = OnboardingView(
|
||||
state: appState,
|
||||
aiSetupGateway: gateway,
|
||||
crestodianDefaults: defaults,
|
||||
systemAgentDefaults: defaults,
|
||||
aiSetupRouteIdentityProvider: { "remote:direct:example.invalid" })
|
||||
view.crestodianState.chat = CrestodianOnboardingChatModel(gateway: gateway)
|
||||
view.systemAgentState.chat = SystemAgentOnboardingChatModel(gateway: gateway)
|
||||
|
||||
let staleResume = view.resumePendingCrestodian(modelRef: "openai/gpt-5.5")
|
||||
let staleResume = view.resumePendingSystemAgent(modelRef: "openai/gpt-5.5")
|
||||
for _ in 0..<200 {
|
||||
if await methods.snapshot() == ["health", "crestodian.setup.verify"] {
|
||||
if await methods.snapshot() == ["health", "openclaw.setup.verify"] {
|
||||
break
|
||||
}
|
||||
try? await Task.sleep(nanoseconds: 5_000_000)
|
||||
@@ -372,30 +372,30 @@ struct OnboardingCrestodianChatTests {
|
||||
await staleResume.value
|
||||
|
||||
#expect(view.aiSetup.connected)
|
||||
#expect(!view.crestodianState.isPresented)
|
||||
#expect(view.crestodianState.chat.messages.isEmpty)
|
||||
#expect(!view.systemAgentState.isPresented)
|
||||
#expect(view.systemAgentState.chat.messages.isEmpty)
|
||||
}
|
||||
|
||||
@Test func `cold launch resumes a completed activation immediately`() async throws {
|
||||
let suiteName = "OnboardingColdPendingHandoffTests-\(UUID().uuidString)"
|
||||
let defaults = try #require(UserDefaults(suiteName: suiteName))
|
||||
defer { defaults.removePersistentDomain(forName: suiteName) }
|
||||
let methods = CrestodianMethodRecorder()
|
||||
let methods = SystemAgentMethodRecorder()
|
||||
let session = GatewayTestWebSocketSession(taskFactory: {
|
||||
GatewayTestWebSocketTask(sendHook: { task, message, sendIndex in
|
||||
guard sendIndex > 0,
|
||||
let id = GatewayWebSocketTestSupport.requestID(from: message),
|
||||
let method = crestodianRequestMethod(from: message)
|
||||
let method = systemAgentRequestMethod(from: message)
|
||||
else { return }
|
||||
await methods.record(method)
|
||||
if respondToCrestodianHealth(task: task, id: id, method: method) { return }
|
||||
if respondToSystemAgentHealth(task: task, id: id, method: method) { return }
|
||||
switch method {
|
||||
case "agents.list":
|
||||
task.emitReceiveSuccess(.data(configuredAgentsResponse(id: id)))
|
||||
case "crestodian.setup.verify":
|
||||
case "openclaw.setup.verify":
|
||||
task.emitReceiveSuccess(.data(verifiedInferenceResponse(id: id)))
|
||||
case "crestodian.chat":
|
||||
task.emitReceiveSuccess(.data(crestodianResponse(id: id)))
|
||||
case "openclaw.chat":
|
||||
task.emitReceiveSuccess(.data(systemAgentResponse(id: id)))
|
||||
default:
|
||||
break
|
||||
}
|
||||
@@ -407,66 +407,66 @@ struct OnboardingCrestodianChatTests {
|
||||
sessionBox: WebSocketSessionBox(session: session))
|
||||
let appState = AppState(preview: true)
|
||||
appState.connectionMode = .local
|
||||
let routeIdentity = OnboardingCrestodianResumeStore.selectedRouteIdentity(state: appState)
|
||||
let routeIdentity = OnboardingSystemAgentResumeStore.selectedRouteIdentity(state: appState)
|
||||
let route = try #require(await gateway.captureRoute())
|
||||
let activationOwner = try OnboardingCrestodianResumeStore.ActivationOwner(
|
||||
let activationOwner = try OnboardingSystemAgentResumeStore.ActivationOwner(
|
||||
id: "completed-before-relaunch",
|
||||
routeFingerprint: #require(route.activationOwnershipFingerprint))
|
||||
OnboardingCrestodianResumeStore.markPending(
|
||||
OnboardingSystemAgentResumeStore.markPending(
|
||||
routeIdentity: routeIdentity,
|
||||
activationOwner: activationOwner,
|
||||
defaults: defaults)
|
||||
OnboardingCrestodianResumeStore.markCompleted(
|
||||
OnboardingSystemAgentResumeStore.markCompleted(
|
||||
ifOwnedBy: routeIdentity,
|
||||
activationOwner: activationOwner,
|
||||
defaults: defaults)
|
||||
let view = OnboardingView(
|
||||
state: appState,
|
||||
aiSetupGateway: gateway,
|
||||
crestodianDefaults: defaults,
|
||||
systemAgentDefaults: defaults,
|
||||
aiSetupRouteIdentityProvider: { routeIdentity })
|
||||
view.crestodianState.chat = CrestodianOnboardingChatModel(gateway: gateway)
|
||||
view.systemAgentState.chat = SystemAgentOnboardingChatModel(gateway: gateway)
|
||||
let aiSetup = view.aiSetup
|
||||
let crestodianState = view.crestodianState
|
||||
let systemAgentState = view.systemAgentState
|
||||
|
||||
let initialProbe = try #require(view.onboardingDidAppear())
|
||||
await initialProbe.value
|
||||
for _ in 0..<200 {
|
||||
if crestodianState.chat.messages.map(\.text) == ["ready"] {
|
||||
if systemAgentState.chat.messages.map(\.text) == ["ready"] {
|
||||
break
|
||||
}
|
||||
try? await Task.sleep(nanoseconds: 5_000_000)
|
||||
}
|
||||
|
||||
#expect(aiSetup.connected)
|
||||
#expect(crestodianState.isPresented)
|
||||
#expect(crestodianState.chat.messages.map(\.text) == ["ready"])
|
||||
#expect(OnboardingCrestodianResumeStore.pendingState(
|
||||
#expect(systemAgentState.isPresented)
|
||||
#expect(systemAgentState.chat.messages.map(\.text) == ["ready"])
|
||||
#expect(OnboardingSystemAgentResumeStore.pendingState(
|
||||
for: routeIdentity,
|
||||
defaults: defaults) == .completed)
|
||||
#expect(await methods.snapshot() == [
|
||||
"agents.list",
|
||||
"health",
|
||||
"crestodian.setup.verify",
|
||||
"crestodian.chat",
|
||||
"openclaw.setup.verify",
|
||||
"openclaw.chat",
|
||||
])
|
||||
}
|
||||
|
||||
@Test func `fresh inference presents and starts Crestodian immediately`() async throws {
|
||||
@Test func `fresh inference presents and starts OpenClaw immediately`() async throws {
|
||||
let session = GatewayTestWebSocketSession(taskFactory: {
|
||||
GatewayTestWebSocketTask(sendHook: { task, message, sendIndex in
|
||||
guard sendIndex > 0,
|
||||
let id = GatewayWebSocketTestSupport.requestID(from: message)
|
||||
else { return }
|
||||
task.emitReceiveSuccess(.data(crestodianResponse(id: id)))
|
||||
task.emitReceiveSuccess(.data(systemAgentResponse(id: id)))
|
||||
})
|
||||
})
|
||||
let url = try #require(URL(string: "ws://example.invalid"))
|
||||
let gateway = GatewayConnection(
|
||||
configProvider: { (url: url, token: nil, password: nil) },
|
||||
sessionBox: WebSocketSessionBox(session: session))
|
||||
let state = OnboardingCrestodianChatState()
|
||||
state.chat = CrestodianOnboardingChatModel(gateway: gateway)
|
||||
let state = OnboardingSystemAgentChatState()
|
||||
state.chat = SystemAgentOnboardingChatModel(gateway: gateway)
|
||||
|
||||
let task = state.presentAndStart()
|
||||
await task.value
|
||||
@@ -483,16 +483,16 @@ struct OnboardingCrestodianChatTests {
|
||||
guard sendIndex > 0,
|
||||
let id = GatewayWebSocketTestSupport.requestID(from: message)
|
||||
else { return }
|
||||
task.emitReceiveSuccess(.data(crestodianResponse(id: id)))
|
||||
task.emitReceiveSuccess(.data(systemAgentResponse(id: id)))
|
||||
})
|
||||
})
|
||||
let url = try #require(URL(string: "ws://example.invalid"))
|
||||
let gateway = GatewayConnection(
|
||||
configProvider: { (url: url, token: nil, password: nil) },
|
||||
sessionBox: WebSocketSessionBox(session: session))
|
||||
let chat = CrestodianOnboardingChatModel(gateway: gateway)
|
||||
let chat = SystemAgentOnboardingChatModel(gateway: gateway)
|
||||
var refreshCount = 0
|
||||
CrestodianSettings.configureChatCallbacks(
|
||||
SystemAgentSettings.configureChatCallbacks(
|
||||
for: chat,
|
||||
onReplyReceived: { refreshCount += 1 })
|
||||
|
||||
@@ -508,8 +508,8 @@ struct OnboardingCrestodianChatTests {
|
||||
let gateway = GatewayConnection(
|
||||
configProvider: { (url: url, token: nil, password: nil) },
|
||||
sessionBox: WebSocketSessionBox(session: session))
|
||||
let chat = CrestodianOnboardingChatModel(gateway: gateway)
|
||||
let state = OnboardingCrestodianChatState()
|
||||
let chat = SystemAgentOnboardingChatModel(gateway: gateway)
|
||||
let state = OnboardingSystemAgentChatState()
|
||||
state.chat = chat
|
||||
var replyCount = 0
|
||||
var handoffCount = 0
|
||||
@@ -535,17 +535,17 @@ struct OnboardingCrestodianChatTests {
|
||||
}
|
||||
|
||||
@Test func `chat session stays bound to its original gateway route`() async throws {
|
||||
let config = CrestodianGatewayConfig()
|
||||
let recorder = CrestodianSessionRecorder()
|
||||
let config = SystemAgentGatewayConfig()
|
||||
let recorder = SystemAgentSessionRecorder()
|
||||
let session = GatewayTestWebSocketSession(taskFactory: {
|
||||
GatewayTestWebSocketTask(sendHook: { task, message, sendIndex in
|
||||
guard sendIndex > 0,
|
||||
let id = GatewayWebSocketTestSupport.requestID(from: message)
|
||||
else { return }
|
||||
if let sessionID = crestodianSessionID(from: message) {
|
||||
if let sessionID = systemAgentSessionID(from: message) {
|
||||
await recorder.record(sessionID)
|
||||
}
|
||||
task.emitReceiveSuccess(.data(crestodianResponse(id: id)))
|
||||
task.emitReceiveSuccess(.data(systemAgentResponse(id: id)))
|
||||
})
|
||||
})
|
||||
let url = try #require(URL(string: "ws://example.invalid"))
|
||||
@@ -555,7 +555,7 @@ struct OnboardingCrestodianChatTests {
|
||||
return (url: url, token: token, password: nil)
|
||||
},
|
||||
sessionBox: WebSocketSessionBox(session: session))
|
||||
let chat = CrestodianOnboardingChatModel(gateway: gateway)
|
||||
let chat = SystemAgentOnboardingChatModel(gateway: gateway)
|
||||
|
||||
await chat.startIfNeeded()
|
||||
#expect(chat.messages.map(\.text) == ["ready"])
|
||||
@@ -573,7 +573,7 @@ struct OnboardingCrestodianChatTests {
|
||||
#expect(session.snapshotMakeCount() == 1)
|
||||
#expect(session.latestTask()?.snapshotSendCount() == 2)
|
||||
#expect(chat.messages.map(\.text) == ["ready", "must stay on route a"])
|
||||
#expect(chat.errorMessage == "The Gateway connection changed. Restart Crestodian to reconnect.")
|
||||
#expect(chat.errorMessage == "The Gateway connection changed. Restart OpenClaw to reconnect.")
|
||||
#expect(await recorder.snapshot() == [routeASessionID])
|
||||
|
||||
let restartTask = try #require(chat.restartAfterError())
|
||||
@@ -590,15 +590,15 @@ struct OnboardingCrestodianChatTests {
|
||||
}
|
||||
|
||||
@Test func `route change while reply is in flight discards reply and action`() async throws {
|
||||
let config = CrestodianGatewayConfig()
|
||||
let requestGate = CrestodianRequestGate()
|
||||
let config = SystemAgentGatewayConfig()
|
||||
let requestGate = SystemAgentRequestGate()
|
||||
let session = GatewayTestWebSocketSession(taskFactory: {
|
||||
GatewayTestWebSocketTask(sendHook: { task, message, sendIndex in
|
||||
guard sendIndex > 0,
|
||||
let id = GatewayWebSocketTestSupport.requestID(from: message)
|
||||
else { return }
|
||||
_ = await requestGate.waitIfFirst()
|
||||
task.emitReceiveSuccess(.data(crestodianResponse(id: id, action: "open-agent")))
|
||||
task.emitReceiveSuccess(.data(systemAgentResponse(id: id, action: "open-agent")))
|
||||
})
|
||||
})
|
||||
let url = try #require(URL(string: "ws://example.invalid"))
|
||||
@@ -608,7 +608,7 @@ struct OnboardingCrestodianChatTests {
|
||||
return (url: url, token: token, password: nil)
|
||||
},
|
||||
sessionBox: WebSocketSessionBox(session: session))
|
||||
let chat = CrestodianOnboardingChatModel(gateway: gateway)
|
||||
let chat = SystemAgentOnboardingChatModel(gateway: gateway)
|
||||
var replyCount = 0
|
||||
var handoffCount = 0
|
||||
chat.onReplyReceived = { replyCount += 1 }
|
||||
@@ -631,11 +631,11 @@ struct OnboardingCrestodianChatTests {
|
||||
#expect(chat.messages.isEmpty)
|
||||
#expect(replyCount == 0)
|
||||
#expect(handoffCount == 0)
|
||||
#expect(chat.errorMessage == "The Gateway connection changed. Restart Crestodian to reconnect.")
|
||||
#expect(chat.errorMessage == "The Gateway connection changed. Restart OpenClaw to reconnect.")
|
||||
}
|
||||
|
||||
@Test func `cancelled initial request exposes restart and recovers`() async throws {
|
||||
let requestGate = CrestodianRequestGate()
|
||||
let requestGate = SystemAgentRequestGate()
|
||||
let session = GatewayTestWebSocketSession(taskFactory: {
|
||||
GatewayTestWebSocketTask(sendHook: { task, message, sendIndex in
|
||||
guard sendIndex > 0,
|
||||
@@ -644,14 +644,14 @@ struct OnboardingCrestodianChatTests {
|
||||
if sendIndex == 1, await requestGate.waitIfFirst() {
|
||||
throw CancellationError()
|
||||
}
|
||||
task.emitReceiveSuccess(.data(crestodianResponse(id: id)))
|
||||
task.emitReceiveSuccess(.data(systemAgentResponse(id: id)))
|
||||
})
|
||||
})
|
||||
let url = try #require(URL(string: "ws://example.invalid"))
|
||||
let gateway = GatewayConnection(
|
||||
configProvider: { (url: url, token: nil, password: nil) },
|
||||
sessionBox: WebSocketSessionBox(session: session))
|
||||
let chat = CrestodianOnboardingChatModel(gateway: gateway)
|
||||
let chat = SystemAgentOnboardingChatModel(gateway: gateway)
|
||||
|
||||
let startTask = Task { await chat.startIfNeeded() }
|
||||
var requestStarted = false
|
||||
@@ -667,7 +667,7 @@ struct OnboardingCrestodianChatTests {
|
||||
await requestGate.release()
|
||||
await startTask.value
|
||||
|
||||
#expect(chat.errorMessage == "Crestodian was interrupted. Restart to try again.")
|
||||
#expect(chat.errorMessage == "OpenClaw was interrupted. Restart to try again.")
|
||||
#expect(!chat.isSending)
|
||||
#expect(chat.messages.isEmpty)
|
||||
|
||||
@@ -73,7 +73,7 @@ struct OnboardingViewSmokeTests {
|
||||
#expect(short < preferred)
|
||||
}
|
||||
|
||||
@Test func `page order delegates setup after inference to Crestodian`() {
|
||||
@Test func `page order delegates setup after inference to OpenClaw`() {
|
||||
let order = OnboardingView.pageOrder(
|
||||
for: .local,
|
||||
requiresCLIInstall: false)
|
||||
@@ -197,17 +197,17 @@ struct OnboardingViewSmokeTests {
|
||||
let state = AppState(preview: true)
|
||||
let view = OnboardingView(state: state)
|
||||
var monitoredPage: Int?
|
||||
let previousCrestodianChat = view.crestodianState.chat
|
||||
let previousSystemAgentChat = view.systemAgentState.chat
|
||||
view.aiSetup.manualKey = "route-bound"
|
||||
view.crestodianState.isPresented = true
|
||||
view.systemAgentState.isPresented = true
|
||||
|
||||
view.handleConnectionModeChange { pageIndex in
|
||||
monitoredPage = pageIndex
|
||||
}
|
||||
|
||||
#expect(view.aiSetup.manualKey.isEmpty)
|
||||
#expect(!view.crestodianState.isPresented)
|
||||
#expect(view.crestodianState.chat !== previousCrestodianChat)
|
||||
#expect(!view.systemAgentState.isPresented)
|
||||
#expect(view.systemAgentState.chat !== previousSystemAgentChat)
|
||||
#expect(monitoredPage == view.activePageIndex)
|
||||
}
|
||||
|
||||
@@ -275,7 +275,7 @@ struct OnboardingViewSmokeTests {
|
||||
defaults.removePersistentDomain(forName: suiteName)
|
||||
}
|
||||
GatewayDiscoveryPreferences.setPreferredStableID("gateway-a")
|
||||
OnboardingCrestodianResumeStore.markPending(
|
||||
OnboardingSystemAgentResumeStore.markPending(
|
||||
routeIdentity: "remote:id:gateway-a",
|
||||
defaults: defaults)
|
||||
|
||||
@@ -286,10 +286,10 @@ struct OnboardingViewSmokeTests {
|
||||
state: state,
|
||||
permissionMonitor: PermissionMonitor.shared,
|
||||
discoveryModel: GatewayDiscoveryModel(localDisplayName: InstanceIdentity.displayName),
|
||||
crestodianDefaults: defaults)
|
||||
let priorChat = view.crestodianState.chat
|
||||
systemAgentDefaults: defaults)
|
||||
let priorChat = view.systemAgentState.chat
|
||||
view.aiSetup.manualKey = "route-a-secret"
|
||||
view.crestodianState.isPresented = true
|
||||
view.systemAgentState.isPresented = true
|
||||
let gateway = GatewayDiscoveryModel.DiscoveredGateway(
|
||||
displayName: "Gateway B",
|
||||
serviceHost: nil,
|
||||
@@ -307,12 +307,12 @@ struct OnboardingViewSmokeTests {
|
||||
|
||||
#expect(state.connectionMode == .remote)
|
||||
#expect(view.aiSetup.manualKey.isEmpty)
|
||||
#expect(!view.crestodianState.isPresented)
|
||||
#expect(view.crestodianState.chat !== priorChat)
|
||||
#expect(!OnboardingCrestodianResumeStore.isPending(
|
||||
#expect(!view.systemAgentState.isPresented)
|
||||
#expect(view.systemAgentState.chat !== priorChat)
|
||||
#expect(!OnboardingSystemAgentResumeStore.isPending(
|
||||
for: "remote:id:gateway-b",
|
||||
defaults: defaults))
|
||||
#expect(OnboardingCrestodianResumeStore.isPending(
|
||||
#expect(OnboardingSystemAgentResumeStore.isPending(
|
||||
for: "remote:id:gateway-a",
|
||||
defaults: defaults))
|
||||
}
|
||||
@@ -326,7 +326,7 @@ struct OnboardingViewSmokeTests {
|
||||
defaults.removePersistentDomain(forName: suiteName)
|
||||
}
|
||||
GatewayDiscoveryPreferences.setPreferredStableID("gateway-a")
|
||||
OnboardingCrestodianResumeStore.markPending(
|
||||
OnboardingSystemAgentResumeStore.markPending(
|
||||
routeIdentity: "remote:id:gateway-a",
|
||||
defaults: defaults)
|
||||
let state = AppState(preview: true)
|
||||
@@ -341,36 +341,36 @@ struct OnboardingViewSmokeTests {
|
||||
let view = OnboardingView(
|
||||
state: state,
|
||||
aiSetupGateway: gateway,
|
||||
crestodianDefaults: defaults)
|
||||
systemAgentDefaults: defaults)
|
||||
view.preferredGatewayID = "gateway-a"
|
||||
view.aiSetup.manualKey = "route-a-secret"
|
||||
view.aiSetup.resumeConfiguredInference(modelRef: "openai/gpt-5.5")
|
||||
view.aiSetup.acceptVerifiedPendingInference(modelRef: "openai/gpt-5.5")
|
||||
let priorChat = view.crestodianState.chat
|
||||
view.crestodianState.isPresented = true
|
||||
let priorChat = view.systemAgentState.chat
|
||||
view.systemAgentState.isPresented = true
|
||||
view.remoteProbeState = .ok(RemoteGatewayProbeSuccess(authSource: .sharedToken))
|
||||
view.remoteAuthIssue = .tokenMismatch
|
||||
|
||||
view.updateManualRemoteURL("wss://gateway-b.example.test")
|
||||
|
||||
let editedRouteIdentity = OnboardingCrestodianResumeStore.selectedRouteIdentity(
|
||||
let editedRouteIdentity = OnboardingSystemAgentResumeStore.selectedRouteIdentity(
|
||||
state: state,
|
||||
preferredGatewayID: view.preferredGatewayID ?? GatewayDiscoveryPreferences.preferredStableID())
|
||||
#expect(view.preferredGatewayID == nil)
|
||||
#expect(GatewayDiscoveryPreferences.preferredStableID() == nil)
|
||||
#expect(editedRouteIdentity?.hasPrefix("remote:direct:") == true)
|
||||
#expect(editedRouteIdentity != "remote:id:gateway-a")
|
||||
#expect(OnboardingCrestodianResumeStore.isPending(
|
||||
#expect(OnboardingSystemAgentResumeStore.isPending(
|
||||
for: "remote:id:gateway-a",
|
||||
defaults: defaults))
|
||||
#expect(!OnboardingCrestodianResumeStore.isPending(
|
||||
#expect(!OnboardingSystemAgentResumeStore.isPending(
|
||||
for: editedRouteIdentity,
|
||||
defaults: defaults))
|
||||
#expect(view.aiSetup.phase == .idle)
|
||||
#expect(!view.aiSetup.connected)
|
||||
#expect(view.aiSetup.manualKey.isEmpty)
|
||||
#expect(!view.crestodianState.isPresented)
|
||||
#expect(view.crestodianState.chat !== priorChat)
|
||||
#expect(!view.systemAgentState.isPresented)
|
||||
#expect(view.systemAgentState.chat !== priorChat)
|
||||
#expect(view.remoteProbeState == .idle)
|
||||
#expect(view.remoteAuthIssue == nil)
|
||||
#expect(gatewaySession.snapshotMakeCount() == 0)
|
||||
@@ -388,7 +388,7 @@ struct OnboardingViewSmokeTests {
|
||||
defaults.removePersistentDomain(forName: suiteName)
|
||||
}
|
||||
GatewayDiscoveryPreferences.setPreferredStableID("gateway-a")
|
||||
OnboardingCrestodianResumeStore.markPending(
|
||||
OnboardingSystemAgentResumeStore.markPending(
|
||||
routeIdentity: "remote:id:gateway-a",
|
||||
defaults: defaults)
|
||||
|
||||
@@ -399,10 +399,10 @@ struct OnboardingViewSmokeTests {
|
||||
state: state,
|
||||
permissionMonitor: PermissionMonitor.shared,
|
||||
discoveryModel: GatewayDiscoveryModel(localDisplayName: InstanceIdentity.displayName),
|
||||
crestodianDefaults: defaults)
|
||||
let priorChat = view.crestodianState.chat
|
||||
systemAgentDefaults: defaults)
|
||||
let priorChat = view.systemAgentState.chat
|
||||
view.aiSetup.manualKey = "pending-secret"
|
||||
view.crestodianState.isPresented = true
|
||||
view.systemAgentState.isPresented = true
|
||||
let gateway = GatewayDiscoveryModel.DiscoveredGateway(
|
||||
displayName: "Gateway A",
|
||||
serviceHost: nil,
|
||||
@@ -419,9 +419,9 @@ struct OnboardingViewSmokeTests {
|
||||
view.selectRemoteGateway(gateway)
|
||||
|
||||
#expect(view.aiSetup.manualKey == "pending-secret")
|
||||
#expect(view.crestodianState.isPresented)
|
||||
#expect(view.crestodianState.chat === priorChat)
|
||||
#expect(OnboardingCrestodianResumeStore.isPending(
|
||||
#expect(view.systemAgentState.isPresented)
|
||||
#expect(view.systemAgentState.chat === priorChat)
|
||||
#expect(OnboardingSystemAgentResumeStore.isPending(
|
||||
for: "remote:id:gateway-a",
|
||||
defaults: defaults))
|
||||
}
|
||||
@@ -435,24 +435,24 @@ struct OnboardingViewSmokeTests {
|
||||
defaults.removePersistentDomain(forName: suiteName)
|
||||
}
|
||||
GatewayDiscoveryPreferences.setPreferredStableID("gateway-a")
|
||||
OnboardingCrestodianResumeStore.markPending(
|
||||
OnboardingSystemAgentResumeStore.markPending(
|
||||
routeIdentity: "remote:id:gateway-a",
|
||||
defaults: defaults)
|
||||
let state = AppState(preview: true)
|
||||
state.connectionMode = .remote
|
||||
let view = OnboardingView(state: state, crestodianDefaults: defaults)
|
||||
let priorChat = view.crestodianState.chat
|
||||
let view = OnboardingView(state: state, systemAgentDefaults: defaults)
|
||||
let priorChat = view.systemAgentState.chat
|
||||
view.aiSetup.manualKey = "route-a-secret"
|
||||
view.crestodianState.isPresented = true
|
||||
view.systemAgentState.isPresented = true
|
||||
|
||||
view.selectLocalGateway()
|
||||
|
||||
#expect(state.connectionMode == .local)
|
||||
#expect(view.aiSetup.manualKey.isEmpty)
|
||||
#expect(!view.crestodianState.isPresented)
|
||||
#expect(view.crestodianState.chat !== priorChat)
|
||||
#expect(!OnboardingCrestodianResumeStore.isPending(for: "local", defaults: defaults))
|
||||
#expect(OnboardingCrestodianResumeStore.isPending(
|
||||
#expect(!view.systemAgentState.isPresented)
|
||||
#expect(view.systemAgentState.chat !== priorChat)
|
||||
#expect(!OnboardingSystemAgentResumeStore.isPending(for: "local", defaults: defaults))
|
||||
#expect(OnboardingSystemAgentResumeStore.isPending(
|
||||
for: "remote:id:gateway-a",
|
||||
defaults: defaults))
|
||||
}
|
||||
@@ -460,40 +460,40 @@ struct OnboardingViewSmokeTests {
|
||||
@Test func `same local selection preserves pending gateway setup state`() throws {
|
||||
let (defaults, suiteName) = try makeOnboardingResumeDefaults()
|
||||
defer { defaults.removePersistentDomain(forName: suiteName) }
|
||||
OnboardingCrestodianResumeStore.markPending(routeIdentity: "local", defaults: defaults)
|
||||
OnboardingSystemAgentResumeStore.markPending(routeIdentity: "local", defaults: defaults)
|
||||
let state = AppState(preview: true)
|
||||
state.connectionMode = .local
|
||||
let view = OnboardingView(state: state, crestodianDefaults: defaults)
|
||||
let priorChat = view.crestodianState.chat
|
||||
let view = OnboardingView(state: state, systemAgentDefaults: defaults)
|
||||
let priorChat = view.systemAgentState.chat
|
||||
view.aiSetup.manualKey = "pending-secret"
|
||||
view.crestodianState.isPresented = true
|
||||
view.systemAgentState.isPresented = true
|
||||
|
||||
view.selectLocalGateway()
|
||||
|
||||
#expect(view.aiSetup.manualKey == "pending-secret")
|
||||
#expect(view.crestodianState.isPresented)
|
||||
#expect(view.crestodianState.chat === priorChat)
|
||||
#expect(OnboardingCrestodianResumeStore.isPending(for: "local", defaults: defaults))
|
||||
#expect(view.systemAgentState.isPresented)
|
||||
#expect(view.systemAgentState.chat === priorChat)
|
||||
#expect(OnboardingSystemAgentResumeStore.isPending(for: "local", defaults: defaults))
|
||||
}
|
||||
|
||||
@Test func `configure later preserves in flight activation lease`() throws {
|
||||
let (defaults, suiteName) = try makeOnboardingResumeDefaults()
|
||||
defer { defaults.removePersistentDomain(forName: suiteName) }
|
||||
OnboardingCrestodianResumeStore.markPending(routeIdentity: "local", defaults: defaults)
|
||||
OnboardingSystemAgentResumeStore.markPending(routeIdentity: "local", defaults: defaults)
|
||||
let state = AppState(preview: true)
|
||||
state.connectionMode = .local
|
||||
let view = OnboardingView(state: state, crestodianDefaults: defaults)
|
||||
let priorChat = view.crestodianState.chat
|
||||
let view = OnboardingView(state: state, systemAgentDefaults: defaults)
|
||||
let priorChat = view.systemAgentState.chat
|
||||
view.aiSetup.manualKey = "local-secret"
|
||||
view.crestodianState.isPresented = true
|
||||
view.systemAgentState.isPresented = true
|
||||
|
||||
view.selectUnconfiguredGateway()
|
||||
|
||||
#expect(state.connectionMode == .unconfigured)
|
||||
#expect(view.aiSetup.manualKey.isEmpty)
|
||||
#expect(!view.crestodianState.isPresented)
|
||||
#expect(view.crestodianState.chat !== priorChat)
|
||||
#expect(OnboardingCrestodianResumeStore.isPending(for: "local", defaults: defaults))
|
||||
#expect(!view.systemAgentState.isPresented)
|
||||
#expect(view.systemAgentState.chat !== priorChat)
|
||||
#expect(OnboardingSystemAgentResumeStore.isPending(for: "local", defaults: defaults))
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -202,39 +202,39 @@ struct SettingsViewSmokeTests {
|
||||
_ = view.body
|
||||
}
|
||||
|
||||
@Test func `Crestodian settings require configured inference`() {
|
||||
#expect(!CrestodianAvailability.shouldShow(configuredModel: nil))
|
||||
#expect(!CrestodianAvailability.shouldShow(configuredModel: " "))
|
||||
#expect(CrestodianAvailability.shouldShow(configuredModel: "openai/gpt-5.5"))
|
||||
@Test func `OpenClaw settings require configured inference`() {
|
||||
#expect(!SystemAgentAvailability.shouldShow(configuredModel: nil))
|
||||
#expect(!SystemAgentAvailability.shouldShow(configuredModel: " "))
|
||||
#expect(SystemAgentAvailability.shouldShow(configuredModel: "openai/gpt-5.5"))
|
||||
|
||||
let hiddenTabs = SettingsTabGroup.defaultGroups(showDebug: false, showCrestodian: false)
|
||||
let hiddenTabs = SettingsTabGroup.defaultGroups(showDebug: false, showSystemAgent: false)
|
||||
.flatMap(\.tabs)
|
||||
let visibleTabs = SettingsTabGroup.defaultGroups(showDebug: false, showCrestodian: true)
|
||||
let visibleTabs = SettingsTabGroup.defaultGroups(showDebug: false, showSystemAgent: true)
|
||||
.flatMap(\.tabs)
|
||||
#expect(!hiddenTabs.contains(.crestodian))
|
||||
#expect(visibleTabs.contains(.crestodian))
|
||||
#expect(!hiddenTabs.contains(.systemAgent))
|
||||
#expect(visibleTabs.contains(.systemAgent))
|
||||
#expect(SettingsRootView.normalizedTab(
|
||||
.crestodian,
|
||||
.systemAgent,
|
||||
showDebug: false,
|
||||
showCrestodian: false) == .general)
|
||||
showSystemAgent: false) == .general)
|
||||
#expect(SettingsRootView.normalizedTab(
|
||||
.crestodian,
|
||||
.systemAgent,
|
||||
showDebug: false,
|
||||
showCrestodian: true) == .crestodian)
|
||||
showSystemAgent: true) == .systemAgent)
|
||||
let loadingSelection = SettingsRootView.tabSelection(
|
||||
requested: .crestodian,
|
||||
requested: .systemAgent,
|
||||
showDebug: false,
|
||||
inferenceConfiguration: .loading)
|
||||
#expect(loadingSelection.selected == .general)
|
||||
#expect(loadingSelection.deferred == .crestodian)
|
||||
#expect(loadingSelection.deferred == .systemAgent)
|
||||
let configuredSelection = SettingsRootView.tabSelection(
|
||||
requested: loadingSelection.deferred ?? .general,
|
||||
showDebug: false,
|
||||
inferenceConfiguration: .loaded("openai/gpt-5.5"))
|
||||
#expect(configuredSelection.selected == .crestodian)
|
||||
#expect(configuredSelection.selected == .systemAgent)
|
||||
#expect(configuredSelection.deferred == nil)
|
||||
let unconfiguredSelection = SettingsRootView.tabSelection(
|
||||
requested: .crestodian,
|
||||
requested: .systemAgent,
|
||||
showDebug: false,
|
||||
inferenceConfiguration: .loaded(nil))
|
||||
#expect(unconfiguredSelection.selected == .general)
|
||||
@@ -247,7 +247,7 @@ struct SettingsViewSmokeTests {
|
||||
result: .confirmed(nil)) == .loaded(nil))
|
||||
}
|
||||
|
||||
@Test func `Crestodian preserves same route and resets for gateway changes`() {
|
||||
@Test func `OpenClaw preserves same route and resets for gateway changes`() {
|
||||
let stateDir = URL(fileURLWithPath: "/Users/tester/.openclaw")
|
||||
let directA = MacChatTranscriptCache.gatewayID(
|
||||
mode: .remote,
|
||||
@@ -266,17 +266,17 @@ struct SettingsViewSmokeTests {
|
||||
|
||||
#expect(directA != directB)
|
||||
#expect(SettingsRootView.configRefreshPlan(
|
||||
selectedTab: .crestodian,
|
||||
selectedTab: .systemAgent,
|
||||
previousGatewayID: directA,
|
||||
currentGatewayID: directA) == .init(clearsPrevious: false, resetsCrestodian: false))
|
||||
currentGatewayID: directA) == .init(clearsPrevious: false, resetsSystemAgent: false))
|
||||
#expect(SettingsRootView.configRefreshPlan(
|
||||
selectedTab: .general,
|
||||
previousGatewayID: directA,
|
||||
currentGatewayID: directA) == .init(clearsPrevious: true, resetsCrestodian: false))
|
||||
currentGatewayID: directA) == .init(clearsPrevious: true, resetsSystemAgent: false))
|
||||
#expect(SettingsRootView.configRefreshPlan(
|
||||
selectedTab: .crestodian,
|
||||
selectedTab: .systemAgent,
|
||||
previousGatewayID: directA,
|
||||
currentGatewayID: directB) == .init(clearsPrevious: true, resetsCrestodian: true))
|
||||
currentGatewayID: directB) == .init(clearsPrevious: true, resetsSystemAgent: true))
|
||||
}
|
||||
|
||||
@Test func `about settings builds body`() {
|
||||
|
||||
@@ -2,7 +2,7 @@ import OpenClawProtocol
|
||||
|
||||
public enum GatewayServerCapability: String, CaseIterable, Sendable {
|
||||
case chatSendRoutingContract = "chat-send-routing-contract"
|
||||
case crestodianSetupModelRef = "crestodian-setup-model-ref"
|
||||
case systemAgentSetupModelRef = "openclaw-setup-model-ref"
|
||||
}
|
||||
|
||||
extension HelloOk {
|
||||
|
||||
@@ -447,9 +447,9 @@
|
||||
"proposal_id"
|
||||
]
|
||||
},
|
||||
"crestodian": {
|
||||
"openclaw": {
|
||||
"emoji": "🦀",
|
||||
"title": "Crestodian",
|
||||
"title": "OpenClaw",
|
||||
"detailKeys": [
|
||||
"action",
|
||||
"path",
|
||||
|
||||
@@ -6335,7 +6335,7 @@ public struct ConfigSchemaLookupResult: Codable, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct CrestodianChatParams: Codable, Sendable {
|
||||
public struct SystemAgentChatParams: Codable, Sendable {
|
||||
public let sessionid: String
|
||||
public let message: String?
|
||||
public let welcomevariant: AnyCodable?
|
||||
@@ -6361,7 +6361,7 @@ public struct CrestodianChatParams: Codable, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct CrestodianChatResult: Codable, Sendable {
|
||||
public struct SystemAgentChatResult: Codable, Sendable {
|
||||
public let sessionid: String
|
||||
public let reply: String
|
||||
public let sensitive: Bool?
|
||||
@@ -6387,9 +6387,9 @@ public struct CrestodianChatResult: Codable, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct CrestodianSetupDetectParams: Codable, Sendable {}
|
||||
public struct SystemAgentSetupDetectParams: Codable, Sendable {}
|
||||
|
||||
public struct CrestodianSetupDetectResult: Codable, Sendable {
|
||||
public struct SystemAgentSetupDetectResult: Codable, Sendable {
|
||||
public let candidates: [[String: AnyCodable]]
|
||||
public let manualproviders: [[String: AnyCodable]]
|
||||
public let authoptions: [[String: AnyCodable]]?
|
||||
@@ -6427,9 +6427,9 @@ public struct CrestodianSetupDetectResult: Codable, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct CrestodianSetupVerifyParams: Codable, Sendable {}
|
||||
public struct SystemAgentSetupVerifyParams: Codable, Sendable {}
|
||||
|
||||
public struct CrestodianSetupActivateParams: Codable, Sendable {
|
||||
public struct SystemAgentSetupActivateParams: Codable, Sendable {
|
||||
public let kind: AnyCodable
|
||||
public let modelref: String?
|
||||
public let authchoice: String?
|
||||
@@ -6459,7 +6459,7 @@ public struct CrestodianSetupActivateParams: Codable, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct CrestodianSetupActivateResult: Codable, Sendable {
|
||||
public struct SystemAgentSetupActivateResult: Codable, Sendable {
|
||||
public let ok: Bool
|
||||
public let modelref: String?
|
||||
public let latencyms: Double?
|
||||
@@ -6493,7 +6493,7 @@ public struct CrestodianSetupActivateResult: Codable, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct CrestodianSetupAuthStartParams: Codable, Sendable {
|
||||
public struct SystemAgentSetupAuthStartParams: Codable, Sendable {
|
||||
public let sessionid: String
|
||||
public let authchoice: String
|
||||
public let workspace: String?
|
||||
@@ -6515,7 +6515,7 @@ public struct CrestodianSetupAuthStartParams: Codable, Sendable {
|
||||
}
|
||||
}
|
||||
|
||||
public struct CrestodianSetupAuthStartResult: Codable, Sendable {
|
||||
public struct SystemAgentSetupAuthStartResult: Codable, Sendable {
|
||||
public let sessionid: String
|
||||
public let done: Bool
|
||||
public let step: WizardStep?
|
||||
|
||||
+5
-348
@@ -1,352 +1,9 @@
|
||||
---
|
||||
summary: "CLI reference and security model for the inference-backed Crestodian setup and repair helper"
|
||||
summary: "Redirect to the OpenClaw system-agent reference"
|
||||
read_when:
|
||||
- You finished inference setup and want Crestodian to configure the rest
|
||||
- You need to inspect or repair OpenClaw with the local setup agent
|
||||
- You are designing or enabling message-channel rescue mode
|
||||
title: "Crestodian"
|
||||
- You followed an older Crestodian documentation link
|
||||
title: "Crestodian (redirect)"
|
||||
redirect: /cli/openclaw
|
||||
---
|
||||
|
||||
# `openclaw crestodian`
|
||||
|
||||
Conversational Crestodian is OpenClaw's local setup, repair, and configuration
|
||||
agent. It starts only after the effective default model completes a real turn.
|
||||
Fresh installs establish inference first; malformed config stays on the
|
||||
classic doctor path.
|
||||
|
||||
## When it starts
|
||||
|
||||
Running `openclaw` with no subcommand routes based on config state:
|
||||
|
||||
- Config missing, or exists with no authored settings (empty, or only `$schema`/`meta` keys): starts guided onboarding with live AI verification.
|
||||
- Config exists but fails validation: starts classic onboarding, which reports the issues and directs you to `openclaw doctor`.
|
||||
- Config exists and is valid: opens the normal agent TUI. A reachable
|
||||
configured Gateway whose default agent has a model goes directly to that UI
|
||||
without onboarding or Crestodian. Use `/crestodian` inside the TUI, or run
|
||||
`openclaw crestodian` directly, to reach Crestodian later.
|
||||
|
||||
Running `openclaw crestodian` first live-tests the configured default model. A passing turn starts Crestodian. An interactive failure opens guided inference setup and hands off to Crestodian after a candidate passes. One-shot, JSON, and other noninteractive requests fail with instructions to run `openclaw onboard` when inference is unavailable. `openclaw --help` and `openclaw --version` keep their normal fast paths.
|
||||
|
||||
Noninteractive bare `openclaw` (no TTY) exits with a short message instead of printing root help: it points to non-interactive onboarding on a fresh or invalid install, or to `openclaw agent --local ...` when config is valid.
|
||||
|
||||
`openclaw onboard --modern` remains a compatibility alias for Crestodian, but uses the same inference gate: working inference opens the chat, interactive failures start guided inference setup, and noninteractive failures exit with onboarding guidance. `openclaw onboard --classic` opens the full step-by-step wizard.
|
||||
|
||||
## What Crestodian shows
|
||||
|
||||
Interactive Crestodian opens the same TUI shell as `openclaw tui`, with a Crestodian chat backend. The startup greeting covers:
|
||||
|
||||
- config validity and the default agent
|
||||
- the verified model Crestodian is using
|
||||
- Gateway reachability from the first startup probe
|
||||
- the next recommended debug action
|
||||
|
||||
It does not dump secrets or load plugin CLI commands just to start.
|
||||
|
||||
Use `status` for the detailed inventory: config path, docs/source paths, local CLI probes, key/token presence, agents, model, and Gateway details.
|
||||
|
||||
Crestodian uses the same reference discovery as regular agents: in a Git checkout it points at local `docs/` and the source tree; in an npm install it uses bundled docs and links to [https://github.com/openclaw/openclaw](https://github.com/openclaw/openclaw), with guidance to check source when docs are not enough.
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
openclaw
|
||||
openclaw crestodian
|
||||
openclaw crestodian --json
|
||||
openclaw crestodian --message "models"
|
||||
openclaw crestodian --message "validate config"
|
||||
openclaw crestodian --message "setup workspace ~/Projects/work" --yes
|
||||
openclaw crestodian --message "set default model openai/gpt-5.6" --yes
|
||||
openclaw onboard --modern
|
||||
```
|
||||
|
||||
Inside the Crestodian TUI:
|
||||
|
||||
```text
|
||||
status
|
||||
health
|
||||
doctor
|
||||
validate config
|
||||
setup
|
||||
setup workspace ~/Projects/work
|
||||
config set gateway.port 19001
|
||||
config set-ref gateway.auth.token env OPENCLAW_GATEWAY_TOKEN
|
||||
gateway status
|
||||
restart gateway
|
||||
agents
|
||||
create agent work workspace ~/Projects/work
|
||||
models
|
||||
configure model provider
|
||||
set default model openai/gpt-5.6
|
||||
channels
|
||||
channel info slack
|
||||
connect slack
|
||||
open channel wizard for slack
|
||||
plugins list
|
||||
plugins search slack
|
||||
plugin install clawhub:openclaw-codex-app-server
|
||||
talk to work agent
|
||||
talk to agent for ~/Projects/work
|
||||
audit
|
||||
quit
|
||||
```
|
||||
|
||||
## Operations and approval
|
||||
|
||||
Crestodian uses typed operations instead of editing config ad hoc.
|
||||
|
||||
Read-only operations run immediately: show overview, list agents, list installed plugins, search ClawHub plugins, show model/backend status, run status/health checks, check Gateway reachability, run doctor without interactive fixes, validate config, show the audit-log path.
|
||||
|
||||
Starting guided channel setup (`connect telegram`) also runs immediately. Its wizard collects explicit answers and owns the resulting writes.
|
||||
|
||||
Persistent operations require conversational approval (or `--yes` for a direct command): write config, `config set`, `config set-ref`, setup/onboarding bootstrap, change the default model, start/stop/restart the Gateway, create agents, and install plugins.
|
||||
|
||||
Crestodian installs only ClawHub, bundled, or official-catalog plugins. Install any other executable source from a trusted shell with `openclaw plugins install <spec>`, where the normal source warning and acknowledgement flow applies.
|
||||
|
||||
Doctor repairs are unavailable inside Crestodian because they can rewrite the provider, authentication, or default-agent inference route powering the session. Exit Crestodian and run `openclaw doctor --fix` in a terminal. Read-only `doctor` remains available inside Crestodian.
|
||||
|
||||
New agents inherit the live-verified default inference route. The agent id `crestodian` is reserved for the privileged virtual custodian and cannot be created as a normal agent.
|
||||
|
||||
`config set` and `config set-ref` cannot change inference-route state,
|
||||
including inference-provider credentials, top-level `auth.*`, model catalogs,
|
||||
CLI backends, default/per-agent model routes, agent params/tools, or root
|
||||
`tools.*`. Raw writes under `env.*`, `secrets.*`, `plugins.*`, and `$include`
|
||||
are also refused because they can replace credential resolution or provider
|
||||
activation. Gateway and channel auth remain normal config surfaces. Use typed plugin/channel workflows and
|
||||
`set default model <provider/model>` for an already
|
||||
configured route; it live-tests the route before saving it. To configure or
|
||||
repair provider/auth access, exit Crestodian and run `openclaw onboard`.
|
||||
|
||||
Plugin uninstall is refused inside Crestodian because removing a provider
|
||||
plugin could disable the inference route powering the session. Exit Crestodian
|
||||
and run `openclaw plugins uninstall <id>` from a terminal.
|
||||
|
||||
Approval is given in your own words: unambiguous replies ("yes", "sure", "go ahead", "not now") resolve from a closed deterministic list. When the configured route supports a separate completion call, other replies can be classified from only your message and the pending proposal — never by the conversation model itself, which cannot self-approve. Unclassified or ambiguous replies keep the proposal pending and the conversation asks again.
|
||||
|
||||
Applied writes are recorded in `~/.openclaw/audit/crestodian.jsonl`. Discovery is not audited; only applied operations and writes are.
|
||||
|
||||
Channel setup can run as a hosted conversation until it reaches a secret. The
|
||||
local Crestodian TUI does not accept sensitive wizard answers because terminal
|
||||
chat input is visible. It offers `open channel wizard` immediately, carrying
|
||||
the selected channel into the masked terminal wizard; you can also run
|
||||
`openclaw channels add --channel <channel>` later.
|
||||
|
||||
### Switching to masked channel setup
|
||||
|
||||
The local chat can hand control to the masked channel wizard:
|
||||
|
||||
```text
|
||||
open channel wizard for slack
|
||||
channel info slack
|
||||
```
|
||||
|
||||
`open channel wizard for <channel>` opens masked channel setup after the chat
|
||||
TUI closes. Use `channel info <channel>` first for the channel label, setup
|
||||
state, prerequisites summary, and docs link.
|
||||
|
||||
Crestodian never changes provider/auth access from inside its own session: the
|
||||
session already depends on that inference route. For model-provider setup or
|
||||
repair, `configure model provider` returns exit/onboarding guidance without
|
||||
starting a wizard or writing config. Exit Crestodian and run `openclaw
|
||||
onboard`; onboarding stages the credentials and saves only a route that
|
||||
completes a real live turn. Start Crestodian again after onboarding succeeds.
|
||||
|
||||
## Setup bootstrap
|
||||
|
||||
`setup` configures the remaining workspace and Gateway state after guided onboarding has already established inference. It writes only through typed config operations and asks for approval first.
|
||||
|
||||
```text
|
||||
setup
|
||||
setup workspace ~/Projects/work
|
||||
```
|
||||
|
||||
`setup` preserves the verified effective model. It does not configure or
|
||||
replace inference.
|
||||
|
||||
If inference is missing or its live check fails, leave Crestodian and run `openclaw onboard`. Guided onboarding detects configured models, API keys, and authenticated local CLIs, asks each candidate for a real reply, and persists only a passing route. Crestodian starts immediately after that boundary and can then configure the workspace, Gateway, channels, agents, plugins, and other optional features.
|
||||
|
||||
The macOS app skips this ladder entirely when it reaches a configured Gateway
|
||||
whose default agent already has a configured model; it opens the normal agent
|
||||
UI.
|
||||
For a fresh or incomplete Gateway, the app drives the inference ladder through
|
||||
the `crestodian.setup.detect` and `crestodian.setup.activate` Gateway methods:
|
||||
detect lists every candidate backend it finds, activate live-tests one
|
||||
candidate (a real "reply with OK" completion), and only persists the model,
|
||||
credential, and provider/runtime state needed for that route after the test passes. Workspace and Gateway defaults remain for Crestodian. A failing candidate
|
||||
never changes config; the app automatically walks down the ladder and finally
|
||||
offers a manual key/token step populated from the Gateway's active
|
||||
text-inference provider plugins. The selected provider owns its starter model
|
||||
and config, and the credential is verified the same way before it is saved.
|
||||
|
||||
Codex supervision and other optional plugin features stay outside this
|
||||
inference activation transaction. Configure them only after inference is
|
||||
working and Crestodian has started; existing plugin policy and explicit
|
||||
supervision opt-outs remain untouched during inference setup.
|
||||
|
||||
## AI conversation
|
||||
|
||||
Interactive Crestodian's free-form conversation runs through the same agent loop as regular OpenClaw agents, restricted to one ring-zero OpenClaw authority tool, `crestodian`, that wraps the typed operations. Read actions run freely, mutations require your conversational approval for that exact operation (see Operations and approval), and every applied write is audited and re-validated. The agent session persists, so Crestodian has real multi-turn memory. If the verified inference route later stops working, return to `openclaw onboard` and repair it before continuing.
|
||||
|
||||
The host does not parse natural-language requests into operations. Free-form
|
||||
messages — including command-looking text and questions such as "why did my
|
||||
gateway stop?" — go to the AI, which can map the request to a typed operation
|
||||
through the `crestodian` tool.
|
||||
|
||||
When a mutation is pending, only unambiguous approval or decline phrases from a
|
||||
closed list are resolved without inference. Ambiguous consent goes to a
|
||||
separate configured completion call and otherwise fails closed. Structured
|
||||
wizard fields and exact host navigation are UI controls, not natural-language
|
||||
operation parsing. One secret-hygiene exception is especially important: an
|
||||
exact `config set` on a sensitive path (tokens, keys, passwords) never reaches
|
||||
a model. The host creates a redacted proposal, and the value is masked in the
|
||||
AI-visible history. Prefer `config set-ref <path> env <ENV_VAR>` for secrets.
|
||||
|
||||
Message-channel rescue mode never uses the model-assisted planner. Remote rescue stays deterministic so a broken or compromised normal agent path cannot be used as a config editor.
|
||||
|
||||
### CLI harness trust model
|
||||
|
||||
Embedded runtimes and the Codex app-server harness enforce the ring-zero
|
||||
restriction directly: the run carries an OpenClaw tool allow-list with only
|
||||
the `crestodian` tool. For Codex, OpenClaw also disables environments, native
|
||||
execution, multi-agent, goal, app/plugin, skill/MCP, web-search, and
|
||||
`request_user_input` surfaces for that run. Codex still injects its inert native `update_plan`
|
||||
utility; it can update the model's temporary checklist but cannot write files
|
||||
or OpenClaw configuration. CLI harnesses do not consume OpenClaw's allow-list,
|
||||
so Crestodian admits only backends whose own tool-selection contract can prove
|
||||
the same restriction:
|
||||
|
||||
- Selectable backends, including Claude Code, launch with an empty native-tool
|
||||
selection and one MCP tool, `crestodian`. Claude's generated MCP config is
|
||||
applied with `--strict-mcp-config`, so no other MCP servers are loaded.
|
||||
- Backends that declare no native tools receive the same dedicated Crestodian
|
||||
MCP server.
|
||||
- Always-on or unknown native-tool backends fail closed before inference; they
|
||||
cannot host a Crestodian session.
|
||||
|
||||
Only Crestodian sessions get the crestodian MCP server; normal agent runs
|
||||
never see this tool. Selectable/no-native CLI backends and API-key models
|
||||
therefore enforce the literal single-tool loop. Codex app-server models enforce
|
||||
a single OpenClaw authority tool plus the inert native planning utility. In all
|
||||
three cases, setup writes remain confined to Crestodian's audited approval
|
||||
contract.
|
||||
|
||||
Gemini CLI remains available for normal agents, but it cannot enforce the
|
||||
tool-free probe required by the inference gate, so it cannot host Crestodian.
|
||||
|
||||
## Switching to an agent
|
||||
|
||||
Use a natural-language selector to leave Crestodian and open the normal TUI:
|
||||
|
||||
```text
|
||||
talk to agent
|
||||
talk to work agent
|
||||
switch to main agent
|
||||
```
|
||||
|
||||
`openclaw tui`, `openclaw chat`, and `openclaw terminal` open the normal agent TUI directly; they do not start Crestodian. After switching into the normal TUI, `/crestodian` returns to Crestodian, optionally with a follow-up request:
|
||||
|
||||
```text
|
||||
/crestodian
|
||||
/crestodian restart gateway
|
||||
```
|
||||
|
||||
## Message rescue mode
|
||||
|
||||
Message rescue mode is the message-channel entrypoint for Crestodian: use it when your normal agent is dead but a trusted channel (for example WhatsApp) still receives commands.
|
||||
|
||||
This is a deterministic emergency command handler, not the conversational
|
||||
Crestodian agent. It does not bootstrap a fresh setup or relax the inference
|
||||
gate for Crestodian chat.
|
||||
|
||||
Supported command: `/crestodian <request>`. Rescue accepts the exact typed command grammar only — natural language is rejected with a hint, never guessed into an operation, and no model is ever consulted.
|
||||
|
||||
```text
|
||||
You, in a trusted owner DM: /crestodian status
|
||||
OpenClaw: Crestodian rescue mode. Gateway reachable: no. Config valid: no.
|
||||
You: /crestodian restart gateway
|
||||
OpenClaw: Plan: restart the Gateway. Reply /crestodian yes to apply.
|
||||
You: /crestodian yes
|
||||
OpenClaw: Applied. Audit entry written.
|
||||
```
|
||||
|
||||
Agent creation can also be queued locally or via rescue:
|
||||
|
||||
```text
|
||||
create agent work workspace ~/Projects/work model openai/gpt-5.6-sol
|
||||
/crestodian create agent work workspace ~/Projects/work
|
||||
```
|
||||
|
||||
Agent creation may name only the current live-verified default model. Omit the
|
||||
model to inherit that route.
|
||||
|
||||
Remote rescue is an admin surface and must be treated like remote config repair, not normal chat.
|
||||
|
||||
Security contract for remote rescue:
|
||||
|
||||
- Disabled when sandboxing is active for the agent/session; Crestodian refuses remote rescue and points to local CLI repair.
|
||||
- Default effective state is `auto`: allow remote rescue only in trusted YOLO operation, where the runtime already has unsandboxed local authority (`tools.exec.security` resolves to `full` and `tools.exec.ask` resolves to `off`, with sandbox mode `off`).
|
||||
- Requires an explicit owner identity; no wildcard sender rules, open group policy, unauthenticated webhooks, or anonymous channels.
|
||||
- Owner DMs only by default; group/channel rescue needs explicit opt-in.
|
||||
- Plugin search and list are read-only. Plugin install is always local-only (blocked in rescue, even when otherwise enabled) because it downloads executable code. Plugin uninstall is refused in both local Crestodian and rescue; run `openclaw plugins uninstall <id>` from a terminal.
|
||||
- Remote rescue cannot open the local TUI or switch into an interactive agent session; use local `openclaw` for agent handoff.
|
||||
- Persistent writes still require approval, even in rescue mode.
|
||||
- Every applied rescue operation is audited. Message-channel rescue records channel, account, sender, and source-address metadata; config-mutating operations also record config hashes before and after.
|
||||
- Secrets are never echoed. SecretRef inspection reports availability, not values.
|
||||
- If the Gateway is alive, rescue prefers Gateway typed operations; if it is dead, rescue uses only the minimal local repair surface that does not depend on the normal agent loop.
|
||||
|
||||
Config shape:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"crestodian": {
|
||||
"rescue": {
|
||||
"enabled": "auto",
|
||||
"ownerDmOnly": true,
|
||||
"pendingTtlMinutes": 15,
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
- `enabled`: `"auto"` (default) allows rescue only when the effective runtime is YOLO and sandboxing is off; `false` never allows message-channel rescue; `true` explicitly allows rescue when owner/channel checks pass (still subject to the sandboxing denial).
|
||||
- `ownerDmOnly`: restrict rescue to owner direct messages. Default `true`.
|
||||
- `pendingTtlMinutes`: how long a pending rescue write stays open for `/crestodian yes` approval before expiring. Default `15`.
|
||||
|
||||
Remote rescue is covered by the Docker lane:
|
||||
|
||||
```bash
|
||||
pnpm test:docker:crestodian-rescue
|
||||
```
|
||||
|
||||
An opt-in live channel command-surface smoke checks `/crestodian status` plus a persistent approval roundtrip through the rescue handler:
|
||||
|
||||
```bash
|
||||
pnpm test:live:crestodian-rescue-channel
|
||||
```
|
||||
|
||||
Inference-gated packaged one-shot setup is covered by:
|
||||
|
||||
```bash
|
||||
pnpm test:docker:crestodian-first-run
|
||||
```
|
||||
|
||||
That packaged-CLI lane starts with an empty state dir and proves Crestodian
|
||||
fails closed without inference. It then tests and activates fake Claude through
|
||||
the packaged activation module. Only afterward does a fuzzy request reach the
|
||||
planner and resolve to typed setup, followed by one-shot commands that create an
|
||||
additional agent, configure Discord through a plugin enablement plus token
|
||||
SecretRef, validate config, and check the audit log. This lane is supporting
|
||||
gate/operation evidence; it does not exercise interactive onboarding or the
|
||||
Crestodian agent/tool/approval conversation. The QA Lab scenario below redirects
|
||||
to the same Docker lane:
|
||||
|
||||
```bash
|
||||
pnpm openclaw qa suite --scenario crestodian-ring-zero-setup
|
||||
```
|
||||
|
||||
## Related
|
||||
|
||||
- [CLI reference](/cli)
|
||||
- [Doctor](/cli/doctor)
|
||||
- [TUI](/cli/tui)
|
||||
- [Sandbox](/cli/sandbox)
|
||||
- [Security](/cli/security)
|
||||
This page moved to [OpenClaw setup and repair](/cli/openclaw).
|
||||
|
||||
+18
-18
@@ -12,29 +12,29 @@ the commands, global flags, and output styling rules that apply across the CLI.
|
||||
|
||||
Setup commands by intent:
|
||||
|
||||
- `openclaw setup` and `openclaw onboard` verify inference first, then start Crestodian for Gateway, workspace, channels, skills, and health setup.
|
||||
- `openclaw setup` and `openclaw onboard` verify inference first, then start OpenClaw for Gateway, workspace, channels, skills, and health setup.
|
||||
- `openclaw setup --baseline` creates the baseline config and workspace without walking the guided onboarding flow.
|
||||
- `openclaw configure` changes targeted parts of an existing setup: model auth, gateway, channels, plugins, or skills.
|
||||
- `openclaw channels add` configures channel accounts after the baseline exists; run without flags for guided setup, or with channel-specific flags for scripts.
|
||||
|
||||
## Command pages
|
||||
|
||||
| Area | Commands |
|
||||
| ---------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Setup and onboarding | [`crestodian`](/cli/crestodian) · [`setup`](/cli/setup) · [`onboard`](/cli/onboard) · [`configure`](/cli/configure) · [`config`](/cli/config) · [`completion`](/cli/completion) · [`doctor`](/cli/doctor) · [`dashboard`](/cli/dashboard) |
|
||||
| Reset, backup, and migration | [`backup`](/cli/backup) · [`migrate`](/cli/migrate) · [`reset`](/cli/reset) · [`uninstall`](/cli/uninstall) · [`update`](/cli/update) |
|
||||
| Messaging and agents | [`message`](/cli/message) · [`agent`](/cli/agent) · [`agents`](/cli/agents) · [`attach`](/cli/attach) · [`acp`](/cli/acp) · [`mcp`](/cli/mcp) |
|
||||
| Health and sessions | [`status`](/cli/status) · [`health`](/cli/health) · [`sessions`](/cli/sessions) · [`audit`](/cli/audit) |
|
||||
| Gateway and logs | [`gateway`](/cli/gateway) · [`logs`](/cli/logs) · [`system`](/cli/system) |
|
||||
| Models and inference | [`models`](/cli/models) · [`promos`](/cli/promos) · [`infer`](/cli/infer) · `capability` (alias for [`infer`](/cli/infer)) · [`memory`](/cli/memory) · [`commitments`](/cli/commitments) · [`wiki`](/cli/wiki) |
|
||||
| Network and nodes | [`directory`](/cli/directory) · [`nodes`](/cli/nodes) · [`devices`](/cli/devices) · [`node`](/cli/node) · [`worker`](/cli/worker) |
|
||||
| Runtime and sandbox | [`approvals`](/cli/approvals) · `exec-policy` (see [`approvals`](/cli/approvals)) · [`sandbox`](/cli/sandbox) · [`tui`](/cli/tui) · `chat`/`terminal` (aliases for [`tui --local`](/cli/tui)) · [`browser`](/cli/browser) |
|
||||
| Automation | [`cron`](/cli/cron) · [`tasks`](/cli/tasks) · [`hooks`](/cli/hooks) · [`webhooks`](/cli/webhooks) · [`transcripts`](/cli/transcripts) |
|
||||
| Discovery and docs | [`dns`](/cli/dns) · [`docs`](/cli/docs) |
|
||||
| Pairing and channels | [`pairing`](/cli/pairing) · [`qr`](/cli/qr) · [`channels`](/cli/channels) |
|
||||
| Security and plugins | [`security`](/cli/security) · [`secrets`](/cli/secrets) · [`skills`](/cli/skills) · [`plugins`](/cli/plugins) · [`proxy`](/cli/proxy) |
|
||||
| Legacy aliases | [`daemon`](/cli/daemon) (gateway service) · [`clawbot`](/cli/clawbot) (namespace) |
|
||||
| Plugins (optional) | [`path`](/cli/path) · [`policy`](/cli/policy) · [`voicecall`](/cli/voicecall) · [`workboard`](/cli/workboard) (if installed) |
|
||||
| Area | Commands |
|
||||
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| Setup and onboarding | [`openclaw`](/cli/openclaw) · [`setup`](/cli/setup) · [`onboard`](/cli/onboard) · [`configure`](/cli/configure) · [`config`](/cli/config) · [`completion`](/cli/completion) · [`doctor`](/cli/doctor) · [`dashboard`](/cli/dashboard) |
|
||||
| Reset, backup, and migration | [`backup`](/cli/backup) · [`migrate`](/cli/migrate) · [`reset`](/cli/reset) · [`uninstall`](/cli/uninstall) · [`update`](/cli/update) |
|
||||
| Messaging and agents | [`message`](/cli/message) · [`agent`](/cli/agent) · [`agents`](/cli/agents) · [`attach`](/cli/attach) · [`acp`](/cli/acp) · [`mcp`](/cli/mcp) |
|
||||
| Health and sessions | [`status`](/cli/status) · [`health`](/cli/health) · [`sessions`](/cli/sessions) · [`audit`](/cli/audit) |
|
||||
| Gateway and logs | [`gateway`](/cli/gateway) · [`logs`](/cli/logs) · [`system`](/cli/system) |
|
||||
| Models and inference | [`models`](/cli/models) · [`promos`](/cli/promos) · [`infer`](/cli/infer) · `capability` (alias for [`infer`](/cli/infer)) · [`memory`](/cli/memory) · [`commitments`](/cli/commitments) · [`wiki`](/cli/wiki) |
|
||||
| Network and nodes | [`directory`](/cli/directory) · [`nodes`](/cli/nodes) · [`devices`](/cli/devices) · [`node`](/cli/node) · [`worker`](/cli/worker) |
|
||||
| Runtime and sandbox | [`approvals`](/cli/approvals) · `exec-policy` (see [`approvals`](/cli/approvals)) · [`sandbox`](/cli/sandbox) · [`tui`](/cli/tui) · `chat`/`terminal` (aliases for [`tui --local`](/cli/tui)) · [`browser`](/cli/browser) |
|
||||
| Automation | [`cron`](/cli/cron) · [`tasks`](/cli/tasks) · [`hooks`](/cli/hooks) · [`webhooks`](/cli/webhooks) · [`transcripts`](/cli/transcripts) |
|
||||
| Discovery and docs | [`dns`](/cli/dns) · [`docs`](/cli/docs) |
|
||||
| Pairing and channels | [`pairing`](/cli/pairing) · [`qr`](/cli/qr) · [`channels`](/cli/channels) |
|
||||
| Security and plugins | [`security`](/cli/security) · [`secrets`](/cli/secrets) · [`skills`](/cli/skills) · [`plugins`](/cli/plugins) · [`proxy`](/cli/proxy) |
|
||||
| Legacy aliases | [`daemon`](/cli/daemon) (gateway service) · [`clawbot`](/cli/clawbot) (namespace) |
|
||||
| Plugins (optional) | [`path`](/cli/path) · [`policy`](/cli/policy) · [`voicecall`](/cli/voicecall) · [`workboard`](/cli/workboard) (if installed) |
|
||||
|
||||
## Global flags
|
||||
|
||||
@@ -83,7 +83,7 @@ independently; run `<command> --help` for the authoritative, current list.
|
||||
|
||||
```
|
||||
openclaw [--dev] [--profile <name>] <command>
|
||||
crestodian
|
||||
openclaw
|
||||
setup
|
||||
onboard
|
||||
configure
|
||||
|
||||
+21
-19
@@ -1,7 +1,7 @@
|
||||
---
|
||||
summary: "CLI reference for `openclaw onboard` (interactive onboarding)"
|
||||
read_when:
|
||||
- You want to establish inference, then finish setup with Crestodian
|
||||
- You want to establish inference, then finish setup with OpenClaw
|
||||
title: "Onboard"
|
||||
---
|
||||
|
||||
@@ -9,8 +9,10 @@ title: "Onboard"
|
||||
|
||||
Guided setup that establishes inference first: it detects existing AI access,
|
||||
requires a live completion, persists only the working route, and then starts
|
||||
Crestodian to configure the rest. `openclaw setup` is the same entry point;
|
||||
`openclaw setup --baseline` only writes the baseline config/workspace.
|
||||
OpenClaw to configure the rest. `openclaw setup` reaches this flow on fresh
|
||||
systems or whenever an onboarding option is present; configured systems use
|
||||
bare `openclaw setup` for system-agent chat. `openclaw setup --baseline` only
|
||||
writes the baseline config/workspace.
|
||||
|
||||
<CardGroup cols={2}>
|
||||
<Card title="CLI onboarding hub" href="/start/wizard" icon="rocket">
|
||||
@@ -51,8 +53,8 @@ openclaw onboard --mode remote --remote-url wss://gateway-host:18789
|
||||
- `--flow manual` (alias `advanced`): opens the classic wizard with full prompts
|
||||
for port, bind, and auth.
|
||||
- `--flow import`: runs a detected migration provider (for example Hermes via `--import-from hermes`), previews the plan, then applies after confirmation. Import only runs against a fresh OpenClaw setup - reset config, credentials, sessions, and workspace state first if any exist. Use [`openclaw migrate`](/cli/migrate) for dry-run plans, overwrite mode, reports, and exact mappings.
|
||||
- `--modern` is a compatibility alias for the Crestodian conversational setup
|
||||
assistant. It uses the same live-inference gate as `openclaw crestodian` and
|
||||
- `--modern` is a compatibility alias for the OpenClaw conversational setup
|
||||
assistant. It uses the same live-inference gate as `openclaw setup` and
|
||||
accepts only `--workspace`, `--accept-risk`,
|
||||
`--non-interactive`, and `--json`. Other setup flags are rejected instead of
|
||||
being silently ignored.
|
||||
@@ -72,21 +74,21 @@ then appear in a second menu. Supported browser or device sign-in and masked
|
||||
API-key or token methods use the same live completion path. OpenClaw persists
|
||||
only the verified model route and its credential after the test succeeds; a
|
||||
failed candidate does not replace the configured model or save the attempted
|
||||
credential. Choose **Skip for now** to exit without starting Crestodian and
|
||||
credential. Choose **Skip for now** to exit without starting OpenClaw and
|
||||
rerun `openclaw onboard` when you are ready. Workspace and Gateway setup remain
|
||||
unchanged until Crestodian starts.
|
||||
unchanged until OpenClaw starts.
|
||||
|
||||
In guided mode, `--workspace <dir>` supplies Crestodian's proposed workspace
|
||||
In guided mode, `--workspace <dir>` supplies OpenClaw's proposed workspace
|
||||
and the isolated inference context. It is not persisted until you approve the
|
||||
Crestodian setup proposal. Classic and noninteractive onboarding persist their
|
||||
OpenClaw setup proposal. Classic and noninteractive onboarding persist their
|
||||
workspace through their normal setup flow.
|
||||
|
||||
After inference passes, guided onboarding immediately starts Crestodian with
|
||||
the verified model. Crestodian can then configure the workspace, Gateway,
|
||||
channels, agents, plugins, and other optional features. Inside Crestodian, use
|
||||
After inference passes, guided onboarding immediately starts OpenClaw with
|
||||
the verified model. OpenClaw can then configure the workspace, Gateway,
|
||||
channels, agents, plugins, and other optional features. Inside OpenClaw, use
|
||||
`open channel wizard for <channel>` to hand channel credential collection to a
|
||||
masked terminal wizard. To change the model provider or its authentication,
|
||||
exit Crestodian and run `openclaw onboard`; Crestodian does not open the guided
|
||||
exit OpenClaw and run `openclaw onboard`; OpenClaw does not open the guided
|
||||
or classic provider flows.
|
||||
|
||||
On a configured install, running `openclaw onboard` again verifies the current
|
||||
@@ -97,10 +99,10 @@ workspace, so a model provided by a workspace plugin can fail here while still
|
||||
working in the agent.
|
||||
Use `openclaw onboard --classic` for provider-specific auth, channels, skills,
|
||||
remote Gateway setup, imports, or full Gateway controls. For conversational
|
||||
non-inference setup and repair, run `openclaw crestodian`; `openclaw onboard
|
||||
non-inference setup and repair, run `openclaw setup`; `openclaw onboard
|
||||
--modern` is a compatibility alias through the same inference gate. The classic
|
||||
wizard can optionally verify the default model with a live completion, but
|
||||
Crestodian will not start until its own live inference check passes.
|
||||
OpenClaw will not start until its own live inference check passes.
|
||||
|
||||
In an interactive terminal, bare `openclaw` (no subcommand) routes by config
|
||||
state:
|
||||
@@ -108,12 +110,12 @@ state:
|
||||
- If the active config file is missing or has no authored settings (empty or
|
||||
metadata-only), it starts guided onboarding.
|
||||
- If the config file exists but fails validation, it starts the classic
|
||||
onboarding path with `openclaw doctor` guidance. Crestodian needs working
|
||||
onboarding path with `openclaw doctor` guidance. OpenClaw needs working
|
||||
inference and is not used to repair this pre-inference state.
|
||||
- If the config file is valid, it opens the normal agent TUI. A reachable
|
||||
configured Gateway with an agent and model goes directly to that UI without
|
||||
onboarding or Crestodian. On a configured install, reach Crestodian with
|
||||
`/crestodian` inside the TUI or `openclaw crestodian`.
|
||||
onboarding or OpenClaw. On a configured install, reach OpenClaw with
|
||||
`/openclaw` inside the TUI or `openclaw setup`.
|
||||
|
||||
Plaintext `ws://` is accepted for loopback, private IP literals, `.local`, and Tailnet `*.ts.net` gateway URLs. For other trusted private-DNS names, set `OPENCLAW_ALLOW_INSECURE_PRIVATE_WS=1` in the onboarding process environment.
|
||||
|
||||
@@ -274,7 +276,7 @@ Output: `--suppress-gateway-token-output` suppresses token-bearing Gateway/UI ou
|
||||
|
||||
<Note>
|
||||
`--json` does not imply non-interactive mode in guided or classic onboarding.
|
||||
With `--modern`, JSON is a one-shot Crestodian overview and exits after that
|
||||
With `--modern`, JSON is a one-shot OpenClaw overview and exits after that
|
||||
single result. Use `--non-interactive` for other scripts.
|
||||
</Note>
|
||||
|
||||
|
||||
@@ -0,0 +1,353 @@
|
||||
---
|
||||
summary: "CLI reference and security model for the inference-backed OpenClaw setup and repair helper"
|
||||
read_when:
|
||||
- You finished inference setup and want OpenClaw to configure the rest
|
||||
- You need to inspect or repair OpenClaw with the local setup agent
|
||||
- You are designing or enabling message-channel rescue mode
|
||||
title: "OpenClaw setup agent"
|
||||
---
|
||||
|
||||
# `openclaw setup`
|
||||
|
||||
OpenClaw ships with a built-in system agent — it speaks as "OpenClaw" — for
|
||||
local setup, repair, and configuration (formerly called Crestodian). It starts only after the effective default model completes a real turn.
|
||||
Fresh installs establish inference first; malformed config stays on the
|
||||
classic doctor path.
|
||||
|
||||
## When it starts
|
||||
|
||||
Running `openclaw` with no subcommand routes based on config state:
|
||||
|
||||
- Config missing, or exists with no authored settings (empty, or only `$schema`/`meta` keys): starts guided onboarding with live AI verification.
|
||||
- Config exists but fails validation: starts classic onboarding, which reports the issues and directs you to `openclaw doctor`.
|
||||
- Config exists and is valid: opens the normal agent TUI. A reachable
|
||||
configured Gateway whose default agent has a model goes directly to that UI
|
||||
without onboarding or OpenClaw. Use `/openclaw` inside the TUI, or run
|
||||
`openclaw setup` directly, to reach OpenClaw later.
|
||||
|
||||
Running `openclaw setup` first live-tests the configured default model. A passing turn starts OpenClaw. An interactive failure opens guided inference setup and hands off to OpenClaw after a candidate passes. One-shot, JSON, and other noninteractive requests fail with instructions to run `openclaw onboard` when inference is unavailable. `openclaw --help` and `openclaw --version` keep their normal fast paths.
|
||||
|
||||
Noninteractive bare `openclaw` (no TTY) exits with a short message instead of printing root help: it points to non-interactive onboarding on a fresh or invalid install, or to `openclaw agent --local ...` when config is valid.
|
||||
|
||||
`openclaw onboard --modern` remains a compatibility alias for OpenClaw, but uses the same inference gate: working inference opens the chat, interactive failures start guided inference setup, and noninteractive failures exit with onboarding guidance. `openclaw onboard --classic` opens the full step-by-step wizard.
|
||||
|
||||
## What OpenClaw shows
|
||||
|
||||
Interactive OpenClaw opens the same TUI shell as `openclaw tui`, with an OpenClaw chat backend. The startup greeting covers:
|
||||
|
||||
- config validity and the default agent
|
||||
- the verified model OpenClaw is using
|
||||
- Gateway reachability from the first startup probe
|
||||
- the next recommended debug action
|
||||
|
||||
It does not dump secrets or load plugin CLI commands just to start.
|
||||
|
||||
Use `status` for the detailed inventory: config path, docs/source paths, local CLI probes, key/token presence, agents, model, and Gateway details.
|
||||
|
||||
OpenClaw uses the same reference discovery as regular agents: in a Git checkout it points at local `docs/` and the source tree; in an npm install it uses bundled docs and links to [https://github.com/openclaw/openclaw](https://github.com/openclaw/openclaw), with guidance to check source when docs are not enough.
|
||||
|
||||
## Examples
|
||||
|
||||
```bash
|
||||
openclaw
|
||||
openclaw setup
|
||||
openclaw setup --json
|
||||
openclaw setup --message "models"
|
||||
openclaw setup --message "validate config"
|
||||
openclaw setup --message "setup workspace ~/Projects/work" --yes
|
||||
openclaw setup --message "set default model openai/gpt-5.6" --yes
|
||||
openclaw onboard --modern
|
||||
```
|
||||
|
||||
Inside the OpenClaw TUI:
|
||||
|
||||
```text
|
||||
status
|
||||
health
|
||||
doctor
|
||||
validate config
|
||||
setup
|
||||
setup workspace ~/Projects/work
|
||||
config set gateway.port 19001
|
||||
config set-ref gateway.auth.token env OPENCLAW_GATEWAY_TOKEN
|
||||
gateway status
|
||||
restart gateway
|
||||
agents
|
||||
create agent work workspace ~/Projects/work
|
||||
models
|
||||
configure model provider
|
||||
set default model openai/gpt-5.6
|
||||
channels
|
||||
channel info slack
|
||||
connect slack
|
||||
open channel wizard for slack
|
||||
plugins list
|
||||
plugins search slack
|
||||
plugin install clawhub:openclaw-codex-app-server
|
||||
talk to work agent
|
||||
talk to agent for ~/Projects/work
|
||||
audit
|
||||
quit
|
||||
```
|
||||
|
||||
## Operations and approval
|
||||
|
||||
OpenClaw uses typed operations instead of editing config ad hoc.
|
||||
|
||||
Read-only operations run immediately: show overview, list agents, list installed plugins, search ClawHub plugins, show model/backend status, run status/health checks, check Gateway reachability, run doctor without interactive fixes, validate config, show the audit-log path.
|
||||
|
||||
Starting guided channel setup (`connect telegram`) also runs immediately. Its wizard collects explicit answers and owns the resulting writes.
|
||||
|
||||
Persistent operations require conversational approval (or `--yes` for a direct command): write config, `config set`, `config set-ref`, setup/onboarding bootstrap, change the default model, start/stop/restart the Gateway, create agents, and install plugins.
|
||||
|
||||
Doctor repairs are unavailable inside OpenClaw because they can rewrite the provider, authentication, or default-agent inference route powering the session. Exit OpenClaw and run `openclaw doctor --fix` in a terminal. Read-only `doctor` remains available inside OpenClaw.
|
||||
|
||||
New agents inherit the live-verified default inference route. The agent ids `openclaw` and `crestodian` are reserved for the system agent and cannot be created as normal agents. The retired id remains blocked so an old config cannot claim it.
|
||||
|
||||
`config set` and `config set-ref` cannot change inference-route state,
|
||||
including inference-provider credentials, top-level `auth.*`, model catalogs,
|
||||
CLI backends, default/per-agent model routes, agent params/tools, or root
|
||||
`tools.*`. Raw writes under `env.*`, `secrets.*`, `plugins.*`, and `$include`
|
||||
are also refused because they can replace credential resolution or provider
|
||||
activation. Gateway and channel auth remain normal config surfaces. Use typed plugin/channel workflows and
|
||||
`set default model <provider/model>` for an already
|
||||
configured route; it live-tests the route before saving it. To configure or
|
||||
repair provider/auth access, exit OpenClaw and run `openclaw onboard`.
|
||||
|
||||
Plugin uninstall is refused inside OpenClaw because removing a provider
|
||||
plugin could disable the inference route powering the session. Exit OpenClaw
|
||||
and run `openclaw plugins uninstall <id>` from a terminal.
|
||||
|
||||
Approval is given in your own words: unambiguous replies ("yes", "sure", "go ahead", "not now") resolve from a closed deterministic list. When the configured route supports a separate completion call, other replies can be classified from only your message and the pending proposal — never by the conversation model itself, which cannot self-approve. Unclassified or ambiguous replies keep the proposal pending and the conversation asks again.
|
||||
|
||||
Applied writes are recorded in `~/.openclaw/audit/system-agent.jsonl`. Discovery is not audited; only applied operations and writes are.
|
||||
|
||||
Channel setup can run as a hosted conversation until it reaches a secret. The
|
||||
local OpenClaw TUI does not accept sensitive wizard answers because terminal
|
||||
chat input is visible. It offers `open channel wizard` immediately, carrying
|
||||
the selected channel into the masked terminal wizard; you can also run
|
||||
`openclaw channels add --channel <channel>` later.
|
||||
|
||||
### Switching to masked channel setup
|
||||
|
||||
The local chat can hand control to the masked channel wizard:
|
||||
|
||||
```text
|
||||
open channel wizard for slack
|
||||
channel info slack
|
||||
```
|
||||
|
||||
`open channel wizard for <channel>` opens masked channel setup after the chat
|
||||
TUI closes. Use `channel info <channel>` first for the channel label, setup
|
||||
state, prerequisites summary, and docs link.
|
||||
|
||||
OpenClaw never changes provider/auth access from inside its own session: the
|
||||
session already depends on that inference route. For model-provider setup or
|
||||
repair, `configure model provider` returns exit/onboarding guidance without
|
||||
starting a wizard or writing config. Exit OpenClaw and run `openclaw
|
||||
onboard`; onboarding stages the credentials and saves only a route that
|
||||
completes a real live turn. Start OpenClaw again after onboarding succeeds.
|
||||
|
||||
## Setup bootstrap
|
||||
|
||||
`setup` configures the remaining workspace and Gateway state after guided onboarding has already established inference. It writes only through typed config operations and asks for approval first.
|
||||
|
||||
```text
|
||||
setup
|
||||
setup workspace ~/Projects/work
|
||||
```
|
||||
|
||||
`setup` preserves the verified effective model. It does not configure or
|
||||
replace inference.
|
||||
|
||||
If inference is missing or its live check fails, leave OpenClaw and run `openclaw onboard`. Guided onboarding detects configured models, API keys, and authenticated local CLIs, asks each candidate for a real reply, and persists only a passing route. OpenClaw starts immediately after that boundary and can then configure the workspace, Gateway, channels, agents, plugins, and other optional features.
|
||||
|
||||
The macOS app skips this ladder entirely when it reaches a configured Gateway
|
||||
whose default agent already has a configured model; it opens the normal agent
|
||||
UI.
|
||||
For a fresh or incomplete Gateway, the app drives the inference ladder through
|
||||
the `openclaw.setup.detect` and `openclaw.setup.activate` Gateway methods:
|
||||
detect lists every candidate backend it finds, activate live-tests one
|
||||
candidate (a real "reply with OK" completion), and only persists the model,
|
||||
credential, and provider/runtime state needed for that route after the test passes. Workspace and Gateway defaults remain for OpenClaw. A failing candidate
|
||||
never changes config; the app automatically walks down the ladder and finally
|
||||
offers a manual key/token step populated from the Gateway's active
|
||||
text-inference provider plugins. The selected provider owns its starter model
|
||||
and config, and the credential is verified the same way before it is saved.
|
||||
|
||||
Codex supervision and other optional plugin features stay outside this
|
||||
inference activation transaction. Configure them only after inference is
|
||||
working and OpenClaw has started; existing plugin policy and explicit
|
||||
supervision opt-outs remain untouched during inference setup.
|
||||
|
||||
## AI conversation
|
||||
|
||||
Interactive OpenClaw's free-form conversation runs through the same agent loop as regular OpenClaw agents, restricted to one ring-zero OpenClaw authority tool, `openclaw`, that wraps the typed operations. Read actions run freely, mutations require your conversational approval for that exact operation (see Operations and approval), and every applied write is audited and re-validated. The agent session persists, so OpenClaw has real multi-turn memory. If the verified inference route later stops working, return to `openclaw onboard` and repair it before continuing.
|
||||
|
||||
The host does not parse natural-language requests into operations. Free-form
|
||||
messages — including command-looking text and questions such as "why did my
|
||||
gateway stop?" — go to the AI, which can map the request to a typed operation
|
||||
through the `openclaw` tool.
|
||||
|
||||
When a mutation is pending, only unambiguous approval or decline phrases from a
|
||||
closed list are resolved without inference. Ambiguous consent goes to a
|
||||
separate configured completion call and otherwise fails closed. Structured
|
||||
wizard fields and exact host navigation are UI controls, not natural-language
|
||||
operation parsing. One secret-hygiene exception is especially important: an
|
||||
exact `config set` on a sensitive path (tokens, keys, passwords) never reaches
|
||||
a model. The host creates a redacted proposal, and the value is masked in the
|
||||
AI-visible history. Prefer `config set-ref <path> env <ENV_VAR>` for secrets.
|
||||
|
||||
Message-channel rescue mode never uses the model-assisted planner. Remote rescue stays deterministic so a broken or compromised normal agent path cannot be used as a config editor.
|
||||
|
||||
### CLI harness trust model
|
||||
|
||||
Embedded runtimes and the Codex app-server harness enforce the ring-zero
|
||||
restriction directly: the run carries an OpenClaw tool allow-list with only
|
||||
the `openclaw` tool. For Codex, OpenClaw also disables environments, native
|
||||
execution, multi-agent, goal, app/plugin, skill/MCP, web-search, and
|
||||
`request_user_input` surfaces for that run. Codex still injects its inert native `update_plan`
|
||||
utility; it can update the model's temporary checklist but cannot write files
|
||||
or OpenClaw configuration. CLI harnesses do not consume OpenClaw's allow-list,
|
||||
so OpenClaw admits only backends whose own tool-selection contract can prove
|
||||
the same restriction:
|
||||
|
||||
- Selectable backends, including Claude Code, launch with an empty native-tool
|
||||
selection and one MCP tool, `openclaw`. Claude's generated MCP config is
|
||||
applied with `--strict-mcp-config`, so no other MCP servers are loaded.
|
||||
- Backends that declare no native tools receive the same dedicated OpenClaw
|
||||
MCP server.
|
||||
- Always-on or unknown native-tool backends fail closed before inference; they
|
||||
cannot host an OpenClaw session.
|
||||
|
||||
Only OpenClaw sessions get the openclaw MCP server; normal agent runs
|
||||
never see this tool. Selectable/no-native CLI backends and API-key models
|
||||
therefore enforce the literal single-tool loop. Codex app-server models enforce
|
||||
a single OpenClaw authority tool plus the inert native planning utility. In all
|
||||
three cases, setup writes remain confined to OpenClaw's audited approval
|
||||
contract.
|
||||
|
||||
Gemini CLI remains available for normal agents, but it cannot enforce the
|
||||
tool-free probe required by the inference gate, so it cannot host OpenClaw.
|
||||
|
||||
## Switching to an agent
|
||||
|
||||
Use a natural-language selector to leave OpenClaw and open the normal TUI:
|
||||
|
||||
```text
|
||||
talk to agent
|
||||
talk to work agent
|
||||
switch to main agent
|
||||
```
|
||||
|
||||
`openclaw tui`, `openclaw chat`, and `openclaw terminal` open the normal agent TUI directly; they do not start OpenClaw. After switching into the normal TUI, `/openclaw` returns to OpenClaw, optionally with a follow-up request:
|
||||
|
||||
```text
|
||||
/openclaw
|
||||
/openclaw restart gateway
|
||||
```
|
||||
|
||||
## Message rescue mode
|
||||
|
||||
Message rescue mode is the message-channel entrypoint for OpenClaw: use it when your normal agent is dead but a trusted channel (for example WhatsApp) still receives commands.
|
||||
|
||||
This is a deterministic emergency command handler, not the conversational
|
||||
OpenClaw agent. It does not bootstrap a fresh setup or relax the inference
|
||||
gate for OpenClaw chat.
|
||||
|
||||
Supported command: `/openclaw <request>`. Rescue accepts the exact typed command grammar only — natural language is rejected with a hint, never guessed into an operation, and no model is ever consulted.
|
||||
|
||||
```text
|
||||
You, in a trusted owner DM: /openclaw status
|
||||
OpenClaw: OpenClaw rescue mode. Gateway reachable: no. Config valid: no.
|
||||
You: /openclaw restart gateway
|
||||
OpenClaw: Plan: restart the Gateway. Reply /openclaw yes to apply.
|
||||
You: /openclaw yes
|
||||
OpenClaw: Applied. Audit entry written.
|
||||
```
|
||||
|
||||
Agent creation can also be queued locally or via rescue:
|
||||
|
||||
```text
|
||||
create agent work workspace ~/Projects/work model openai/gpt-5.6-sol
|
||||
/openclaw create agent work workspace ~/Projects/work
|
||||
```
|
||||
|
||||
Agent creation may name only the current live-verified default model. Omit the
|
||||
model to inherit that route.
|
||||
|
||||
Remote rescue is an admin surface and must be treated like remote config repair, not normal chat.
|
||||
|
||||
Security contract for remote rescue:
|
||||
|
||||
- Disabled when sandboxing is active for the agent/session; OpenClaw refuses remote rescue and points to local CLI repair.
|
||||
- Default effective state is `auto`: allow remote rescue only in trusted YOLO operation, where the runtime already has unsandboxed local authority (`tools.exec.security` resolves to `full` and `tools.exec.ask` resolves to `off`, with sandbox mode `off`).
|
||||
- Requires an explicit owner identity; no wildcard sender rules, open group policy, unauthenticated webhooks, or anonymous channels.
|
||||
- Owner DMs only by default; group/channel rescue needs explicit opt-in.
|
||||
- Plugin search and list are read-only. Plugin install is always local-only (blocked in rescue, even when otherwise enabled) because it downloads executable code. Plugin uninstall is refused in both local OpenClaw and rescue; run `openclaw plugins uninstall <id>` from a terminal.
|
||||
- Remote rescue cannot open the local TUI or switch into an interactive agent session; use local `openclaw` for agent handoff.
|
||||
- Persistent writes still require approval, even in rescue mode.
|
||||
- Every applied rescue operation is audited. Message-channel rescue records channel, account, sender, and source-address metadata; config-mutating operations also record config hashes before and after.
|
||||
- Secrets are never echoed. SecretRef inspection reports availability, not values.
|
||||
- If the Gateway is alive, rescue prefers Gateway typed operations; if it is dead, rescue uses only the minimal local repair surface that does not depend on the normal agent loop.
|
||||
|
||||
Config shape:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"systemAgent": {
|
||||
"rescue": {
|
||||
"enabled": "auto",
|
||||
"ownerDmOnly": true,
|
||||
"pendingTtlMinutes": 15,
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
- `enabled`: `"auto"` (default) allows rescue only when the effective runtime is YOLO and sandboxing is off; `false` never allows message-channel rescue; `true` explicitly allows rescue when owner/channel checks pass (still subject to the sandboxing denial).
|
||||
- `ownerDmOnly`: restrict rescue to owner direct messages. Default `true`.
|
||||
- `pendingTtlMinutes`: how long a pending rescue write stays open for `/openclaw yes` approval before expiring. Default `15`.
|
||||
|
||||
`openclaw doctor --fix` migrates the legacy `crestodian` config block to
|
||||
`systemAgent`. Runtime reads only the canonical block.
|
||||
|
||||
Remote rescue is covered by the Docker lane:
|
||||
|
||||
```bash
|
||||
pnpm test:docker:system-agent-rescue
|
||||
```
|
||||
|
||||
An opt-in live channel command-surface smoke checks `/openclaw status` plus a persistent approval roundtrip through the rescue handler:
|
||||
|
||||
```bash
|
||||
pnpm test:live:system-agent-rescue-channel
|
||||
```
|
||||
|
||||
Inference-gated packaged one-shot setup is covered by:
|
||||
|
||||
```bash
|
||||
pnpm test:docker:system-agent-first-run
|
||||
```
|
||||
|
||||
That packaged-CLI lane starts with an empty state dir and proves OpenClaw
|
||||
fails closed without inference. It then tests and activates fake Claude through
|
||||
the packaged activation module. Only afterward does a fuzzy request reach the
|
||||
planner and resolve to typed setup, followed by one-shot commands that create an
|
||||
additional agent, configure Discord through a plugin enablement plus token
|
||||
SecretRef, validate config, and check the audit log. This lane is supporting
|
||||
gate/operation evidence; it does not exercise interactive onboarding or the
|
||||
OpenClaw agent/tool/approval conversation. The QA Lab scenario below redirects
|
||||
to the same Docker lane:
|
||||
|
||||
```bash
|
||||
pnpm openclaw qa suite --scenario system-agent-ring-zero-setup
|
||||
```
|
||||
|
||||
## Related
|
||||
|
||||
- [CLI reference](/cli)
|
||||
- [Doctor](/cli/doctor)
|
||||
- [TUI](/cli/tui)
|
||||
- [Sandbox](/cli/sandbox)
|
||||
- [Security](/cli/security)
|
||||
+30
-12
@@ -1,7 +1,8 @@
|
||||
---
|
||||
summary: "CLI reference for `openclaw setup` (alias for onboarding, with baseline setup available by flag)"
|
||||
summary: "CLI reference for `openclaw setup` (system-agent chat with onboarding fallback)"
|
||||
read_when:
|
||||
- You're doing first-run setup with the CLI onboarding wizard
|
||||
- You want to chat with OpenClaw for setup or repair
|
||||
- You're doing first-run setup with the onboarding wizard
|
||||
- You want to set the default workspace path
|
||||
- You need the baseline-only setup flag for scripts
|
||||
title: "Setup"
|
||||
@@ -9,12 +10,23 @@ title: "Setup"
|
||||
|
||||
# `openclaw setup`
|
||||
|
||||
`openclaw setup` runs the same guided onboarding flow as `openclaw onboard`:
|
||||
it verifies and persists inference first, then starts Crestodian to configure
|
||||
the workspace, Gateway, channels, skills, and health. Use `--baseline` when you
|
||||
only need to initialize config/workspace folders without the wizard.
|
||||
`openclaw setup` is the system-agent entry point. On a configured system, bare
|
||||
`openclaw setup` opens an interactive OpenClaw chat. On a fresh system, it
|
||||
falls through to guided onboarding. Use `-m`/`--message` for one request or
|
||||
`--baseline` to initialize config/workspace folders without the wizard.
|
||||
|
||||
In guided mode, `--workspace <dir>` is the workspace proposed to Crestodian;
|
||||
Routing order:
|
||||
|
||||
1. Any onboarding option (`--wizard`, `--baseline`, workspace, reset,
|
||||
non-interactive, flow, mode, Gateway, daemon, skip, import, remote, or auth
|
||||
options) runs onboarding exactly as `openclaw onboard` does.
|
||||
2. `-m`/`--message` or `--yes` runs the system agent.
|
||||
3. With no routing option, a configured interactive system opens OpenClaw. A
|
||||
fresh system runs onboarding. On a configured system, `--json` prints the
|
||||
system overview even without a TTY; an onboarding option keeps onboarding's
|
||||
JSON summary.
|
||||
|
||||
In guided mode, `--workspace <dir>` is the workspace proposed to OpenClaw;
|
||||
it is persisted only after you approve that proposal. Baseline, classic, and
|
||||
noninteractive setup persist the supplied workspace through their normal flow.
|
||||
|
||||
@@ -26,8 +38,8 @@ Tailscale (`--tailscale`), reset (`--reset`, `--reset-scope`), flow
|
||||
(`--skip-channels`, `--skip-skills`, `--skip-bootstrap`, `--skip-search`,
|
||||
`--skip-health`, `--skip-ui`, `--skip-hooks`). See [Onboard](/cli/onboard) and
|
||||
[CLI automation](/start/wizard-cli-automation) for the full flag reference and
|
||||
non-interactive examples. `openclaw onboard --modern` is the compatibility
|
||||
alias for the inference-gated Crestodian assistant and has no `setup` equivalent.
|
||||
non-interactive examples. `openclaw onboard --modern` remains a compatibility
|
||||
entry for the same inference-gated OpenClaw assistant.
|
||||
|
||||
<Note>
|
||||
`openclaw setup` is for mutable config installs. In Nix mode (`OPENCLAW_NIX_MODE=1`) OpenClaw refuses setup writes because the config file is managed by Nix. Use the first-party [nix-openclaw Quick Start](https://github.com/openclaw/nix-openclaw#quick-start) or the equivalent source config for another Nix package.
|
||||
@@ -37,9 +49,11 @@ alias for the inference-gated Crestodian assistant and has no `setup` equivalent
|
||||
|
||||
| Flag | Description |
|
||||
| -------------------------- | ----------------------------------------------------------------------------------------------------- |
|
||||
| `-m, --message <text>` | Run one OpenClaw request. |
|
||||
| `--yes` | Approve persistent config writes for one `--message` request. |
|
||||
| `--workspace <dir>` | Workspace proposal in guided mode; persisted directly by baseline, classic, and noninteractive setup. |
|
||||
| `--baseline` | Create baseline config/workspace/session folders without onboarding. |
|
||||
| `--wizard` | Accepted for compatibility; setup runs onboarding by default. |
|
||||
| `--wizard` | Force interactive onboarding. |
|
||||
| `--non-interactive` | Run onboarding without prompts. |
|
||||
| `--accept-risk` | Acknowledge full-system agent access risk; required with `--non-interactive`. |
|
||||
| `--mode <mode>` | Onboarding mode: `local` or `remote`. |
|
||||
@@ -51,7 +65,7 @@ alias for the inference-gated Crestodian assistant and has no `setup` equivalent
|
||||
| `--import-secrets` | Import supported secrets during onboarding migration. |
|
||||
| `--remote-url <url>` | Remote Gateway WebSocket URL. |
|
||||
| `--remote-token <token>` | Remote Gateway token (optional). |
|
||||
| `--json` | Output a JSON summary. |
|
||||
| `--json` | Configured system: OpenClaw overview. Onboarding route: onboarding summary. |
|
||||
|
||||
`--classic` and `--non-interactive` are mutually exclusive: classic opens the
|
||||
prompted wizard, while noninteractive setup uses the automation path.
|
||||
@@ -66,6 +80,10 @@ running onboarding.
|
||||
|
||||
```bash
|
||||
openclaw setup
|
||||
openclaw setup -m "status"
|
||||
openclaw setup -m "restart gateway" --yes
|
||||
openclaw setup --json
|
||||
openclaw setup --wizard
|
||||
openclaw setup --baseline
|
||||
openclaw setup --workspace ~/.openclaw/workspace
|
||||
openclaw setup --import-from hermes --import-source ~/.hermes
|
||||
@@ -74,7 +92,7 @@ openclaw setup --non-interactive --accept-risk --mode remote --remote-url wss://
|
||||
|
||||
## Notes
|
||||
|
||||
- After baseline setup, run `openclaw setup` or `openclaw onboard` for the full guided journey, `openclaw configure` for targeted changes, or `openclaw channels add` to add channel accounts.
|
||||
- After baseline setup, run `openclaw onboard` for the full guided journey, `openclaw configure` for targeted changes, or `openclaw channels add` to add channel accounts.
|
||||
- If Hermes state is detected, interactive onboarding can offer migration automatically. Import onboarding requires a fresh setup; use [Migrate](/cli/migrate) for dry-run plans, backups, and overwrite mode outside onboarding.
|
||||
|
||||
## Related
|
||||
|
||||
+5
-1
@@ -52,6 +52,10 @@
|
||||
]
|
||||
},
|
||||
"redirects": [
|
||||
{
|
||||
"source": "/cli/crestodian",
|
||||
"destination": "/cli/openclaw"
|
||||
},
|
||||
{
|
||||
"source": "/channels/bluebubbles",
|
||||
"destination": "/channels/imessage-from-bluebubbles"
|
||||
@@ -1751,7 +1755,7 @@
|
||||
"group": "Gateway and service",
|
||||
"pages": [
|
||||
"cli/backup",
|
||||
"cli/crestodian",
|
||||
"cli/openclaw",
|
||||
"cli/daemon",
|
||||
"cli/doctor",
|
||||
"cli/fleet",
|
||||
|
||||
+19
-14
@@ -1383,19 +1383,7 @@ Do not edit it by hand; run `pnpm docs:map:gen`.
|
||||
## cli/crestodian.md
|
||||
|
||||
- Route: /cli/crestodian
|
||||
- Headings:
|
||||
- H1: openclaw crestodian
|
||||
- H2: When it starts
|
||||
- H2: What Crestodian shows
|
||||
- H2: Examples
|
||||
- H2: Operations and approval
|
||||
- H3: Switching to masked channel setup
|
||||
- H2: Setup bootstrap
|
||||
- H2: AI conversation
|
||||
- H3: CLI harness trust model
|
||||
- H2: Switching to an agent
|
||||
- H2: Message rescue mode
|
||||
- H2: Related
|
||||
- Headings: none
|
||||
|
||||
## cli/cron.md
|
||||
|
||||
@@ -1794,6 +1782,23 @@ Do not edit it by hand; run `pnpm docs:map:gen`.
|
||||
- H2: Other behaviors
|
||||
- H2: Common follow-up commands
|
||||
|
||||
## cli/openclaw.md
|
||||
|
||||
- Route: /cli/openclaw
|
||||
- Headings:
|
||||
- H1: openclaw setup
|
||||
- H2: When it starts
|
||||
- H2: What OpenClaw shows
|
||||
- H2: Examples
|
||||
- H2: Operations and approval
|
||||
- H3: Switching to masked channel setup
|
||||
- H2: Setup bootstrap
|
||||
- H2: AI conversation
|
||||
- H3: CLI harness trust model
|
||||
- H2: Switching to an agent
|
||||
- H2: Message rescue mode
|
||||
- H2: Related
|
||||
|
||||
## cli/pairing.md
|
||||
|
||||
- Route: /cli/pairing
|
||||
@@ -10436,7 +10441,7 @@ Do not edit it by hand; run `pnpm docs:map:gen`.
|
||||
- H2: Keyboard shortcuts
|
||||
- H2: Slash commands
|
||||
- H2: Local shell commands
|
||||
- H2: Crestodian setup and repair helper
|
||||
- H2: OpenClaw setup and repair helper
|
||||
- H2: Tool output
|
||||
- H2: Terminal colors
|
||||
- H2: History + streaming
|
||||
|
||||
@@ -131,21 +131,21 @@ When debugging real providers/models (requires real creds):
|
||||
through `npm-pack:`, verifies the dependency under the managed npm
|
||||
project root, then asks a live OpenAI model to call the plugin tool and
|
||||
return the hidden slug.
|
||||
- Crestodian rescue command smoke: `pnpm test:live:crestodian-rescue-channel`
|
||||
- OpenClaw rescue command smoke: `pnpm test:live:system-agent-rescue-channel`
|
||||
- Opt-in belt-and-suspenders check for the message-channel rescue command
|
||||
surface. Exercises `/crestodian status`, queues a persistent model
|
||||
change, replies `/crestodian yes`, and verifies the audit/config write
|
||||
surface. Exercises `/openclaw status`, queues a persistent model
|
||||
change, replies `/openclaw yes`, and verifies the audit/config write
|
||||
path.
|
||||
- Crestodian first-run Docker smoke: `pnpm test:docker:crestodian-first-run`
|
||||
- OpenClaw first-run Docker smoke: `pnpm test:docker:system-agent-first-run`
|
||||
- Starts from an empty OpenClaw state dir and first proves the packaged
|
||||
`openclaw crestodian` CLI fails closed without inference. It then
|
||||
`openclaw setup` CLI fails closed without inference. It then
|
||||
tests and activates fake Claude through the packaged activation module.
|
||||
Only afterward does a fuzzy packaged CLI request reach the planner and
|
||||
resolve to typed setup, followed by one-shot model, agent, Discord plugin,
|
||||
and SecretRef operations. It validates config and audit entries. This is
|
||||
supporting gate/operation evidence, not an interactive onboarding or
|
||||
Crestodian agent/tool/approval proof. The same lane is exposed in QA Lab by
|
||||
`pnpm openclaw qa suite --scenario crestodian-ring-zero-setup`.
|
||||
OpenClaw agent/tool/approval proof. The same lane is exposed in QA Lab by
|
||||
`pnpm openclaw qa suite --scenario system-agent-ring-zero-setup`.
|
||||
- Moonshot/Kimi cost smoke: with `MOONSHOT_API_KEY` set, run
|
||||
`openclaw models list --provider moonshot --json`, then run an isolated
|
||||
`openclaw agent --local --session-id live-kimi-cost --message 'Reply exactly: KIMI_LIVE_OK' --thinking off --json`
|
||||
|
||||
@@ -31,13 +31,13 @@ has no macOS app asset, use the newest one that does, or build from source with
|
||||
2. Pick **This Mac** for a local Gateway, or connect to a remote Gateway.
|
||||
3. Wait while the app installs the matching CLI runtime. In local mode it also
|
||||
installs and starts the Gateway.
|
||||
4. Establish inference with a live model check. After it passes, Crestodian
|
||||
4. Establish inference with a live model check. After it passes, OpenClaw
|
||||
handles the remaining setup.
|
||||
5. Complete the macOS permission checklist and send the onboarding test message.
|
||||
|
||||
If the app reaches an existing Gateway whose default agent has a configured
|
||||
model, it treats that Gateway as already set up, skips provider onboarding and
|
||||
Crestodian, and opens the dashboard. If the Gateway cannot connect or its
|
||||
OpenClaw, and opens the dashboard. If the Gateway cannot connect or its
|
||||
default agent has no model, inference onboarding remains available for
|
||||
recovery.
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ implementation that completed the probe. When
|
||||
matching `runtimeArtifact.validate(...)` capability that rechecks that binding
|
||||
without loading a different harness or scanning unrelated plugins.
|
||||
|
||||
Verified Crestodian continuations also pass `params.expectedRuntimeArtifact`.
|
||||
Verified OpenClaw continuations also pass `params.expectedRuntimeArtifact`.
|
||||
The harness must compare it with the exact native process it acquired and fail
|
||||
before starting or resuming a native thread if they differ. Ordinary agent
|
||||
turns omit both fields, so content hashing stays out of the normal request hot
|
||||
|
||||
@@ -894,7 +894,7 @@ sessionId}` and session key context.
|
||||
- Active-memory blocking subagent runs use SQLite transcript rows instead of
|
||||
creating temporary or persisted `session.jsonl` files under plugin state. The
|
||||
old `transcriptDir` option is removed.
|
||||
- One-off slug generation and Crestodian planner runs use SQLite transcript rows
|
||||
- One-off slug generation and system-agent planner runs use SQLite transcript rows
|
||||
instead of creating temporary `session.jsonl` files.
|
||||
- `llm-task` helper runs and hidden commitment extraction also use SQLite
|
||||
transcript rows, so these model-only helper sessions no longer create
|
||||
@@ -1030,7 +1030,7 @@ sessionId})`; create, branch, continue, list, and fork flows live in their
|
||||
- Memory Wiki no longer creates `.openclaw-wiki/state.json` or the unused
|
||||
`.openclaw-wiki/locks` directory. The migration provider removes those retired
|
||||
plugin metadata files if an older vault still has them.
|
||||
- Crestodian audit entries now use core SQLite plugin state instead of
|
||||
- System-agent audit entries now use core SQLite plugin state instead of
|
||||
`audit/crestodian.jsonl`. Doctor imports the legacy JSONL audit log and
|
||||
removes it after successful import.
|
||||
- Config write/observe audit entries now use core SQLite plugin state instead
|
||||
@@ -1040,7 +1040,7 @@ sessionId})`; create, branch, continue, list, and fork flows live in their
|
||||
`logs/config-health.json` sidecars while editing `openclaw.json`. The config
|
||||
file remains file-backed, recovery snapshots stay next to the config file,
|
||||
and durable config audit/health state belongs to the Gateway SQLite store.
|
||||
- Crestodian rescue pending approvals now use core SQLite plugin state instead
|
||||
- System-agent rescue pending approvals now use core SQLite plugin state instead
|
||||
of `crestodian/rescue-pending/*.json`. Doctor imports legacy pending approval
|
||||
files and removes them after successful import.
|
||||
- Phone Control temporary arm state now uses SQLite plugin state instead of
|
||||
|
||||
@@ -9,20 +9,20 @@ sidebarTitle: "Onboarding Overview"
|
||||
|
||||
OpenClaw has terminal and macOS app onboarding. Both establish inference first:
|
||||
they detect existing AI access, require a live completion, and only then start
|
||||
Crestodian to configure the remaining setup. A reachable, configured Gateway
|
||||
OpenClaw to configure the remaining setup. A reachable, configured Gateway
|
||||
whose default agent already has a configured model skips onboarding and opens
|
||||
the normal agent UI. The terminal flow also offers the full classic wizard for
|
||||
detailed setup.
|
||||
|
||||
## Which path should I use?
|
||||
|
||||
| | CLI onboarding | macOS app onboarding |
|
||||
| -------------- | -------------------------------------- | -------------------------------- |
|
||||
| **Platforms** | macOS, Linux, Windows (native or WSL2) | macOS only |
|
||||
| **Interface** | Inference setup, then Crestodian | Inference setup, then Crestodian |
|
||||
| **Best for** | Servers, headless, full control | Desktop Mac, visual setup |
|
||||
| **Automation** | `--non-interactive` for scripts | Manual only |
|
||||
| **Command** | `openclaw onboard` | Launch the app |
|
||||
| | CLI onboarding | macOS app onboarding |
|
||||
| -------------- | -------------------------------------- | ------------------------------ |
|
||||
| **Platforms** | macOS, Linux, Windows (native or WSL2) | macOS only |
|
||||
| **Interface** | Inference setup, then OpenClaw | Inference setup, then OpenClaw |
|
||||
| **Best for** | Servers, headless, full control | Desktop Mac, visual setup |
|
||||
| **Automation** | `--non-interactive` for scripts | Manual only |
|
||||
| **Command** | `openclaw onboard` | Launch the app |
|
||||
|
||||
Most users should start with **CLI onboarding** — it works everywhere and gives
|
||||
you the most control.
|
||||
@@ -36,7 +36,7 @@ The guided inference phase establishes only:
|
||||
2. **Verified inference** — a real completion on the default agent's effective
|
||||
model
|
||||
|
||||
After that completion passes, Crestodian can configure the workspace, Gateway,
|
||||
After that completion passes, OpenClaw can configure the workspace, Gateway,
|
||||
Gateway service, channels, agents, plugins, and other optional features.
|
||||
|
||||
The classic CLI wizard can additionally configure:
|
||||
@@ -59,20 +59,20 @@ and falls through on failure. If detection is exhausted, it shows OpenAI,
|
||||
Anthropic, xAI (Grok), Google, and OpenRouter first. **More…** contains the
|
||||
remaining providers in provider groups, with regions, plans, and supported
|
||||
browser, device, API-key, or token methods in a second menu. It saves the model
|
||||
and credential only after a passing completion, then starts Crestodian to
|
||||
and credential only after a passing completion, then starts OpenClaw to
|
||||
configure the workspace, Gateway, channels, agents, plugins, and other optional
|
||||
features. **Skip for now** exits without starting Crestodian. There is no
|
||||
features. **Skip for now** exits without starting OpenClaw. There is no
|
||||
in-flow classic handoff; exit and run `openclaw onboard --classic` when you want
|
||||
the classic wizard instead.
|
||||
|
||||
After inference passes, Crestodian can hand channel setup to a masked terminal
|
||||
wizard. It does not open guided or classic provider setup; exit Crestodian and
|
||||
After inference passes, OpenClaw can hand channel setup to a masked terminal
|
||||
wizard. It does not open guided or classic provider setup; exit OpenClaw and
|
||||
run `openclaw onboard` to change the model provider or its authentication.
|
||||
|
||||
Use `openclaw onboard --classic` for detailed model/auth, channel, skill,
|
||||
remote Gateway, or import setup. Adding `--install-daemon` also selects the
|
||||
classic flow and installs the background service in one step. Use `openclaw
|
||||
crestodian` for conversational non-inference setup and repair. `openclaw
|
||||
openclaw` for conversational non-inference setup and repair. `openclaw
|
||||
onboard --modern` is a compatibility alias that uses the same live-inference
|
||||
gate.
|
||||
|
||||
@@ -83,13 +83,13 @@ CLI command docs: [`openclaw onboard`](/cli/onboard)
|
||||
|
||||
Open the OpenClaw app. If its configured local or remote Gateway is reachable
|
||||
and the default agent already has a configured model, the app skips onboarding
|
||||
and Crestodian and opens the normal agent UI immediately.
|
||||
and OpenClaw and opens the normal agent UI immediately.
|
||||
|
||||
For a fresh or incomplete Gateway, the first-run flow detects existing AI
|
||||
access (Claude Code, Codex, or API keys), live-tests the best
|
||||
option, and saves it only after a real reply — falling back automatically and
|
||||
offering a verified manual API-key step when nothing is found. Sensitive
|
||||
credentials use masked input. Once inference passes, Crestodian starts and
|
||||
credentials use masked input. Once inference passes, OpenClaw starts and
|
||||
helps configure the rest.
|
||||
|
||||
Gemini CLI remains available for normal agents after setup, but it is not
|
||||
|
||||
@@ -67,7 +67,7 @@ Where does the **Gateway** run?
|
||||
</Step>
|
||||
<Step title="Connect your AI">
|
||||
A connected Gateway that already has a configured agent model skips this
|
||||
page entirely and opens the normal agent UI. Crestodian and provider setup
|
||||
page entirely and opens the normal agent UI. OpenClaw and provider setup
|
||||
only run for a fresh or incomplete Gateway.
|
||||
|
||||
Once the Gateway is ready, onboarding looks for AI access you already have:
|
||||
@@ -90,9 +90,9 @@ The manual key/token picker uses the same provider registry. In every route,
|
||||
the provider supplies its starter model and configuration; OpenClaw verifies
|
||||
the credential with the same live test before storing its auth profile. Next
|
||||
remains locked until one backend has passed, so the first agent chat cannot
|
||||
start without working inference. After that live check passes, Crestodian becomes
|
||||
start without working inference. After that live check passes, OpenClaw becomes
|
||||
available to help configure the remaining workspace, Gateway, channels, and
|
||||
other optional features; it is also available later under Settings → Crestodian.
|
||||
other optional features; it is also available later under Settings → OpenClaw.
|
||||
</Step>
|
||||
<Step title="Permissions">
|
||||
|
||||
@@ -104,10 +104,10 @@ Onboarding requests TCC permissions for: Automation (AppleScript), Notifications
|
||||
|
||||
</Step>
|
||||
<Step title="Finish">
|
||||
After inference passes, Crestodian owns the remaining optional setup and can
|
||||
After inference passes, OpenClaw owns the remaining optional setup and can
|
||||
hand you off to the normal agent chat. Finishing the permission walkthrough
|
||||
opens that same chat; the app does not create a workspace or launch a separate
|
||||
agent setup conversation before Crestodian. See
|
||||
agent setup conversation before OpenClaw. See
|
||||
[Bootstrapping](/start/bootstrapping) for what happens on the gateway host
|
||||
during the agent's first real turn.
|
||||
</Step>
|
||||
|
||||
+12
-12
@@ -1,5 +1,5 @@
|
||||
---
|
||||
summary: "CLI onboarding: verify inference, then hand remaining setup to Crestodian"
|
||||
summary: "CLI onboarding: verify inference, then hand remaining setup to OpenClaw"
|
||||
read_when:
|
||||
- Running or configuring CLI onboarding
|
||||
- Setting up a new machine
|
||||
@@ -13,23 +13,23 @@ openclaw onboard
|
||||
|
||||
CLI onboarding is the recommended terminal setup path on macOS, Linux, and
|
||||
Windows (native or WSL2). By default it detects AI access already available on
|
||||
the machine, verifies it with a real completion, and starts Crestodian to
|
||||
the machine, verifies it with a real completion, and starts OpenClaw to
|
||||
configure the workspace, Gateway, and optional features. `openclaw setup` runs the same flow ([Setup](/cli/setup) covers
|
||||
the `--baseline` config-only variant). Windows desktop users can also start
|
||||
from [Windows Hub](/platforms/windows).
|
||||
|
||||
Guided onboarding establishes inference first. It detects available AI access,
|
||||
requires a real completion, and only then starts [Crestodian](/cli/crestodian)
|
||||
requires a real completion, and only then starts [OpenClaw](/cli/openclaw)
|
||||
to configure the rest of OpenClaw. Choosing **Skip for now** exits onboarding
|
||||
without starting Crestodian.
|
||||
without starting OpenClaw.
|
||||
|
||||
The classic wizard remains available for custom providers, remote Gateway
|
||||
setup, channel pairing, daemon controls, skills, and imports. Run it explicitly
|
||||
with `openclaw onboard --classic`; the guided inference picker does not delegate
|
||||
into it. After inference passes, Crestodian can use `open channel wizard for
|
||||
into it. After inference passes, OpenClaw can use `open channel wizard for
|
||||
<channel>` to hand channel setup that needs secrets to a masked terminal wizard.
|
||||
To change the model provider or its authentication, exit Crestodian and run
|
||||
`openclaw onboard`; Crestodian does not open guided or classic provider flows.
|
||||
To change the model provider or its authentication, exit OpenClaw and run
|
||||
`openclaw onboard`; OpenClaw does not open guided or classic provider flows.
|
||||
|
||||
<Info>
|
||||
Fastest first chat: finish guided setup, run `openclaw dashboard`, and chat in
|
||||
@@ -81,10 +81,10 @@ Plain `openclaw onboard` follows this path:
|
||||
OpenRouter, or choose **More…** for the remaining providers. Each provider's
|
||||
regions, plans, and supported browser, device, API-key, or token methods
|
||||
appear in a second menu and are tested with the same real completion.
|
||||
Choose **Skip for now** to exit without starting Crestodian.
|
||||
Choose **Skip for now** to exit without starting OpenClaw.
|
||||
5. Persist only the verified model route and any credential/plugin state it
|
||||
requires. Workspace and Gateway settings remain untouched.
|
||||
6. Start Crestodian with the verified model so it can configure the workspace,
|
||||
6. Start OpenClaw with the verified model so it can configure the workspace,
|
||||
Gateway, channels, agents, plugins, and the remaining optional setup.
|
||||
|
||||
Re-running the command on a configured installation tests the current default
|
||||
@@ -146,7 +146,7 @@ Local mode (default) walks through these steps:
|
||||
`exec`), with a fast preflight check before saving. After model/auth setup,
|
||||
the wizard offers an optional live completion test; a failure can return to
|
||||
model/auth setup once or be ignored without blocking the rest of the
|
||||
classic wizard. Ignoring it does not unlock Crestodian; conversational setup
|
||||
classic wizard. Ignoring it does not unlock OpenClaw; conversational setup
|
||||
still requires a passing inference check.
|
||||
2. **Workspace** - directory for agent files (default `~/.openclaw/workspace`). Seeds bootstrap files.
|
||||
3. **Gateway** - port, bind address, auth mode, Tailscale exposure. In
|
||||
@@ -178,8 +178,8 @@ config is invalid or contains legacy keys, onboarding asks you to run
|
||||
`--flow import` runs a detected migration flow (for example Hermes) in the
|
||||
classic wizard instead of fresh setup; see [Migrate](/cli/migrate) and the migration guides under
|
||||
[Install](/install/migrating-hermes). `openclaw onboard --modern` is a
|
||||
compatibility alias for [Crestodian](/cli/crestodian). It uses the same
|
||||
inference gate as `openclaw crestodian`: verified inference starts the
|
||||
compatibility alias for [OpenClaw](/cli/openclaw). It uses the same
|
||||
inference gate as `openclaw setup`: verified inference starts the
|
||||
assistant, while an interactive failure returns to guided inference setup.
|
||||
|
||||
## Add another agent
|
||||
|
||||
@@ -240,7 +240,7 @@ plugins.
|
||||
| `/status plugins` | Show detailed plugin health: load errors, quarantines, channel plugin failures, dependency issues, compatibility notices. Requires `commands.plugins: true` |
|
||||
| `/goal [status\|start\|edit\|pause\|resume\|complete\|block\|clear] ...` | Manage the current session's durable [goal](/tools/goal) |
|
||||
| `/diagnostics [note]` | Owner-only support-report flow. Asks for exec approval every time |
|
||||
| `/crestodian <request>` | Run the Crestodian setup and repair helper from an owner DM |
|
||||
| `/openclaw <request>` | Run the OpenClaw setup and repair helper from an owner DM |
|
||||
| `/tasks` | List active/recent background tasks for the current session |
|
||||
| `/context [list\|detail\|map\|json]` | Explain how context is assembled |
|
||||
| `/whoami` | Show your sender id. Alias: `/id` |
|
||||
|
||||
+8
-8
@@ -138,9 +138,9 @@ Local mode only:
|
||||
|
||||
- `/auth [provider]` opens the provider auth/login flow inside the TUI.
|
||||
|
||||
Crestodian:
|
||||
OpenClaw:
|
||||
|
||||
- `/crestodian [request]` returns from the normal agent TUI to the [Crestodian](#crestodian-setup-and-repair-helper) setup/repair chat, optionally forwarding one request.
|
||||
- `/openclaw [request]` returns from the normal agent TUI to the [OpenClaw](#openclaw-setup-and-repair-helper) setup/repair chat, optionally forwarding one request.
|
||||
|
||||
Other Gateway slash commands (for example, `/context`) are forwarded to the Gateway and shown as system output. See [Slash commands](/tools/slash-commands).
|
||||
|
||||
@@ -152,19 +152,19 @@ Other Gateway slash commands (for example, `/context`) are forwarded to the Gate
|
||||
- Local shell commands receive `OPENCLAW_SHELL=tui-local` in their environment.
|
||||
- A lone `!` is sent as a normal message; leading spaces do not trigger local exec.
|
||||
|
||||
## Crestodian setup and repair helper
|
||||
## OpenClaw setup and repair helper
|
||||
|
||||
Crestodian is the ring-zero setup/repair assistant, exposed as `openclaw crestodian` after the configured default model passes a live inference check. If inference is unavailable, an interactive invocation returns to inference onboarding and automation fails with repair guidance. It runs inside the same local TUI shell as `openclaw tui --local`, backed by an AI agent restricted to Crestodian's typed, approval-gated operations:
|
||||
OpenClaw is the ring-zero setup/repair assistant, exposed as `openclaw setup` after the configured default model passes a live inference check. If inference is unavailable, an interactive invocation returns to inference onboarding and automation fails with repair guidance. It runs inside the same local TUI shell as `openclaw tui --local`, backed by an AI agent restricted to OpenClaw's typed, approval-gated operations:
|
||||
|
||||
```bash
|
||||
openclaw crestodian # start interactively
|
||||
openclaw crestodian -m "status" # run one request and exit
|
||||
openclaw crestodian -m "set default model openai/gpt-5.2" --yes # apply a config write
|
||||
openclaw setup # start interactively
|
||||
openclaw setup -m "status" # run one request and exit
|
||||
openclaw setup -m "set default model openai/gpt-5.2" --yes # apply a config write
|
||||
```
|
||||
|
||||
- Persistent config writes need approval: either confirm interactively or pass `--yes`.
|
||||
- `--json` prints the startup overview as JSON instead of starting the chat.
|
||||
- From inside Crestodian, an `open-tui` request (for example, asking to talk to a normal agent) exits Crestodian and opens the regular agent TUI; use `/crestodian` there to come back.
|
||||
- From inside OpenClaw, an `open-tui` request (for example, asking to talk to a normal agent) exits OpenClaw and opens the regular agent TUI; use `/openclaw` there to come back.
|
||||
|
||||
Use local mode when the current config already validates and you want the embedded agent to inspect it on the same machine, compare it against the docs, and help repair drift without depending on a running Gateway.
|
||||
|
||||
|
||||
@@ -114,7 +114,7 @@ describe("Claude CLI model aliases", () => {
|
||||
});
|
||||
|
||||
describe("resolveClaudeCliExecutionArgs", () => {
|
||||
it("isolates Crestodian from Claude user customizations while preserving exact MCP", () => {
|
||||
it("isolates OpenClaw from Claude user customizations while preserving exact MCP", () => {
|
||||
expect(
|
||||
resolveClaudeCliExecutionArgs({
|
||||
workspaceDir: "/tmp",
|
||||
@@ -156,7 +156,7 @@ describe("resolveClaudeCliExecutionArgs", () => {
|
||||
"--ide",
|
||||
"--strict-mcp-config",
|
||||
"--mcp-config",
|
||||
"/tmp/openclaw-crestodian-mcp.json",
|
||||
"/tmp/openclaw-openclaw-mcp.json",
|
||||
"--resume",
|
||||
"native-session",
|
||||
"--tools",
|
||||
@@ -168,7 +168,7 @@ describe("resolveClaudeCliExecutionArgs", () => {
|
||||
],
|
||||
toolAvailability: {
|
||||
native: [],
|
||||
mcp: ["mcp__openclaw__crestodian"],
|
||||
mcp: ["mcp__openclaw__openclaw"],
|
||||
},
|
||||
}),
|
||||
).toEqual([
|
||||
@@ -176,7 +176,7 @@ describe("resolveClaudeCliExecutionArgs", () => {
|
||||
"--output-format",
|
||||
"stream-json",
|
||||
"--mcp-config",
|
||||
"/tmp/openclaw-crestodian-mcp.json",
|
||||
"/tmp/openclaw-openclaw-mcp.json",
|
||||
"--resume",
|
||||
"native-session",
|
||||
"--setting-sources",
|
||||
@@ -189,11 +189,11 @@ describe("resolveClaudeCliExecutionArgs", () => {
|
||||
"--tools",
|
||||
"",
|
||||
"--allowedTools",
|
||||
"mcp__openclaw__crestodian",
|
||||
"mcp__openclaw__openclaw",
|
||||
]);
|
||||
});
|
||||
|
||||
it("leaves non-Crestodian customization args intact under generic tool availability", () => {
|
||||
it("leaves non-OpenClaw customization args intact under generic tool availability", () => {
|
||||
expect(
|
||||
resolveClaudeCliExecutionArgs({
|
||||
workspaceDir: "/tmp",
|
||||
|
||||
@@ -93,8 +93,8 @@ const CLAUDE_BYPASS_PERMISSION_MODE = "bypassPermissions";
|
||||
const CLAUDE_DEFAULT_PERMISSION_MODE = "default";
|
||||
const CLAUDE_NO_TOOLS_VALUE = "";
|
||||
const CLAUDE_DENY_MCP_TOOLS_VALUE = "mcp__*";
|
||||
const CLAUDE_CRESTODIAN_MCP_TOOL = "mcp__openclaw__crestodian";
|
||||
const CLAUDE_CRESTODIAN_SETTINGS =
|
||||
const CLAUDE_SYSTEM_AGENT_MCP_TOOL = "mcp__openclaw__openclaw";
|
||||
const CLAUDE_SYSTEM_AGENT_SETTINGS =
|
||||
'{"disableAllHooks":true,"enabledPlugins":{},"autoMemoryEnabled":false,"claudeMdExcludes":["**/CLAUDE.md","**/CLAUDE.local.md","**/.claude/rules/**"]}';
|
||||
|
||||
type ClaudeCliEffort = "low" | "medium" | "high" | "xhigh" | "max";
|
||||
@@ -304,13 +304,13 @@ const CLAUDE_TOOL_AVAILABILITY_ARGS = new Set([
|
||||
"--disallowed-tools",
|
||||
]);
|
||||
|
||||
const CLAUDE_CRESTODIAN_VARIADIC_VALUE_ARGS = new Set([
|
||||
const CLAUDE_SYSTEM_AGENT_VARIADIC_VALUE_ARGS = new Set([
|
||||
...CLAUDE_TOOL_AVAILABILITY_ARGS,
|
||||
"--add-dir",
|
||||
"--file",
|
||||
]);
|
||||
|
||||
const CLAUDE_CRESTODIAN_VALUE_ARGS = new Set([
|
||||
const CLAUDE_SYSTEM_AGENT_VALUE_ARGS = new Set([
|
||||
CLAUDE_PERMISSION_MODE_ARG,
|
||||
CLAUDE_SETTING_SOURCES_ARG,
|
||||
CLAUDE_SETTINGS_ARG,
|
||||
@@ -326,7 +326,7 @@ const CLAUDE_CRESTODIAN_VALUE_ARGS = new Set([
|
||||
"--append-system-prompt-file",
|
||||
]);
|
||||
|
||||
const CLAUDE_CRESTODIAN_BARE_ARGS = new Set([
|
||||
const CLAUDE_SYSTEM_AGENT_BARE_ARGS = new Set([
|
||||
CLAUDE_BARE_ARG,
|
||||
CLAUDE_SAFE_MODE_ARG,
|
||||
CLAUDE_DISABLE_SLASH_COMMANDS_ARG,
|
||||
@@ -436,17 +436,17 @@ function resolveClaudeCliToolAvailabilityArgs(
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function isCrestodianToolAvailability(
|
||||
function isSystemAgentToolAvailability(
|
||||
availability: NonNullable<CliBackendResolveExecutionArgsContext["toolAvailability"]>,
|
||||
): boolean {
|
||||
return availability.mcp.length === 1 && availability.mcp[0] === CLAUDE_CRESTODIAN_MCP_TOOL;
|
||||
return availability.mcp.length === 1 && availability.mcp[0] === CLAUDE_SYSTEM_AGENT_MCP_TOOL;
|
||||
}
|
||||
|
||||
function resolveClaudeCliCrestodianExecutionArgs(baseArgs: readonly string[]): string[] {
|
||||
function resolveClaudeCliSystemAgentExecutionArgs(baseArgs: readonly string[]): string[] {
|
||||
const normalized = stripClaudeArgs(baseArgs, {
|
||||
bare: CLAUDE_CRESTODIAN_BARE_ARGS,
|
||||
variadicValue: CLAUDE_CRESTODIAN_VARIADIC_VALUE_ARGS,
|
||||
value: CLAUDE_CRESTODIAN_VALUE_ARGS,
|
||||
bare: CLAUDE_SYSTEM_AGENT_BARE_ARGS,
|
||||
variadicValue: CLAUDE_SYSTEM_AGENT_VARIADIC_VALUE_ARGS,
|
||||
value: CLAUDE_SYSTEM_AGENT_VALUE_ARGS,
|
||||
});
|
||||
// Safe mode also suppresses explicit MCP, while bare mode drops OAuth. Empty
|
||||
// setting sources plus restrictive flag settings isolate user customizations;
|
||||
@@ -455,14 +455,14 @@ function resolveClaudeCliCrestodianExecutionArgs(baseArgs: readonly string[]): s
|
||||
CLAUDE_SETTING_SOURCES_ARG,
|
||||
"",
|
||||
CLAUDE_SETTINGS_ARG,
|
||||
CLAUDE_CRESTODIAN_SETTINGS,
|
||||
CLAUDE_SYSTEM_AGENT_SETTINGS,
|
||||
CLAUDE_DISABLE_SLASH_COMMANDS_ARG,
|
||||
CLAUDE_NO_CHROME_ARG,
|
||||
CLAUDE_STRICT_MCP_CONFIG_ARG,
|
||||
CLAUDE_TOOLS_ARG,
|
||||
CLAUDE_NO_TOOLS_VALUE,
|
||||
CLAUDE_ALLOWED_TOOLS_ARG,
|
||||
CLAUDE_CRESTODIAN_MCP_TOOL,
|
||||
CLAUDE_SYSTEM_AGENT_MCP_TOOL,
|
||||
);
|
||||
return normalized;
|
||||
}
|
||||
@@ -490,8 +490,8 @@ export function resolveClaudeCliExecutionArgs(
|
||||
if (!context.toolAvailability) {
|
||||
return executionArgs;
|
||||
}
|
||||
return isCrestodianToolAvailability(context.toolAvailability)
|
||||
? resolveClaudeCliCrestodianExecutionArgs(executionArgs)
|
||||
return isSystemAgentToolAvailability(context.toolAvailability)
|
||||
? resolveClaudeCliSystemAgentExecutionArgs(executionArgs)
|
||||
: resolveClaudeCliToolAvailabilityArgs(executionArgs, context.toolAvailability);
|
||||
}
|
||||
|
||||
|
||||
@@ -266,30 +266,30 @@ describe("Codex app-server dynamic tool build", () => {
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves the host-provided Crestodian tool through the Codex allowlist", async () => {
|
||||
it("preserves the host-provided OpenClaw tool through the Codex allowlist", async () => {
|
||||
const workspaceDir = path.join(tempDir, "workspace");
|
||||
const params = createParams(path.join(tempDir, "session.jsonl"), workspaceDir);
|
||||
params.disableTools = false;
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
params.toolsAllow = ["crestodian"];
|
||||
params.toolsAllow = ["openclaw"];
|
||||
setOpenClawCodingToolsFactoryForTests(() => [
|
||||
{ ...createRuntimeDynamicTool("crestodian"), catalogMode: "direct-only" },
|
||||
{ ...createRuntimeDynamicTool("openclaw"), catalogMode: "direct-only" },
|
||||
]);
|
||||
|
||||
const tools = await buildDynamicToolsForTest(params, workspaceDir, {
|
||||
isHostScopedToolActive: (toolName) => toolName === "crestodian",
|
||||
pluginConfig: { codexDynamicToolsExclude: ["crestodian"] },
|
||||
isHostScopedToolActive: (toolName) => toolName === "openclaw",
|
||||
pluginConfig: { codexDynamicToolsExclude: ["openclaw"] },
|
||||
});
|
||||
|
||||
expect(tools.map((tool) => tool.name)).toEqual(["crestodian"]);
|
||||
expect(tools.map((tool) => tool.name)).toEqual(["openclaw"]);
|
||||
});
|
||||
|
||||
it.each([
|
||||
{ label: "host scope is inactive", hostActive: false, toolsAllow: ["crestodian"] },
|
||||
{ label: "host scope is inactive", hostActive: false, toolsAllow: ["openclaw"] },
|
||||
{
|
||||
label: "the public allowlist is not exact",
|
||||
hostActive: true,
|
||||
toolsAllow: ["crestodian", "read"],
|
||||
toolsAllow: ["openclaw", "read"],
|
||||
},
|
||||
])("does not bypass Codex excludes when $label", async ({ hostActive, toolsAllow }) => {
|
||||
const workspaceDir = path.join(tempDir, "workspace");
|
||||
@@ -298,12 +298,12 @@ describe("Codex app-server dynamic tool build", () => {
|
||||
params.runtimePlan = createCodexRuntimePlanFixture();
|
||||
params.toolsAllow = toolsAllow;
|
||||
setOpenClawCodingToolsFactoryForTests(() => [
|
||||
{ ...createRuntimeDynamicTool("crestodian"), catalogMode: "direct-only" },
|
||||
{ ...createRuntimeDynamicTool("openclaw"), catalogMode: "direct-only" },
|
||||
]);
|
||||
|
||||
const tools = await buildDynamicToolsForTest(params, workspaceDir, {
|
||||
isHostScopedToolActive: () => hostActive,
|
||||
pluginConfig: { codexDynamicToolsExclude: ["crestodian"] },
|
||||
pluginConfig: { codexDynamicToolsExclude: ["openclaw"] },
|
||||
});
|
||||
|
||||
expect(tools).toEqual([]);
|
||||
|
||||
@@ -24,7 +24,7 @@ import { readCodexPluginConfig, type CodexPluginConfig } from "./config.js";
|
||||
import { dynamicToolBuildState } from "./dynamic-tool-build-state.js";
|
||||
import {
|
||||
filterCodexDynamicTools,
|
||||
isCrestodianOnlyCodexDynamicToolAllowlist,
|
||||
isSystemAgentOnlyCodexDynamicToolAllowlist,
|
||||
isForcedPrivateQaCodexRuntime,
|
||||
normalizeCodexDynamicToolName,
|
||||
} from "./dynamic-tool-profile.js";
|
||||
@@ -63,17 +63,17 @@ const CODEX_MEMORY_FLUSH_DYNAMIC_TOOL_ALLOW = new Set(["read", "write"]);
|
||||
const CODEX_NODE_EXEC_DYNAMIC_TOOL_NAME = "node_exec";
|
||||
const CODEX_NODE_PROCESS_DYNAMIC_TOOL_NAME = "node_process";
|
||||
const CODEX_NODE_EXEC_POLICY_PARAMETER_NAMES = new Set(["host", "security", "ask"]);
|
||||
function preserveRingZeroCrestodianTool<T extends { name: string; catalogMode?: string }>(
|
||||
function preserveRingZeroSystemAgentTool<T extends { name: string; catalogMode?: string }>(
|
||||
allTools: T[],
|
||||
filteredTools: T[],
|
||||
): T[] {
|
||||
const crestodian = allTools.find(
|
||||
(tool) => tool.name === "crestodian" && tool.catalogMode === "direct-only",
|
||||
const openclaw = allTools.find(
|
||||
(tool) => tool.name === "openclaw" && tool.catalogMode === "direct-only",
|
||||
);
|
||||
if (!crestodian) {
|
||||
if (!openclaw) {
|
||||
return filteredTools;
|
||||
}
|
||||
return [crestodian, ...filteredTools.filter((tool) => tool.name !== "crestodian")];
|
||||
return [openclaw, ...filteredTools.filter((tool) => tool.name !== "openclaw")];
|
||||
}
|
||||
/** Runtime inputs needed to derive the exact Codex dynamic tool surface for a turn. */
|
||||
type DynamicToolBuildParams = {
|
||||
@@ -339,11 +339,11 @@ export async function buildDynamicTools(input: DynamicToolBuildParams) {
|
||||
});
|
||||
const readableAllTools = [...readableAllToolProjection.tools];
|
||||
const normallyProfiledTools = filterCodexDynamicTools(readableAllTools, input.pluginConfig);
|
||||
const hostCrestodianActive =
|
||||
input.isHostScopedToolActive?.("crestodian") ?? isHostScopedAgentToolActive("crestodian");
|
||||
const hostSystemAgentActive =
|
||||
input.isHostScopedToolActive?.("openclaw") ?? isHostScopedAgentToolActive("openclaw");
|
||||
const profileFilteredTools =
|
||||
hostCrestodianActive && isCrestodianOnlyCodexDynamicToolAllowlist(params.toolsAllow)
|
||||
? preserveRingZeroCrestodianTool(readableAllTools, normallyProfiledTools)
|
||||
hostSystemAgentActive && isSystemAgentOnlyCodexDynamicToolAllowlist(params.toolsAllow)
|
||||
? preserveRingZeroSystemAgentTool(readableAllTools, normallyProfiledTools)
|
||||
: normallyProfiledTools;
|
||||
const codexFilteredTools = addNodeShellDynamicToolsIfNeeded(
|
||||
addSandboxShellDynamicToolsIfAvailable(
|
||||
|
||||
@@ -38,12 +38,12 @@ export function normalizeCodexDynamicToolName(name: string): string {
|
||||
return DYNAMIC_TOOL_NAME_ALIASES[normalized] ?? normalized;
|
||||
}
|
||||
|
||||
/** True only for the host-scoped Crestodian run's exact tool contract. */
|
||||
export function isCrestodianOnlyCodexDynamicToolAllowlist(
|
||||
/** True only for the host-scoped OpenClaw run's exact tool contract. */
|
||||
export function isSystemAgentOnlyCodexDynamicToolAllowlist(
|
||||
toolsAllow: readonly string[] | undefined,
|
||||
): boolean {
|
||||
return (
|
||||
toolsAllow?.length === 1 && normalizeCodexDynamicToolName(toolsAllow[0] ?? "") === "crestodian"
|
||||
toolsAllow?.length === 1 && normalizeCodexDynamicToolName(toolsAllow[0] ?? "") === "openclaw"
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -887,8 +887,8 @@ function createCodexDynamicToolSpecs(params: {
|
||||
const directOnlyNamespaceTools: CodexDynamicToolFunctionSpec[] = [];
|
||||
for (const entry of params.entries) {
|
||||
const functionSpec = createCodexDynamicToolFunctionSpec({ entry });
|
||||
if (entry.name === "crestodian" && params.directToolNames.has(entry.name)) {
|
||||
// Crestodian is ring-zero and its whole turn surface. Keep its canonical
|
||||
if (entry.name === "openclaw" && params.directToolNames.has(entry.name)) {
|
||||
// OpenClaw is ring-zero and its whole turn surface. Keep its canonical
|
||||
// root name even though generic direct-only tools use a model namespace.
|
||||
specs.push(functionSpec);
|
||||
continue;
|
||||
|
||||
@@ -224,7 +224,7 @@ export async function prepareCodexAttemptTools(runtime: CodexAttemptRuntime) {
|
||||
}),
|
||||
directToolNames: resolveCodexDynamicToolDirectNames(
|
||||
params,
|
||||
isHostScopedAgentToolActive("crestodian"),
|
||||
isHostScopedAgentToolActive("openclaw"),
|
||||
),
|
||||
hookContext: {
|
||||
agentId: sessionAgentId,
|
||||
|
||||
@@ -4,7 +4,7 @@ import type {
|
||||
} from "openclaw/plugin-sdk/agent-harness-runtime";
|
||||
import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
|
||||
import { handleCodexAppServerApprovalRequest } from "./approval-bridge.js";
|
||||
import { isCrestodianOnlyCodexDynamicToolAllowlist } from "./dynamic-tool-profile.js";
|
||||
import { isSystemAgentOnlyCodexDynamicToolAllowlist } from "./dynamic-tool-profile.js";
|
||||
import type {
|
||||
CodexDynamicToolCallParams,
|
||||
CodexDynamicToolCallResponse,
|
||||
@@ -108,15 +108,15 @@ export function handleApprovalRequest(params: {
|
||||
|
||||
export function resolveCodexDynamicToolDirectNames(
|
||||
params: EmbeddedRunAttemptParams,
|
||||
hostCrestodianActive = false,
|
||||
hostSystemAgentActive = false,
|
||||
): string[] {
|
||||
// Tools with catalogMode=direct-only use the model-only namespace. This list
|
||||
// remains for control tools that intentionally live at the dynamic-tool root.
|
||||
const names: string[] = [];
|
||||
// Crestodian is the run's only tool and must stay callable when Codex tool
|
||||
// OpenClaw is the run's only tool and must stay callable when Codex tool
|
||||
// search is unavailable. Exact toolsAllow is the public harness contract.
|
||||
if (hostCrestodianActive && isCrestodianOnlyCodexDynamicToolAllowlist(params.toolsAllow)) {
|
||||
names.push("crestodian");
|
||||
if (hostSystemAgentActive && isSystemAgentOnlyCodexDynamicToolAllowlist(params.toolsAllow)) {
|
||||
names.push("openclaw");
|
||||
}
|
||||
if (params.sourceReplyDeliveryMode === "message_tool_only") {
|
||||
names.push("message");
|
||||
|
||||
@@ -108,15 +108,15 @@ const testing = {
|
||||
filterCodexDynamicTools,
|
||||
resolveCodexDynamicToolDirectNames(
|
||||
params: EmbeddedRunAttemptParams,
|
||||
hostCrestodianActive = false,
|
||||
hostSystemAgentActive = false,
|
||||
): string[] {
|
||||
const names: string[] = [];
|
||||
if (
|
||||
hostCrestodianActive &&
|
||||
hostSystemAgentActive &&
|
||||
params.toolsAllow?.length === 1 &&
|
||||
params.toolsAllow[0] === "crestodian"
|
||||
params.toolsAllow[0] === "openclaw"
|
||||
) {
|
||||
names.push("crestodian");
|
||||
names.push("openclaw");
|
||||
}
|
||||
if (params.sourceReplyDeliveryMode === "message_tool_only") {
|
||||
names.push("message");
|
||||
@@ -419,14 +419,14 @@ function createCodexToolBridgeForTest(
|
||||
params: EmbeddedRunAttemptParams,
|
||||
tools: RuntimeDynamicToolForTest[],
|
||||
registeredTools: RuntimeDynamicToolForTest[] = tools,
|
||||
hostCrestodianActive = false,
|
||||
hostSystemAgentActive = false,
|
||||
) {
|
||||
const signal = new AbortController().signal;
|
||||
return createCodexDynamicToolBridge({
|
||||
tools,
|
||||
registeredTools,
|
||||
signal,
|
||||
directToolNames: testing.resolveCodexDynamicToolDirectNames(params, hostCrestodianActive),
|
||||
directToolNames: testing.resolveCodexDynamicToolDirectNames(params, hostSystemAgentActive),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -265,7 +265,7 @@ function createTwoCalendarAppPolicyContext() {
|
||||
setupRunAttemptTestHooks();
|
||||
|
||||
describe("Codex app-server thread lifecycle bindings", () => {
|
||||
it("resumes the same restricted Crestodian thread so turn two retains native memory", async () => {
|
||||
it("resumes the same restricted OpenClaw thread so turn two retains native memory", async () => {
|
||||
const sessionFile = path.join(tempDir, "session.jsonl");
|
||||
const workspaceDir = path.join(tempDir, "workspace");
|
||||
await writeCodexAppServerBinding(sessionFile, {
|
||||
@@ -276,7 +276,7 @@ describe("Codex app-server thread lifecycle bindings", () => {
|
||||
dynamicToolsFingerprint: "[]",
|
||||
});
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
params.toolsAllow = ["crestodian"];
|
||||
params.toolsAllow = ["openclaw"];
|
||||
let nextThread = 1;
|
||||
const request = vi.fn(async (method: string, _requestParams?: unknown) => {
|
||||
if (method === "config/read") {
|
||||
@@ -308,11 +308,11 @@ describe("Codex app-server thread lifecycle bindings", () => {
|
||||
client: { request } as never,
|
||||
params,
|
||||
cwd: workspaceDir,
|
||||
dynamicTools: [createNamedDynamicTool("crestodian")],
|
||||
dynamicTools: [createNamedDynamicTool("openclaw")],
|
||||
appServer: createThreadLifecycleAppServerOptions(),
|
||||
nativeCodeModeEnabled: false,
|
||||
userMcpServersEnabled: false,
|
||||
hostCrestodianActive: true,
|
||||
hostSystemAgentActive: true,
|
||||
};
|
||||
|
||||
const first = await startOrResumeThread(common);
|
||||
@@ -347,11 +347,11 @@ describe("Codex app-server thread lifecycle bindings", () => {
|
||||
expect(binding?.ringZeroClientInstanceId).toEqual(expect.any(String));
|
||||
});
|
||||
|
||||
it("starts a fresh restricted Crestodian thread for a new app-server client", async () => {
|
||||
it("starts a fresh restricted OpenClaw thread for a new app-server client", async () => {
|
||||
const sessionFile = path.join(tempDir, "session.jsonl");
|
||||
const workspaceDir = path.join(tempDir, "workspace");
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
params.toolsAllow = ["crestodian"];
|
||||
params.toolsAllow = ["openclaw"];
|
||||
let nextThread = 1;
|
||||
const request = vi.fn(async (method: string, _requestParams?: unknown) => {
|
||||
if (method === "config/read") {
|
||||
@@ -371,11 +371,11 @@ describe("Codex app-server thread lifecycle bindings", () => {
|
||||
const common = {
|
||||
params,
|
||||
cwd: workspaceDir,
|
||||
dynamicTools: [createNamedDynamicTool("crestodian")],
|
||||
dynamicTools: [createNamedDynamicTool("openclaw")],
|
||||
appServer: createThreadLifecycleAppServerOptions(),
|
||||
nativeCodeModeEnabled: false,
|
||||
userMcpServersEnabled: false,
|
||||
hostCrestodianActive: true,
|
||||
hostSystemAgentActive: true,
|
||||
};
|
||||
|
||||
const first = await startOrResumeThread({ ...common, client: { request } as never });
|
||||
@@ -393,11 +393,11 @@ describe("Codex app-server thread lifecycle bindings", () => {
|
||||
expect((await readCodexAppServerBinding(sessionFile))?.threadId).toBe("thread-ring-zero-2");
|
||||
});
|
||||
|
||||
it("retires a warm Crestodian binding when resume MCP attestation fails", async () => {
|
||||
it("retires a warm OpenClaw binding when resume MCP attestation fails", async () => {
|
||||
const sessionFile = path.join(tempDir, "session.jsonl");
|
||||
const workspaceDir = path.join(tempDir, "workspace");
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
params.toolsAllow = ["crestodian"];
|
||||
params.toolsAllow = ["openclaw"];
|
||||
let attestationCount = 0;
|
||||
const request = vi.fn(async (method: string) => {
|
||||
if (method === "config/read") {
|
||||
@@ -424,11 +424,11 @@ describe("Codex app-server thread lifecycle bindings", () => {
|
||||
abandonClient,
|
||||
params,
|
||||
cwd: workspaceDir,
|
||||
dynamicTools: [createNamedDynamicTool("crestodian")],
|
||||
dynamicTools: [createNamedDynamicTool("openclaw")],
|
||||
appServer: createThreadLifecycleAppServerOptions(),
|
||||
nativeCodeModeEnabled: false,
|
||||
userMcpServersEnabled: false,
|
||||
hostCrestodianActive: true,
|
||||
hostSystemAgentActive: true,
|
||||
};
|
||||
|
||||
await startOrResumeThread(common);
|
||||
@@ -451,7 +451,7 @@ describe("Codex app-server thread lifecycle bindings", () => {
|
||||
expect(await readCodexAppServerBinding(sessionFile)).toBeUndefined();
|
||||
});
|
||||
|
||||
it("fails closed before starting Crestodian when inherited MCP enumeration fails", async () => {
|
||||
it("fails closed before starting OpenClaw when inherited MCP enumeration fails", async () => {
|
||||
const sessionFile = path.join(tempDir, "session.jsonl");
|
||||
const workspaceDir = path.join(tempDir, "workspace");
|
||||
await writeCodexAppServerBinding(sessionFile, {
|
||||
@@ -462,7 +462,7 @@ describe("Codex app-server thread lifecycle bindings", () => {
|
||||
dynamicToolsFingerprint: "[]",
|
||||
});
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
params.toolsAllow = ["crestodian"];
|
||||
params.toolsAllow = ["openclaw"];
|
||||
const request = vi.fn(async (method: string) => {
|
||||
if (method === "config/read") {
|
||||
throw new Error("config unavailable");
|
||||
@@ -475,11 +475,11 @@ describe("Codex app-server thread lifecycle bindings", () => {
|
||||
client: { request } as never,
|
||||
params,
|
||||
cwd: workspaceDir,
|
||||
dynamicTools: [createNamedDynamicTool("crestodian")],
|
||||
dynamicTools: [createNamedDynamicTool("openclaw")],
|
||||
appServer: createThreadLifecycleAppServerOptions(),
|
||||
nativeCodeModeEnabled: false,
|
||||
userMcpServersEnabled: false,
|
||||
hostCrestodianActive: true,
|
||||
hostSystemAgentActive: true,
|
||||
}),
|
||||
).rejects.toThrow("config unavailable");
|
||||
expect(request.mock.calls.map(([method]) => method)).toEqual(["config/read"]);
|
||||
@@ -491,11 +491,11 @@ describe("Codex app-server thread lifecycle bindings", () => {
|
||||
{ name: "legacy managed MDM", layer: { name: { type: "legacyManagedConfigTomlFromMdm" } } },
|
||||
{ name: "unknown future", layer: { name: { type: "futureManaged" } } },
|
||||
{ name: "malformed", layer: { name: {} } },
|
||||
])("fails closed on $name config layers before Crestodian thread/start", async ({ layer }) => {
|
||||
])("fails closed on $name config layers before OpenClaw thread/start", async ({ layer }) => {
|
||||
const sessionFile = path.join(tempDir, "session.jsonl");
|
||||
const workspaceDir = path.join(tempDir, "workspace");
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
params.toolsAllow = ["crestodian"];
|
||||
params.toolsAllow = ["openclaw"];
|
||||
const request = vi.fn(async (method: string) => {
|
||||
if (method === "config/read") {
|
||||
return { config: {}, layers: [layer] };
|
||||
@@ -508,23 +508,23 @@ describe("Codex app-server thread lifecycle bindings", () => {
|
||||
client: { request } as never,
|
||||
params,
|
||||
cwd: workspaceDir,
|
||||
dynamicTools: [createNamedDynamicTool("crestodian")],
|
||||
dynamicTools: [createNamedDynamicTool("openclaw")],
|
||||
appServer: createThreadLifecycleAppServerOptions(),
|
||||
nativeCodeModeEnabled: false,
|
||||
userMcpServersEnabled: false,
|
||||
hostCrestodianActive: true,
|
||||
hostSystemAgentActive: true,
|
||||
}),
|
||||
).rejects.toThrow(/config layer|config layers/u);
|
||||
expect(request.mock.calls.map(([method]) => method)).toEqual(["config/read"]);
|
||||
});
|
||||
|
||||
it.each(["hooks", "managed_hooks"] as const)(
|
||||
"fails closed on non-empty %s requirements before Crestodian thread/start",
|
||||
"fails closed on non-empty %s requirements before OpenClaw thread/start",
|
||||
async (requirementsKey) => {
|
||||
const sessionFile = path.join(tempDir, "session.jsonl");
|
||||
const workspaceDir = path.join(tempDir, "workspace");
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
params.toolsAllow = ["crestodian"];
|
||||
params.toolsAllow = ["openclaw"];
|
||||
const request = vi.fn(async (method: string) => {
|
||||
if (method === "config/read") {
|
||||
return { config: {}, layers: [] };
|
||||
@@ -546,11 +546,11 @@ describe("Codex app-server thread lifecycle bindings", () => {
|
||||
client: { request } as never,
|
||||
params,
|
||||
cwd: workspaceDir,
|
||||
dynamicTools: [createNamedDynamicTool("crestodian")],
|
||||
dynamicTools: [createNamedDynamicTool("openclaw")],
|
||||
appServer: createThreadLifecycleAppServerOptions(),
|
||||
nativeCodeModeEnabled: false,
|
||||
userMcpServersEnabled: false,
|
||||
hostCrestodianActive: true,
|
||||
hostSystemAgentActive: true,
|
||||
}),
|
||||
).rejects.toThrow("cannot override managed hooks");
|
||||
expect(request.mock.calls.map(([method]) => method)).toEqual([
|
||||
@@ -564,7 +564,7 @@ describe("Codex app-server thread lifecycle bindings", () => {
|
||||
const sessionFile = path.join(tempDir, "session.jsonl");
|
||||
const workspaceDir = path.join(tempDir, "workspace");
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
params.toolsAllow = ["crestodian"];
|
||||
params.toolsAllow = ["openclaw"];
|
||||
const request = vi.fn(async (method: string) => {
|
||||
if (method === "config/read") {
|
||||
return { config: {}, layers: [] };
|
||||
@@ -580,11 +580,11 @@ describe("Codex app-server thread lifecycle bindings", () => {
|
||||
client: { request } as never,
|
||||
params,
|
||||
cwd: workspaceDir,
|
||||
dynamicTools: [createNamedDynamicTool("crestodian")],
|
||||
dynamicTools: [createNamedDynamicTool("openclaw")],
|
||||
appServer: createThreadLifecycleAppServerOptions(),
|
||||
nativeCodeModeEnabled: false,
|
||||
userMcpServersEnabled: false,
|
||||
hostCrestodianActive: true,
|
||||
hostSystemAgentActive: true,
|
||||
}),
|
||||
).rejects.toThrow("cannot override required feature hooks");
|
||||
expect(request.mock.calls.map(([method]) => method)).toEqual([
|
||||
@@ -597,7 +597,7 @@ describe("Codex app-server thread lifecycle bindings", () => {
|
||||
{ name: "a newly raced server", attestation: { data: [{ name: "raced" }] } },
|
||||
{ name: "a malformed inventory", attestation: { data: "invalid" } },
|
||||
{ name: "an inventory RPC failure", attestation: new Error("inventory failed") },
|
||||
])("retires the cold Crestodian thread when attestation finds $name", async ({ attestation }) => {
|
||||
])("retires the cold OpenClaw thread when attestation finds $name", async ({ attestation }) => {
|
||||
const sessionFile = path.join(tempDir, "session.jsonl");
|
||||
const workspaceDir = path.join(tempDir, "workspace");
|
||||
await writeCodexAppServerBinding(sessionFile, {
|
||||
@@ -608,7 +608,7 @@ describe("Codex app-server thread lifecycle bindings", () => {
|
||||
dynamicToolsFingerprint: "[]",
|
||||
});
|
||||
const params = createParams(sessionFile, workspaceDir);
|
||||
params.toolsAllow = ["crestodian"];
|
||||
params.toolsAllow = ["openclaw"];
|
||||
const abandonClient = vi.fn(async () => {});
|
||||
const request = vi.fn(async (method: string) => {
|
||||
if (method === "config/read") {
|
||||
@@ -635,11 +635,11 @@ describe("Codex app-server thread lifecycle bindings", () => {
|
||||
abandonClient,
|
||||
params,
|
||||
cwd: workspaceDir,
|
||||
dynamicTools: [createNamedDynamicTool("crestodian")],
|
||||
dynamicTools: [createNamedDynamicTool("openclaw")],
|
||||
appServer: createThreadLifecycleAppServerOptions(),
|
||||
nativeCodeModeEnabled: false,
|
||||
userMcpServersEnabled: false,
|
||||
hostCrestodianActive: true,
|
||||
hostSystemAgentActive: true,
|
||||
}),
|
||||
).rejects.toThrow();
|
||||
expect(abandonClient).toHaveBeenCalledTimes(1);
|
||||
|
||||
@@ -39,19 +39,19 @@ type CodexThreadLifecycleTimingLogger = NonNullable<
|
||||
describe("Codex ring-zero thread config", () => {
|
||||
it("applies the restriction to both thread start and resume", () => {
|
||||
const params = createAttemptParams({ provider: "openai" });
|
||||
params.toolsAllow = ["crestodian"];
|
||||
params.toolsAllow = ["openclaw"];
|
||||
const appServer = createAppServerOptions() as never;
|
||||
const start = buildThreadStartParams(params, {
|
||||
appServer,
|
||||
cwd: "/repo",
|
||||
dynamicTools: [],
|
||||
hostCrestodianActive: true,
|
||||
hostSystemAgentActive: true,
|
||||
nativeCodeModeEnabled: false,
|
||||
});
|
||||
const resume = buildThreadResumeParams(params, {
|
||||
appServer,
|
||||
dynamicTools: [],
|
||||
hostCrestodianActive: true,
|
||||
hostSystemAgentActive: true,
|
||||
nativeCodeModeEnabled: false,
|
||||
threadId: "thread-1",
|
||||
});
|
||||
@@ -78,7 +78,7 @@ describe("Codex ring-zero thread config", () => {
|
||||
appServer,
|
||||
cwd: "/repo",
|
||||
dynamicTools: [],
|
||||
hostCrestodianActive: false,
|
||||
hostSystemAgentActive: false,
|
||||
});
|
||||
expect(normal.baseInstructions).toBeUndefined();
|
||||
});
|
||||
|
||||
@@ -39,7 +39,7 @@ import {
|
||||
resolveCodexContextEngineProjectionReserveTokens,
|
||||
} from "./context-engine-projection.js";
|
||||
import {
|
||||
isCrestodianOnlyCodexDynamicToolAllowlist,
|
||||
isSystemAgentOnlyCodexDynamicToolAllowlist,
|
||||
normalizeCodexDynamicToolName,
|
||||
shouldDisableCodexToolSearchForModel,
|
||||
} from "./dynamic-tool-profile.js";
|
||||
@@ -431,7 +431,7 @@ export async function startOrResumeThread(params: {
|
||||
contextEngineProjection?: CodexContextEngineThreadBootstrapProjection;
|
||||
signal?: AbortSignal;
|
||||
timing?: CodexThreadLifecycleTimingOptions;
|
||||
hostCrestodianActive?: boolean;
|
||||
hostSystemAgentActive?: boolean;
|
||||
}): Promise<CodexAppServerThreadLifecycleBinding> {
|
||||
const bindingIdentity: CodexAppServerBindingIdentity = sessionBindingIdentity({
|
||||
sessionId: params.params.sessionId,
|
||||
@@ -490,10 +490,10 @@ export async function startOrResumeThread(params: {
|
||||
const environmentSelectionFingerprint = fingerprintEnvironmentSelection(
|
||||
params.environmentSelection,
|
||||
);
|
||||
const hostCrestodianActive =
|
||||
params.hostCrestodianActive ?? isHostScopedAgentToolActive("crestodian");
|
||||
const hostSystemAgentActive =
|
||||
params.hostSystemAgentActive ?? isHostScopedAgentToolActive("openclaw");
|
||||
const ringZeroActive =
|
||||
hostCrestodianActive && isCrestodianOnlyCodexDynamicToolAllowlist(params.params.toolsAllow);
|
||||
hostSystemAgentActive && isSystemAgentOnlyCodexDynamicToolAllowlist(params.params.toolsAllow);
|
||||
if (ringZeroActive && params.nativeCodeModeEnabled !== false) {
|
||||
throw new Error("Codex ring-zero requires native code mode to be disabled");
|
||||
}
|
||||
@@ -1004,7 +1004,7 @@ export async function startOrResumeThread(params: {
|
||||
nativeProviderWebSearchSupport: params.nativeProviderWebSearchSupport,
|
||||
nativeCodeModeOnlyEnabled: params.nativeCodeModeOnlyEnabled,
|
||||
webSearchAllowed: params.webSearchAllowed,
|
||||
hostCrestodianActive,
|
||||
hostSystemAgentActive,
|
||||
ringZeroInheritedMcpServerNames,
|
||||
}),
|
||||
);
|
||||
@@ -1215,7 +1215,7 @@ export async function startOrResumeThread(params: {
|
||||
environmentSelection: params.environmentSelection,
|
||||
model: startModelSelection.model,
|
||||
modelProvider: startModelProvider,
|
||||
hostCrestodianActive,
|
||||
hostSystemAgentActive,
|
||||
ringZeroInheritedMcpServerNames,
|
||||
}),
|
||||
);
|
||||
@@ -2161,13 +2161,13 @@ export function buildThreadStartParams(
|
||||
environmentSelection?: CodexTurnEnvironmentParams[];
|
||||
model?: string | null;
|
||||
modelProvider?: string | null;
|
||||
hostCrestodianActive?: boolean;
|
||||
hostSystemAgentActive?: boolean;
|
||||
ringZeroInheritedMcpServerNames?: readonly string[];
|
||||
},
|
||||
): CodexThreadStartParams {
|
||||
const ringZeroActive =
|
||||
(options.hostCrestodianActive ?? isHostScopedAgentToolActive("crestodian")) &&
|
||||
isCrestodianOnlyCodexDynamicToolAllowlist(params.toolsAllow);
|
||||
(options.hostSystemAgentActive ?? isHostScopedAgentToolActive("openclaw")) &&
|
||||
isSystemAgentOnlyCodexDynamicToolAllowlist(params.toolsAllow);
|
||||
const resolvedModelProvider = resolveCodexAppServerModelProvider({
|
||||
provider: params.provider,
|
||||
authProfileId: params.authProfileId,
|
||||
@@ -2203,7 +2203,7 @@ export function buildThreadStartParams(
|
||||
directOnlyToolNamespaces: resolveDirectOnlyToolNamespaces(options.dynamicTools),
|
||||
webSearchAllowed: options.webSearchAllowed,
|
||||
appServer: options.appServer,
|
||||
hostCrestodianActive: options.hostCrestodianActive,
|
||||
hostSystemAgentActive: options.hostSystemAgentActive,
|
||||
ringZeroInheritedMcpServerNames: options.ringZeroInheritedMcpServerNames,
|
||||
}),
|
||||
...resolveCodexThreadEnvironmentSelection(options),
|
||||
@@ -2232,7 +2232,7 @@ export function buildThreadResumeParams(
|
||||
nativeCodeModeOnlyEnabled?: boolean;
|
||||
webSearchAllowed?: boolean;
|
||||
model?: string | null;
|
||||
hostCrestodianActive?: boolean;
|
||||
hostSystemAgentActive?: boolean;
|
||||
ringZeroInheritedMcpServerNames?: readonly string[];
|
||||
preserveNativeModel?: boolean;
|
||||
},
|
||||
@@ -2277,7 +2277,7 @@ export function buildThreadResumeParams(
|
||||
directOnlyToolNamespaces: resolveDirectOnlyToolNamespaces(options.dynamicTools),
|
||||
webSearchAllowed: options.webSearchAllowed,
|
||||
appServer: options.appServer,
|
||||
hostCrestodianActive: options.hostCrestodianActive,
|
||||
hostSystemAgentActive: options.hostSystemAgentActive,
|
||||
ringZeroInheritedMcpServerNames: options.ringZeroInheritedMcpServerNames,
|
||||
}),
|
||||
developerInstructions:
|
||||
@@ -2463,13 +2463,13 @@ function buildCodexRuntimeThreadConfigForRun(
|
||||
directOnlyToolNamespaces?: readonly string[];
|
||||
webSearchAllowed?: boolean;
|
||||
appServer?: Pick<CodexAppServerRuntimeOptions, "networkProxy">;
|
||||
hostCrestodianActive?: boolean;
|
||||
hostSystemAgentActive?: boolean;
|
||||
ringZeroInheritedMcpServerNames?: readonly string[];
|
||||
} = {},
|
||||
): JsonObject {
|
||||
const ringZeroActive =
|
||||
(options.hostCrestodianActive ?? isHostScopedAgentToolActive("crestodian")) &&
|
||||
isCrestodianOnlyCodexDynamicToolAllowlist(params.toolsAllow);
|
||||
(options.hostSystemAgentActive ?? isHostScopedAgentToolActive("openclaw")) &&
|
||||
isSystemAgentOnlyCodexDynamicToolAllowlist(params.toolsAllow);
|
||||
const configMcpServers = config?.mcp_servers;
|
||||
if (ringZeroActive && configMcpServers !== undefined && !isJsonObject(configMcpServers)) {
|
||||
throw new Error("Codex ring-zero received invalid thread mcp_servers config");
|
||||
@@ -2498,7 +2498,7 @@ function buildCodexRuntimeThreadConfigForRun(
|
||||
: undefined,
|
||||
buildCodexRingZeroThreadConfigPatch(
|
||||
params,
|
||||
options.hostCrestodianActive,
|
||||
options.hostSystemAgentActive,
|
||||
ringZeroMcpServerNames,
|
||||
),
|
||||
) ?? baseConfig;
|
||||
@@ -2515,10 +2515,10 @@ function buildCodexRuntimeThreadConfigForRun(
|
||||
|
||||
function buildCodexRingZeroThreadConfigPatch(
|
||||
params: Pick<EmbeddedRunAttemptParams, "toolsAllow">,
|
||||
hostCrestodianActive = isHostScopedAgentToolActive("crestodian"),
|
||||
hostSystemAgentActive = isHostScopedAgentToolActive("openclaw"),
|
||||
inheritedMcpServerNames: readonly string[] = [],
|
||||
): JsonObject | undefined {
|
||||
if (!hostCrestodianActive || !isCrestodianOnlyCodexDynamicToolAllowlist(params.toolsAllow)) {
|
||||
if (!hostSystemAgentActive || !isSystemAgentOnlyCodexDynamicToolAllowlist(params.toolsAllow)) {
|
||||
return undefined;
|
||||
}
|
||||
// Narrow OpenClaw allowlists already send environments: [] and disable
|
||||
|
||||
@@ -3398,19 +3398,19 @@ describe("runCopilotAttempt", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps a host-scoped Crestodian create-session surface ring-zero", async () => {
|
||||
it("keeps a host-scoped OpenClaw create-session surface ring-zero", async () => {
|
||||
const sdk = makeFakeSdk();
|
||||
const pool = makeFakePool(sdk);
|
||||
const sdkTools = [makeFakeSdkTool("crestodian")];
|
||||
const sdkTools = [makeFakeSdkTool("openclaw")];
|
||||
const createToolBridge = vi.fn(async () => ({ sdkTools, sourceTools: [] }));
|
||||
|
||||
await runCopilotAttempt(makeParams({ toolsAllow: ["crestodian"] }), {
|
||||
await runCopilotAttempt(makeParams({ toolsAllow: ["openclaw"] }), {
|
||||
createToolBridge,
|
||||
isHostScopedToolActive: (toolName) => toolName === "crestodian",
|
||||
isHostScopedToolActive: (toolName) => toolName === "openclaw",
|
||||
pool,
|
||||
});
|
||||
|
||||
expect(readAvailableTools(sdk.createSession.mock.calls[0])).toEqual(["crestodian"]);
|
||||
expect(readAvailableTools(sdk.createSession.mock.calls[0])).toEqual(["openclaw"]);
|
||||
});
|
||||
|
||||
it("forwards `[]` to the SDK when the bridge returns no tools (disable / raw / fully filtered)", async () => {
|
||||
@@ -3481,31 +3481,31 @@ describe("runCopilotAttempt", () => {
|
||||
expect(resumeCfg?.availableTools).toEqual(["read", "builtin:ask_user"]);
|
||||
});
|
||||
|
||||
it("keeps a host-scoped Crestodian resume-session surface ring-zero", async () => {
|
||||
it("keeps a host-scoped OpenClaw resume-session surface ring-zero", async () => {
|
||||
const sdk = makeFakeSdk({
|
||||
onResumeSession: (session) => {
|
||||
session.sendAndWait.mockResolvedValueOnce(makeAssistantMessageEvent("resumed"));
|
||||
},
|
||||
});
|
||||
const pool = makeFakePool(sdk);
|
||||
const sdkTools = [makeFakeSdkTool("crestodian")];
|
||||
const sdkTools = [makeFakeSdkTool("openclaw")];
|
||||
const createToolBridge = vi.fn(async () => ({ sdkTools, sourceTools: [] }));
|
||||
|
||||
await runCopilotAttempt(
|
||||
makeParams({
|
||||
initialReplayState: { sdkSessionId: "sess-crestodian" },
|
||||
toolsAllow: ["crestodian"],
|
||||
initialReplayState: { sdkSessionId: "sess-openclaw" },
|
||||
toolsAllow: ["openclaw"],
|
||||
} as never),
|
||||
{
|
||||
createToolBridge,
|
||||
isHostScopedToolActive: (toolName) => toolName === "crestodian",
|
||||
isHostScopedToolActive: (toolName) => toolName === "openclaw",
|
||||
pool,
|
||||
},
|
||||
);
|
||||
|
||||
const resumeCall = sdk.resumeSession.mock.calls[0] as unknown[] | undefined;
|
||||
const resumeCfg = resumeCall?.[1] as { availableTools?: string[] };
|
||||
expect(resumeCfg?.availableTools).toEqual(["crestodian"]);
|
||||
expect(resumeCfg?.availableTools).toEqual(["openclaw"]);
|
||||
});
|
||||
|
||||
it("forwards `[]` to resumeSession when the bridge returns no tools", async () => {
|
||||
|
||||
@@ -364,10 +364,10 @@ export async function runCopilotAttempt(
|
||||
const attemptStartedAt = now();
|
||||
const input = params as AttemptParamsLike;
|
||||
const createToolBridge = deps.createToolBridge ?? createCopilotToolBridge;
|
||||
const hostCrestodianActive =
|
||||
deps.isHostScopedToolActive?.("crestodian") ?? isHostScopedAgentToolActive("crestodian");
|
||||
const ringZeroCrestodianRun =
|
||||
hostCrestodianActive && isCrestodianOnlyToolAllowlist(input.toolsAllow);
|
||||
const hostSystemAgentActive =
|
||||
deps.isHostScopedToolActive?.("openclaw") ?? isHostScopedAgentToolActive("openclaw");
|
||||
const ringZeroSystemAgentRun =
|
||||
hostSystemAgentActive && isSystemAgentOnlyToolAllowlist(input.toolsAllow);
|
||||
const messages = getMessagesSnapshotInput(input);
|
||||
const modelRef = resolveModelRef(input);
|
||||
const resolvedWorkspaceForSandbox =
|
||||
@@ -772,7 +772,7 @@ export async function runCopilotAttempt(
|
||||
emitLlmInput(prompt, additionalContext),
|
||||
}
|
||||
: undefined,
|
||||
includeAskUser: !ringZeroCrestodianRun,
|
||||
includeAskUser: !ringZeroSystemAgentRun,
|
||||
},
|
||||
);
|
||||
const compactionSessionConfig = byokProxy
|
||||
@@ -793,7 +793,7 @@ export async function runCopilotAttempt(
|
||||
emitLlmInput(prompt, additionalContext),
|
||||
}
|
||||
: undefined,
|
||||
includeAskUser: !ringZeroCrestodianRun,
|
||||
includeAskUser: !ringZeroSystemAgentRun,
|
||||
},
|
||||
)
|
||||
: sessionConfig;
|
||||
@@ -1372,7 +1372,7 @@ function createSessionConfig(
|
||||
tools: sdkTools,
|
||||
// Restrict the SDK's tool catalog to the bridged tool names returned
|
||||
// by `createCopilotToolBridge`, plus the built-in `ask_user` tool for
|
||||
// normal runs. Ring-zero Crestodian runs expose only Crestodian. Without this, the SDK
|
||||
// normal runs. Ring-zero OpenClaw runs expose only OpenClaw. Without this, the SDK
|
||||
// would still expose its native read/write/shell/url/mcp/memory/
|
||||
// hook tools to the model alongside our overrides, which would
|
||||
// bypass OpenClaw's wrapped-tool enforcement under any permissive
|
||||
@@ -1441,8 +1441,8 @@ function buildCopilotAvailableTools(sdkTools: SdkTool[], includeAskUser: boolean
|
||||
return [...new Set(availableTools)];
|
||||
}
|
||||
|
||||
function isCrestodianOnlyToolAllowlist(toolsAllow: readonly string[] | undefined): boolean {
|
||||
return toolsAllow?.length === 1 && toolsAllow[0]?.trim().toLowerCase() === "crestodian";
|
||||
function isSystemAgentOnlyToolAllowlist(toolsAllow: readonly string[] | undefined): boolean {
|
||||
return toolsAllow?.length === 1 && toolsAllow[0]?.trim().toLowerCase() === "openclaw";
|
||||
}
|
||||
|
||||
async function createMessageOptions(
|
||||
|
||||
@@ -188,27 +188,27 @@ describe("createCopilotToolBridge", () => {
|
||||
expect(result.sdkTools.map((tool) => tool.name)).toEqual(["tool-a", "tool-b"]);
|
||||
});
|
||||
|
||||
it("preserves direct-only Crestodian through the exact Copilot allowlist", async () => {
|
||||
const crestodianTool = makeTool({
|
||||
name: "crestodian",
|
||||
it("preserves direct-only OpenClaw through the exact Copilot allowlist", async () => {
|
||||
const systemAgentTool = makeTool({
|
||||
name: "openclaw",
|
||||
catalogMode: "direct-only",
|
||||
} as never);
|
||||
|
||||
const result = await createCopilotToolBridge({
|
||||
agentId: "crestodian",
|
||||
agentId: "openclaw",
|
||||
attemptParams: {
|
||||
runId: "crestodian-turn-1",
|
||||
sessionKey: "agent:crestodian:main",
|
||||
toolsAllow: ["crestodian"],
|
||||
runId: "openclaw-turn-1",
|
||||
sessionKey: "agent:openclaw:main",
|
||||
toolsAllow: ["openclaw"],
|
||||
} as never,
|
||||
createOpenClawCodingTools: async () => [crestodianTool],
|
||||
createOpenClawCodingTools: async () => [systemAgentTool],
|
||||
modelId: "gpt-4.1",
|
||||
modelProvider: "github-copilot",
|
||||
sessionId: "crestodian-session",
|
||||
sessionId: "openclaw-session",
|
||||
});
|
||||
|
||||
expect(result.sourceTools).toEqual([crestodianTool]);
|
||||
expect(result.sdkTools.map((tool) => tool.name)).toEqual(["crestodian"]);
|
||||
expect(result.sourceTools).toEqual([systemAgentTool]);
|
||||
expect(result.sdkTools.map((tool) => tool.name)).toEqual(["openclaw"]);
|
||||
});
|
||||
|
||||
it("compacts the Copilot tool surface behind tool_search controls when enabled", async () => {
|
||||
|
||||
@@ -10,7 +10,7 @@ const QA_SMOKE_CI_PARTS = ["profile-1", "profile-2"] as const;
|
||||
const QA_SMOKE_CI_CHANNELS = ["matrix", OPENCLAW_CRABLINE_DEFAULT_CHANNEL] as const;
|
||||
const QA_SMOKE_CI_SCENARIO_IDS = new Set([
|
||||
"control-ui-chat-flow-playwright",
|
||||
"crestodian-ring-zero-setup",
|
||||
"system-agent-ring-zero-setup",
|
||||
"dreaming-shadow-trial-report",
|
||||
"gateway-smoke",
|
||||
"luna-thinking-visibility-switch",
|
||||
|
||||
@@ -257,7 +257,7 @@ describe("qa suite runtime agent process helpers", () => {
|
||||
alternateModel: "openai/gpt-5.6-luna-mini",
|
||||
providerMode: "mock-openai",
|
||||
} as never,
|
||||
["crestodian", "-m", "overview"],
|
||||
["openclaw", "-m", "overview"],
|
||||
{
|
||||
env: {
|
||||
OPENCLAW_STATE_DIR: "/tmp/isolated-state",
|
||||
@@ -275,7 +275,7 @@ describe("qa suite runtime agent process helpers", () => {
|
||||
expect(spawnCall?.[0]).toBe("/usr/bin/node");
|
||||
expect(spawnCall?.[1]).toEqual([
|
||||
path.join("/repo", "dist", "index.js"),
|
||||
"crestodian",
|
||||
"openclaw",
|
||||
"-m",
|
||||
"overview",
|
||||
]);
|
||||
|
||||
@@ -32,7 +32,7 @@ const PRIVATE_ONLY_CORE_COMMANDS = new Set([
|
||||
"tools",
|
||||
"skill",
|
||||
"diagnostics",
|
||||
"crestodian",
|
||||
"openclaw",
|
||||
"tasks",
|
||||
"allowlist",
|
||||
"approve",
|
||||
|
||||
+3
-3
@@ -1818,8 +1818,8 @@
|
||||
"test:docker:cleanup": "bash scripts/test-cleanup-docker.sh",
|
||||
"test:docker:commitments-safety": "bash scripts/e2e/commitments-safety-docker.sh",
|
||||
"test:docker:config-reload": "bash scripts/e2e/config-reload-source-docker.sh",
|
||||
"test:docker:crestodian-first-run": "bash scripts/e2e/crestodian-first-run-docker.sh",
|
||||
"test:docker:crestodian-rescue": "bash scripts/e2e/crestodian-rescue-docker.sh",
|
||||
"test:docker:system-agent-first-run": "bash scripts/e2e/system-agent-first-run-docker.sh",
|
||||
"test:docker:system-agent-rescue": "bash scripts/e2e/system-agent-rescue-docker.sh",
|
||||
"test:docker:cron-cli": "bash scripts/e2e/cron-cli-docker.sh",
|
||||
"test:docker:cron-mcp-cleanup": "bash scripts/e2e/cron-mcp-cleanup-docker.sh",
|
||||
"test:docker:codex-media-path": "bash scripts/e2e/codex-media-path-docker.sh",
|
||||
@@ -1922,7 +1922,7 @@
|
||||
"test:live": "node scripts/test-live.mjs",
|
||||
"test:live:cache": "node scripts/run-with-env.mjs OPENCLAW_LIVE_TEST=1 OPENCLAW_LIVE_CACHE_TEST=1 -- node --import tsx scripts/check-live-cache.ts",
|
||||
"test:live:codex-harness": "node scripts/test-live.mjs --codex-harness -- src/gateway/gateway-codex-harness.live.test.ts",
|
||||
"test:live:crestodian-rescue-channel": "node scripts/run-with-env.mjs OPENCLAW_LIVE_CRESTODIAN_RESCUE_CHANNEL=1 -- node scripts/test-live.mjs -- src/crestodian/rescue-channel.live.test.ts",
|
||||
"test:live:system-agent-rescue-channel": "node scripts/run-with-env.mjs OPENCLAW_LIVE_SYSTEM_AGENT_RESCUE_CHANNEL=1 -- node scripts/test-live.mjs -- src/system-agent/rescue-channel.live.test.ts",
|
||||
"test:live:gateway-profiles": "node scripts/test-live.mjs -- src/gateway/gateway-models.profiles.live.test.ts",
|
||||
"test:live:media": "node --import tsx test/e2e/qa-lab/media/hosted-media-provider-live.ts",
|
||||
"test:live:media:image": "node --import tsx test/e2e/qa-lab/media/hosted-media-provider-live.ts image",
|
||||
|
||||
@@ -446,16 +446,16 @@ import {
|
||||
WakeParamsSchema,
|
||||
WebLoginStartParamsSchema,
|
||||
WebLoginWaitParamsSchema,
|
||||
CrestodianChatParamsSchema,
|
||||
CrestodianChatResultSchema,
|
||||
CrestodianSetupDetectParamsSchema,
|
||||
CrestodianSetupDetectResultSchema,
|
||||
CrestodianSetupVerifyParamsSchema,
|
||||
CrestodianSetupVerifyResultSchema,
|
||||
CrestodianSetupActivateParamsSchema,
|
||||
CrestodianSetupActivateResultSchema,
|
||||
CrestodianSetupAuthStartParamsSchema,
|
||||
CrestodianSetupAuthStartResultSchema,
|
||||
SystemAgentChatParamsSchema,
|
||||
SystemAgentChatResultSchema,
|
||||
SystemAgentSetupDetectParamsSchema,
|
||||
SystemAgentSetupDetectResultSchema,
|
||||
SystemAgentSetupVerifyParamsSchema,
|
||||
SystemAgentSetupVerifyResultSchema,
|
||||
SystemAgentSetupActivateParamsSchema,
|
||||
SystemAgentSetupActivateResultSchema,
|
||||
SystemAgentSetupAuthStartParamsSchema,
|
||||
SystemAgentSetupAuthStartResultSchema,
|
||||
WizardCancelParamsSchema,
|
||||
WizardNextParamsSchema,
|
||||
WizardNextResultSchema,
|
||||
@@ -680,14 +680,14 @@ export const validateConfigPatchParams = lazyCompile(ConfigPatchParamsSchema);
|
||||
export const validateConfigSchemaParams = lazyCompile(ConfigSchemaParamsSchema);
|
||||
export const validateConfigSchemaLookupParams = lazyCompile(ConfigSchemaLookupParamsSchema);
|
||||
export const validateConfigSchemaLookupResult = lazyCompile(ConfigSchemaLookupResultSchema);
|
||||
export const validateCrestodianChatParams = lazyCompile(CrestodianChatParamsSchema);
|
||||
export const validateCrestodianSetupDetectParams = lazyCompile(CrestodianSetupDetectParamsSchema);
|
||||
export const validateCrestodianSetupVerifyParams = lazyCompile(CrestodianSetupVerifyParamsSchema);
|
||||
export const validateCrestodianSetupActivateParams = lazyCompile(
|
||||
CrestodianSetupActivateParamsSchema,
|
||||
export const validateSystemAgentChatParams = lazyCompile(SystemAgentChatParamsSchema);
|
||||
export const validateSystemAgentSetupDetectParams = lazyCompile(SystemAgentSetupDetectParamsSchema);
|
||||
export const validateSystemAgentSetupVerifyParams = lazyCompile(SystemAgentSetupVerifyParamsSchema);
|
||||
export const validateSystemAgentSetupActivateParams = lazyCompile(
|
||||
SystemAgentSetupActivateParamsSchema,
|
||||
);
|
||||
export const validateCrestodianSetupAuthStartParams = lazyCompile(
|
||||
CrestodianSetupAuthStartParamsSchema,
|
||||
export const validateSystemAgentSetupAuthStartParams = lazyCompile(
|
||||
SystemAgentSetupAuthStartParamsSchema,
|
||||
);
|
||||
export const validateWizardStartParams = lazyCompile(WizardStartParamsSchema);
|
||||
export const validateWizardNextParams = lazyCompile(WizardNextParamsSchema);
|
||||
@@ -1049,16 +1049,16 @@ export {
|
||||
ConfigSchemaResponseSchema,
|
||||
ConfigSchemaLookupResultSchema,
|
||||
UpdateStatusParamsSchema,
|
||||
CrestodianChatParamsSchema,
|
||||
CrestodianChatResultSchema,
|
||||
CrestodianSetupDetectParamsSchema,
|
||||
CrestodianSetupDetectResultSchema,
|
||||
CrestodianSetupVerifyParamsSchema,
|
||||
CrestodianSetupVerifyResultSchema,
|
||||
CrestodianSetupActivateParamsSchema,
|
||||
CrestodianSetupActivateResultSchema,
|
||||
CrestodianSetupAuthStartParamsSchema,
|
||||
CrestodianSetupAuthStartResultSchema,
|
||||
SystemAgentChatParamsSchema,
|
||||
SystemAgentChatResultSchema,
|
||||
SystemAgentSetupDetectParamsSchema,
|
||||
SystemAgentSetupDetectResultSchema,
|
||||
SystemAgentSetupVerifyParamsSchema,
|
||||
SystemAgentSetupVerifyResultSchema,
|
||||
SystemAgentSetupActivateParamsSchema,
|
||||
SystemAgentSetupActivateResultSchema,
|
||||
SystemAgentSetupAuthStartParamsSchema,
|
||||
SystemAgentSetupAuthStartResultSchema,
|
||||
WizardStartParamsSchema,
|
||||
WizardNextParamsSchema,
|
||||
WizardCancelParamsSchema,
|
||||
@@ -1340,16 +1340,16 @@ export type {
|
||||
ConfigPatchParams,
|
||||
ConfigSchemaParams,
|
||||
ConfigSchemaResponse,
|
||||
CrestodianChatParams,
|
||||
CrestodianChatResult,
|
||||
CrestodianSetupDetectParams,
|
||||
CrestodianSetupDetectResult,
|
||||
CrestodianSetupVerifyParams,
|
||||
CrestodianSetupVerifyResult,
|
||||
CrestodianSetupActivateParams,
|
||||
CrestodianSetupActivateResult,
|
||||
CrestodianSetupAuthStartParams,
|
||||
CrestodianSetupAuthStartResult,
|
||||
SystemAgentChatParams,
|
||||
SystemAgentChatResult,
|
||||
SystemAgentSetupDetectParams,
|
||||
SystemAgentSetupDetectResult,
|
||||
SystemAgentSetupVerifyParams,
|
||||
SystemAgentSetupVerifyResult,
|
||||
SystemAgentSetupActivateParams,
|
||||
SystemAgentSetupActivateResult,
|
||||
SystemAgentSetupAuthStartParams,
|
||||
SystemAgentSetupAuthStartResult,
|
||||
WizardStartParams,
|
||||
WizardNextParams,
|
||||
WizardCancelParams,
|
||||
|
||||
@@ -16,7 +16,7 @@ export * from "./schema/channels.js";
|
||||
export * from "./schema/talk-marks.js";
|
||||
export * from "./schema/commands.js";
|
||||
export * from "./schema/config.js";
|
||||
export * from "./schema/crestodian.js";
|
||||
export * from "./schema/openclaw.js";
|
||||
export * from "./schema/cron.js";
|
||||
export * from "./schema/error-codes.js";
|
||||
export * from "./schema/environments.js";
|
||||
|
||||
@@ -7,7 +7,7 @@ import { SnapshotSchema, StateVersionSchema } from "./snapshot.js";
|
||||
|
||||
export const GATEWAY_SERVER_CAPS = {
|
||||
CHAT_SEND_ROUTING_CONTRACT: "chat-send-routing-contract",
|
||||
CRESTODIAN_SETUP_MODEL_REF: "crestodian-setup-model-ref",
|
||||
SYSTEM_AGENT_SETUP_MODEL_REF: "openclaw-setup-model-ref",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
|
||||
+10
-10
@@ -1,24 +1,24 @@
|
||||
import { Value } from "typebox/value";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { validateCrestodianSetupVerifyParams } from "../index.js";
|
||||
import { CrestodianSetupVerifyResultSchema } from "./crestodian.js";
|
||||
import { validateSystemAgentSetupVerifyParams } from "../index.js";
|
||||
import { SystemAgentSetupVerifyResultSchema } from "./openclaw.js";
|
||||
|
||||
describe("Crestodian setup verification protocol", () => {
|
||||
describe("OpenClaw setup verification protocol", () => {
|
||||
it("accepts only an empty request", () => {
|
||||
expect(validateCrestodianSetupVerifyParams({})).toBe(true);
|
||||
expect(validateCrestodianSetupVerifyParams({ modelRef: "openai/gpt-5.5" })).toBe(false);
|
||||
expect(validateSystemAgentSetupVerifyParams({})).toBe(true);
|
||||
expect(validateSystemAgentSetupVerifyParams({ modelRef: "openai/gpt-5.5" })).toBe(false);
|
||||
});
|
||||
|
||||
it("accepts the structured success and failure results", () => {
|
||||
expect(
|
||||
Value.Check(CrestodianSetupVerifyResultSchema, {
|
||||
Value.Check(SystemAgentSetupVerifyResultSchema, {
|
||||
ok: true,
|
||||
modelRef: "openai/gpt-5.5",
|
||||
latencyMs: 25,
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
Value.Check(CrestodianSetupVerifyResultSchema, {
|
||||
Value.Check(SystemAgentSetupVerifyResultSchema, {
|
||||
ok: false,
|
||||
status: "unavailable",
|
||||
error: "no configured model",
|
||||
@@ -28,7 +28,7 @@ describe("Crestodian setup verification protocol", () => {
|
||||
|
||||
it("rejects mixed or incomplete results", () => {
|
||||
expect(
|
||||
Value.Check(CrestodianSetupVerifyResultSchema, {
|
||||
Value.Check(SystemAgentSetupVerifyResultSchema, {
|
||||
ok: true,
|
||||
modelRef: "openai/gpt-5.5",
|
||||
latencyMs: 25,
|
||||
@@ -36,14 +36,14 @@ describe("Crestodian setup verification protocol", () => {
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
Value.Check(CrestodianSetupVerifyResultSchema, {
|
||||
Value.Check(SystemAgentSetupVerifyResultSchema, {
|
||||
ok: false,
|
||||
status: "ok",
|
||||
error: "contradictory result",
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
Value.Check(CrestodianSetupVerifyResultSchema, {
|
||||
Value.Check(SystemAgentSetupVerifyResultSchema, {
|
||||
ok: false,
|
||||
status: "unavailable",
|
||||
}),
|
||||
+23
-23
@@ -1,4 +1,4 @@
|
||||
// Gateway Protocol schema module defines Crestodian chat payloads.
|
||||
// Gateway Protocol schema module defines OpenClaw chat payloads.
|
||||
import type { Static } from "typebox";
|
||||
import { Type } from "typebox";
|
||||
import { closedObject } from "./closed-object.js";
|
||||
@@ -6,12 +6,12 @@ import { NonEmptyString } from "./primitives.js";
|
||||
import { WizardStartResultSchema } from "./wizard.js";
|
||||
|
||||
/**
|
||||
* Crestodian chat lets clients (macOS app onboarding, future UIs) hold the
|
||||
* OpenClaw chat lets clients (macOS app onboarding, future UIs) hold the
|
||||
* setup/repair conversation over the gateway. The gateway live-tests the
|
||||
* configured inference route before creating a session. Omitting `message`
|
||||
* returns the welcome/greeting for a verified fresh session without input.
|
||||
*/
|
||||
export const CrestodianChatParamsSchema = closedObject({
|
||||
export const SystemAgentChatParamsSchema = closedObject({
|
||||
sessionId: NonEmptyString,
|
||||
message: Type.Optional(Type.String()),
|
||||
/** "onboarding" seeds the first-run setup proposal in the greeting. */
|
||||
@@ -20,8 +20,8 @@ export const CrestodianChatParamsSchema = closedObject({
|
||||
reset: Type.Optional(Type.Boolean()),
|
||||
});
|
||||
|
||||
/** One Crestodian reply; `action` tells clients about conversation handoffs. */
|
||||
export const CrestodianChatResultSchema = closedObject({
|
||||
/** One OpenClaw reply; `action` tells clients about conversation handoffs. */
|
||||
export const SystemAgentChatResultSchema = closedObject({
|
||||
sessionId: NonEmptyString,
|
||||
reply: NonEmptyString,
|
||||
/** The next reply is a hosted-wizard secret and clients must mask its input/echo. */
|
||||
@@ -42,7 +42,7 @@ export const CrestodianChatResultSchema = closedObject({
|
||||
* client can walk the ladder candidate-by-candidate without ever leaving a
|
||||
* broken default model behind.
|
||||
*/
|
||||
export const CrestodianSetupDetectParamsSchema = closedObject({});
|
||||
export const SystemAgentSetupDetectParamsSchema = closedObject({});
|
||||
|
||||
const SetupInferenceKind = Type.Union([
|
||||
Type.Literal("existing-model"),
|
||||
@@ -74,7 +74,7 @@ const SetupInferenceFailureStatus = Type.Union([
|
||||
Type.Literal("unknown"),
|
||||
]);
|
||||
|
||||
export const CrestodianSetupDetectResultSchema = closedObject({
|
||||
export const SystemAgentSetupDetectResultSchema = closedObject({
|
||||
candidates: Type.Array(
|
||||
closedObject({
|
||||
kind: SetupInferenceKind,
|
||||
@@ -115,9 +115,9 @@ export const CrestodianSetupDetectResultSchema = closedObject({
|
||||
});
|
||||
|
||||
/** Live verification of the Gateway's current default-agent inference route. */
|
||||
export const CrestodianSetupVerifyParamsSchema = closedObject({});
|
||||
export const SystemAgentSetupVerifyParamsSchema = closedObject({});
|
||||
|
||||
export const CrestodianSetupVerifyResultSchema = Type.Union([
|
||||
export const SystemAgentSetupVerifyResultSchema = Type.Union([
|
||||
closedObject({
|
||||
ok: Type.Literal(true),
|
||||
modelRef: NonEmptyString,
|
||||
@@ -130,7 +130,7 @@ export const CrestodianSetupVerifyResultSchema = Type.Union([
|
||||
}),
|
||||
]);
|
||||
|
||||
export const CrestodianSetupActivateParamsSchema = closedObject({
|
||||
export const SystemAgentSetupActivateParamsSchema = closedObject({
|
||||
kind: Type.Union([
|
||||
Type.Literal("existing-model"),
|
||||
Type.Literal("openai-api-key"),
|
||||
@@ -149,7 +149,7 @@ export const CrestodianSetupActivateParamsSchema = closedObject({
|
||||
workspace: Type.Optional(Type.String()),
|
||||
});
|
||||
|
||||
export const CrestodianSetupActivateResultSchema = closedObject({
|
||||
export const SystemAgentSetupActivateResultSchema = closedObject({
|
||||
ok: Type.Boolean(),
|
||||
/** Present on success: the model ref that answered the live test. */
|
||||
modelRef: Type.Optional(Type.String()),
|
||||
@@ -162,24 +162,24 @@ export const CrestodianSetupActivateResultSchema = closedObject({
|
||||
});
|
||||
|
||||
/** Starts one provider-owned interactive login as a gateway wizard session. */
|
||||
export const CrestodianSetupAuthStartParamsSchema = closedObject({
|
||||
export const SystemAgentSetupAuthStartParamsSchema = closedObject({
|
||||
/** Client-generated so cancellation remains possible if the start reply is lost. */
|
||||
sessionId: NonEmptyString,
|
||||
authChoice: NonEmptyString,
|
||||
workspace: Type.Optional(Type.String()),
|
||||
});
|
||||
|
||||
export const CrestodianSetupAuthStartResultSchema = WizardStartResultSchema;
|
||||
export const SystemAgentSetupAuthStartResultSchema = WizardStartResultSchema;
|
||||
|
||||
// Wire types derive directly from local schema consts so public d.ts graphs never
|
||||
// pull in the ProtocolSchemas registry.
|
||||
export type CrestodianChatParams = Static<typeof CrestodianChatParamsSchema>;
|
||||
export type CrestodianChatResult = Static<typeof CrestodianChatResultSchema>;
|
||||
export type CrestodianSetupDetectParams = Static<typeof CrestodianSetupDetectParamsSchema>;
|
||||
export type CrestodianSetupDetectResult = Static<typeof CrestodianSetupDetectResultSchema>;
|
||||
export type CrestodianSetupActivateParams = Static<typeof CrestodianSetupActivateParamsSchema>;
|
||||
export type CrestodianSetupActivateResult = Static<typeof CrestodianSetupActivateResultSchema>;
|
||||
export type CrestodianSetupVerifyParams = Static<typeof CrestodianSetupVerifyParamsSchema>;
|
||||
export type CrestodianSetupVerifyResult = Static<typeof CrestodianSetupVerifyResultSchema>;
|
||||
export type CrestodianSetupAuthStartParams = Static<typeof CrestodianSetupAuthStartParamsSchema>;
|
||||
export type CrestodianSetupAuthStartResult = Static<typeof CrestodianSetupAuthStartResultSchema>;
|
||||
export type SystemAgentChatParams = Static<typeof SystemAgentChatParamsSchema>;
|
||||
export type SystemAgentChatResult = Static<typeof SystemAgentChatResultSchema>;
|
||||
export type SystemAgentSetupDetectParams = Static<typeof SystemAgentSetupDetectParamsSchema>;
|
||||
export type SystemAgentSetupDetectResult = Static<typeof SystemAgentSetupDetectResultSchema>;
|
||||
export type SystemAgentSetupActivateParams = Static<typeof SystemAgentSetupActivateParamsSchema>;
|
||||
export type SystemAgentSetupActivateResult = Static<typeof SystemAgentSetupActivateResultSchema>;
|
||||
export type SystemAgentSetupVerifyParams = Static<typeof SystemAgentSetupVerifyParamsSchema>;
|
||||
export type SystemAgentSetupVerifyResult = Static<typeof SystemAgentSetupVerifyResultSchema>;
|
||||
export type SystemAgentSetupAuthStartParams = Static<typeof SystemAgentSetupAuthStartParamsSchema>;
|
||||
export type SystemAgentSetupAuthStartResult = Static<typeof SystemAgentSetupAuthStartResultSchema>;
|
||||
@@ -193,18 +193,6 @@ import {
|
||||
UpdateStatusParamsSchema,
|
||||
UpdateRunParamsSchema,
|
||||
} from "./config.js";
|
||||
import {
|
||||
CrestodianChatParamsSchema,
|
||||
CrestodianChatResultSchema,
|
||||
CrestodianSetupActivateParamsSchema,
|
||||
CrestodianSetupActivateResultSchema,
|
||||
CrestodianSetupAuthStartParamsSchema,
|
||||
CrestodianSetupAuthStartResultSchema,
|
||||
CrestodianSetupDetectParamsSchema,
|
||||
CrestodianSetupDetectResultSchema,
|
||||
CrestodianSetupVerifyParamsSchema,
|
||||
CrestodianSetupVerifyResultSchema,
|
||||
} from "./crestodian.js";
|
||||
import {
|
||||
CronAddParamsSchema,
|
||||
CronAddResultSchema,
|
||||
@@ -322,6 +310,18 @@ import {
|
||||
NodeSkillsUpdateParamsSchema,
|
||||
NodeRenameParamsSchema,
|
||||
} from "./nodes.js";
|
||||
import {
|
||||
SystemAgentChatParamsSchema,
|
||||
SystemAgentChatResultSchema,
|
||||
SystemAgentSetupActivateParamsSchema,
|
||||
SystemAgentSetupActivateResultSchema,
|
||||
SystemAgentSetupAuthStartParamsSchema,
|
||||
SystemAgentSetupAuthStartResultSchema,
|
||||
SystemAgentSetupDetectParamsSchema,
|
||||
SystemAgentSetupDetectResultSchema,
|
||||
SystemAgentSetupVerifyParamsSchema,
|
||||
SystemAgentSetupVerifyResultSchema,
|
||||
} from "./openclaw.js";
|
||||
import {
|
||||
PluginApprovalRequestParamsSchema,
|
||||
PluginApprovalResolveParamsSchema,
|
||||
@@ -694,16 +694,16 @@ export const ProtocolSchemas = {
|
||||
ConfigSchemaLookupParams: ConfigSchemaLookupParamsSchema,
|
||||
ConfigSchemaResponse: ConfigSchemaResponseSchema,
|
||||
ConfigSchemaLookupResult: ConfigSchemaLookupResultSchema,
|
||||
CrestodianChatParams: CrestodianChatParamsSchema,
|
||||
CrestodianChatResult: CrestodianChatResultSchema,
|
||||
CrestodianSetupDetectParams: CrestodianSetupDetectParamsSchema,
|
||||
CrestodianSetupDetectResult: CrestodianSetupDetectResultSchema,
|
||||
CrestodianSetupVerifyParams: CrestodianSetupVerifyParamsSchema,
|
||||
CrestodianSetupVerifyResult: CrestodianSetupVerifyResultSchema,
|
||||
CrestodianSetupActivateParams: CrestodianSetupActivateParamsSchema,
|
||||
CrestodianSetupActivateResult: CrestodianSetupActivateResultSchema,
|
||||
CrestodianSetupAuthStartParams: CrestodianSetupAuthStartParamsSchema,
|
||||
CrestodianSetupAuthStartResult: CrestodianSetupAuthStartResultSchema,
|
||||
SystemAgentChatParams: SystemAgentChatParamsSchema,
|
||||
SystemAgentChatResult: SystemAgentChatResultSchema,
|
||||
SystemAgentSetupDetectParams: SystemAgentSetupDetectParamsSchema,
|
||||
SystemAgentSetupDetectResult: SystemAgentSetupDetectResultSchema,
|
||||
SystemAgentSetupVerifyParams: SystemAgentSetupVerifyParamsSchema,
|
||||
SystemAgentSetupVerifyResult: SystemAgentSetupVerifyResultSchema,
|
||||
SystemAgentSetupActivateParams: SystemAgentSetupActivateParamsSchema,
|
||||
SystemAgentSetupActivateResult: SystemAgentSetupActivateResultSchema,
|
||||
SystemAgentSetupAuthStartParams: SystemAgentSetupAuthStartParamsSchema,
|
||||
SystemAgentSetupAuthStartResult: SystemAgentSetupAuthStartResultSchema,
|
||||
WizardStartParams: WizardStartParamsSchema,
|
||||
WizardNextParams: WizardNextParamsSchema,
|
||||
WizardCancelParams: WizardCancelParamsSchema,
|
||||
|
||||
+11
-11
@@ -1,33 +1,33 @@
|
||||
title: Crestodian ring-zero setup
|
||||
title: OpenClaw ring-zero setup
|
||||
|
||||
scenario:
|
||||
id: crestodian-ring-zero-setup
|
||||
id: system-agent-ring-zero-setup
|
||||
surface: config
|
||||
coverage:
|
||||
secondary:
|
||||
- config.crestodian-setup
|
||||
- config.system-agent-setup
|
||||
- channels.discord-config
|
||||
- agents.create
|
||||
objective: Verify the package-installed Crestodian inference gate and supporting one-shot setup operations, without claiming the interactive agent/tool/approval flow.
|
||||
objective: Verify the package-installed OpenClaw inference gate and supporting one-shot setup operations, without claiming the interactive agent/tool/approval flow.
|
||||
successCriteria:
|
||||
- Crestodian fails closed in an empty state dir and directs the user to inference onboarding.
|
||||
- The packaged activation module tests a fake Claude inference backend before persisting its model, with remaining setup deferred to Crestodian.
|
||||
- OpenClaw fails closed in an empty state dir and directs the user to inference onboarding.
|
||||
- The packaged activation module tests a fake Claude inference backend before persisting its model, with remaining setup deferred to OpenClaw.
|
||||
- A fuzzy packaged CLI request reaches the verified planner and resolves to a typed setup operation only after activation.
|
||||
- Supporting one-shot commands write the workspace/model and create a non-main agent.
|
||||
- Discord is enabled and configured through an env SecretRef without persisting the raw token.
|
||||
- Config validation passes and audit entries exist for every applied write.
|
||||
docsRefs:
|
||||
- docs/cli/crestodian.md
|
||||
- docs/cli/setup.md
|
||||
- docs/channels/discord.md
|
||||
- docs/help/testing.md
|
||||
codeRefs:
|
||||
- scripts/e2e/crestodian-first-run-docker.sh
|
||||
- scripts/e2e/crestodian-first-run-spec.json
|
||||
- test/e2e/qa-lab/runtime/crestodian-first-run-docker-client.ts
|
||||
- scripts/e2e/system-agent-first-run-docker.sh
|
||||
- scripts/e2e/system-agent-first-run-spec.json
|
||||
- test/e2e/qa-lab/runtime/system-agent-first-run-docker-client.ts
|
||||
execution:
|
||||
kind: script
|
||||
path: test/e2e/qa-lab/runtime/docker-e2e-lane.ts
|
||||
summary: Redirects to the packaged inference-gate and typed-operation Docker lane; coverage remains secondary until a real interactive agent/tool/approval flow is exercised.
|
||||
args:
|
||||
- --lane
|
||||
- crestodian-first-run
|
||||
- system-agent-first-run
|
||||
+11
-11
@@ -1,33 +1,33 @@
|
||||
title: Docker packaged Crestodian inference gate
|
||||
title: Docker packaged OpenClaw inference gate
|
||||
|
||||
scenario:
|
||||
id: docker-crestodian-first-run
|
||||
id: docker-system-agent-first-run
|
||||
surface: docker-podman-hosting
|
||||
category: docker-podman-hosting.container-setup
|
||||
coverage:
|
||||
secondary:
|
||||
- docker.first-run-onboarding
|
||||
- raspberry-pi.first-run-verification
|
||||
objective: Verify a fresh package-installed Docker state gates Crestodian on a tested inference route, then supports planner-backed and typed setup operations without leaking secrets.
|
||||
objective: Verify a fresh package-installed Docker state gates OpenClaw on a tested inference route, then supports planner-backed and typed setup operations without leaking secrets.
|
||||
successCriteria:
|
||||
- The bare-command routing policy selects onboarding, and the packaged Crestodian CLI exits with onboarding guidance while inference is unavailable.
|
||||
- The bare-command routing policy selects onboarding, and the packaged OpenClaw CLI exits with onboarding guidance while inference is unavailable.
|
||||
- The packaged activation module tests fake Claude before persisting its model, while workspace and Gateway setup remain untouched.
|
||||
- The modern compatibility entrypoint becomes available after activation.
|
||||
- A fuzzy request uses the verified planner for typed setup; subsequent one-shot commands write the default workspace/model and create the configured agent.
|
||||
- Discord setup is written through a SecretRef without persisting the raw token.
|
||||
- Config validation and expected Crestodian audit entries succeed.
|
||||
- Config validation and expected OpenClaw audit entries succeed.
|
||||
docsRefs:
|
||||
- docs/install/docker.md
|
||||
- docs/cli/crestodian.md
|
||||
- docs/cli/setup.md
|
||||
- docs/help/testing.md
|
||||
codeRefs:
|
||||
- scripts/e2e/crestodian-first-run-docker.sh
|
||||
- test/e2e/qa-lab/runtime/crestodian-first-run-docker-client.ts
|
||||
- scripts/e2e/crestodian-first-run-spec.json
|
||||
- scripts/e2e/system-agent-first-run-docker.sh
|
||||
- test/e2e/qa-lab/runtime/system-agent-first-run-docker-client.ts
|
||||
- scripts/e2e/system-agent-first-run-spec.json
|
||||
execution:
|
||||
kind: script
|
||||
path: test/e2e/qa-lab/runtime/docker-e2e-lane.ts
|
||||
summary: Runs supporting packaged-CLI evidence for the inference gate, activation module, planner, and typed operations; it does not claim an interactive onboarding or Crestodian agent/tool/approval flow.
|
||||
summary: Runs supporting packaged-CLI evidence for the inference gate, activation module, planner, and typed operations; it does not claim an interactive onboarding or OpenClaw agent/tool/approval flow.
|
||||
args:
|
||||
- --lane
|
||||
- crestodian-first-run
|
||||
- system-agent-first-run
|
||||
@@ -92,7 +92,7 @@ const legacyStorePatterns = [
|
||||
/\b(?:credentials\/oauth|github-copilot\.token|openrouter-models|auth-profiles|auth-state|exec-approvals|workspace-state)\.json\b/u,
|
||||
/\bcron\/(?:runs\/[^"'`]+\.jsonl|jobs\.json|jobs-state\.json)\b/u,
|
||||
/\b(?:process-leases|session-toggles|known-users|msteams-conversations|msteams-polls|msteams-sso-tokens|bot-storage|sync-store|thread-bindings|inbound-dedupe|startup-verification|storage-meta|crypto-idb-snapshot|command-deploy-cache|plugin-binding-approvals|plugins\/installs|config-health|port-guard|restart-sentinel|gateway-restart-intent|gateway-supervisor-restart-handoff)\.json\b/u,
|
||||
/\b(?:calls|ref-index|audit\/file-transfer|audit\/crestodian)\.jsonl\b/u,
|
||||
/\b(?:calls|ref-index|audit\/file-transfer|audit\/openclaw)\.jsonl\b/u,
|
||||
/\b(?:reply-cache|sent-echoes|events|claims)\.jsonl\b/u,
|
||||
/\bplugin-state\/state\.sqlite\b/u,
|
||||
/\btasks\/(?:runs\.sqlite|flows\/registry\.sqlite)\b/u,
|
||||
|
||||
@@ -335,10 +335,12 @@ export const KNIP_UNUSED_EXPORT_BASELINE = [
|
||||
"src/agents/mcp-ui-resource.ts: testing",
|
||||
"src/agents/media-generation-task-status-shared.ts: resetRecentMediaGenerationDuplicateGuardsForTests",
|
||||
"src/agents/model-fallback-observation.ts: resetModelFallbackDecisionLogCoalescingForTest",
|
||||
"src/agents/model-fallback.ts: FallbackSummaryError",
|
||||
"src/agents/model-fallback.ts: testing",
|
||||
"src/agents/model-suppression.ts: clearModelSuppressionResolverCacheForTest",
|
||||
"src/agents/models-config-state.ts: resetModelsJsonReadyCacheForTest",
|
||||
"src/agents/models-config.plan.ts: planOpenClawModelsJsonWithDeps",
|
||||
"src/agents/models-config.plan.ts: ResolveImplicitProvidersForModelsJson",
|
||||
"src/agents/models-config.plan.ts: resolveProvidersForModelsJsonWithDeps",
|
||||
"src/agents/models-config.providers.implicit.ts: resolvePluginMetadataProviderOwnersForTest",
|
||||
"src/agents/models-config.providers.implicit.ts: resolveProviderDiscoveryFilterForTest",
|
||||
@@ -354,10 +356,22 @@ export const KNIP_UNUSED_EXPORT_BASELINE = [
|
||||
"src/agents/session-suspension.ts: testing",
|
||||
"src/agents/session-write-lock.ts: resetSessionWriteLockStateForTest",
|
||||
"src/agents/session-write-lock.ts: testing",
|
||||
"src/agents/sessions/keybindings.ts: AppKeybindings",
|
||||
"src/agents/sessions/keybindings.ts: Keybinding",
|
||||
"src/agents/sessions/keybindings.ts: KEYBINDINGS",
|
||||
"src/agents/sessions/keybindings.ts: KeyId",
|
||||
"src/agents/sessions/prompt-templates.ts: parseCommandArgs",
|
||||
"src/agents/sessions/prompt-templates.ts: substituteArgs",
|
||||
"src/agents/sessions/tools/bash.ts: resolveBashTimeoutMs",
|
||||
"src/agents/shell-snapshot.ts: resetShellSnapshotCacheForTests",
|
||||
"src/agents/shell-snapshot.ts: resolveShellSnapshotDir",
|
||||
"src/agents/shell-utils.ts: getPosixShellArgs",
|
||||
"src/agents/shell-utils.ts: resolvePowerShellPath",
|
||||
"src/agents/shell-utils.ts: resolveShellFromPath",
|
||||
"src/agents/shell-utils.ts: resolveShellFromWhich",
|
||||
"src/agents/shell-utils.ts: resolveWindowsBashPath",
|
||||
"src/agents/subagent-announce-delivery.ts: testing",
|
||||
"src/agents/subagent-announce-dispatch.ts: mapSteerOutcomeToDeliveryResult",
|
||||
"src/agents/subagent-announce-output.ts: testing",
|
||||
"src/agents/subagent-attachments.ts: decodeStrictBase64",
|
||||
"src/agents/subagent-control.ts: testing",
|
||||
@@ -381,6 +395,11 @@ export const KNIP_UNUSED_EXPORT_BASELINE = [
|
||||
"src/agents/subagent-registry.ts: testing",
|
||||
"src/agents/subagent-spawn.ts: testing",
|
||||
"src/agents/system-prompt-config.ts: resolveAgentSystemPromptConfig",
|
||||
"src/agents/tool-loop-detection.ts: CRITICAL_THRESHOLD",
|
||||
"src/agents/tool-loop-detection.ts: GLOBAL_CIRCUIT_BREAKER_THRESHOLD",
|
||||
"src/agents/tool-loop-detection.ts: hashToolCall",
|
||||
"src/agents/tool-loop-detection.ts: TOOL_CALL_HISTORY_SIZE",
|
||||
"src/agents/tool-loop-detection.ts: WARNING_THRESHOLD",
|
||||
"src/agents/tool-policy-pipeline.ts: resetToolPolicyWarningCacheForTest",
|
||||
"src/agents/tool-search.ts: testing",
|
||||
"src/agents/tool-search.ts: ToolSearchCatalogSession",
|
||||
@@ -389,6 +408,7 @@ export const KNIP_UNUSED_EXPORT_BASELINE = [
|
||||
"src/agents/tools/image-tool.ts: resolveImageModelConfigForTool",
|
||||
"src/agents/tools/image-tool.ts: testing",
|
||||
"src/agents/tools/model-config.helpers.ts: hasDirectProviderApiKeyAuthForTool",
|
||||
"src/agents/tools/sessions-resolution.ts: looksLikeSessionKey",
|
||||
"src/agents/tools/sessions-resolution.ts: testing",
|
||||
"src/agents/tools/sessions-send-tool.a2a.ts: testing",
|
||||
"src/agents/tools/video-generate-tool.ts: resolveVideoGenerationModelConfigForTool",
|
||||
@@ -407,21 +427,45 @@ export const KNIP_UNUSED_EXPORT_BASELINE = [
|
||||
"src/auto-reply/reply/acp-reset-target.ts: testing",
|
||||
"src/auto-reply/reply/agent-runner-execution.ts: buildContextOverflowRecoveryText",
|
||||
"src/auto-reply/reply/agent-runner-execution.ts: computeContextAwareReserveTokensFloor",
|
||||
"src/auto-reply/reply/agent-runner-execution.ts: MAX_LIVE_SWITCH_RETRIES",
|
||||
"src/auto-reply/reply/agent-runner-memory.ts: setAgentRunnerMemoryTestDeps",
|
||||
"src/auto-reply/reply/agent-runner-session-reset.ts: setAgentRunnerSessionResetTestDeps",
|
||||
"src/auto-reply/reply/commands-login.ts: testing",
|
||||
"src/auto-reply/reply/dispatch-from-config.ts: getDispatcherFinalOutcomeCounts",
|
||||
"src/auto-reply/reply/dispatch-from-config.ts: testing",
|
||||
"src/auto-reply/reply/get-reply-directives-apply.ts: formatModelOverrideResetEvent",
|
||||
"src/auto-reply/reply/get-reply-fast-path.ts: markCompleteReplyConfig",
|
||||
"src/auto-reply/reply/get-reply-fast-path.ts: withFastReplyConfig",
|
||||
"src/auto-reply/reply/get-reply-run.ts: buildExecOverridePromptHint",
|
||||
"src/auto-reply/reply/get-reply-run.ts: resolvePromptSessionContextForSystemEvent",
|
||||
"src/auto-reply/reply/get-reply-run.ts: resolvePromptSilentReplyConversationType",
|
||||
"src/auto-reply/reply/inbound-dedupe.ts: buildInboundDedupeKey",
|
||||
"src/auto-reply/reply/progress-narrator.ts: createProgressNarrator",
|
||||
"src/auto-reply/reply/prompt-prelude.ts: buildReplyPromptBodies",
|
||||
"src/auto-reply/reply/queue/cleanup.ts: testing",
|
||||
"src/auto-reply/reply/queue/drain.ts: resolveFollowupAuthorizationKey",
|
||||
"src/auto-reply/reply/queue/enqueue.ts: resetRecentQueuedMessageIdDedupe",
|
||||
"src/auto-reply/reply/reply-run-registry.ts: testing",
|
||||
"src/auto-reply/reply/stage-sandbox-media.ts: appendScpStderrTail",
|
||||
"src/auto-reply/reply/stage-sandbox-media.ts: SCP_STDERR_TAIL_CHARS",
|
||||
"src/auto-reply/reply/stage-sandbox-media.ts: testing",
|
||||
"src/auto-reply/usage-bar/template.ts: clearUsageBarTemplateCacheForTest",
|
||||
"src/cli/channel-options.ts: testing",
|
||||
"src/cli/command-path-policy.ts: resolveCliCatalogCommandPath",
|
||||
"src/cli/command-secret-gateway.ts: testing",
|
||||
"src/cli/cron-cli/register.cron-simple.ts: loadCronJobForShow",
|
||||
"src/cli/daemon-cli/response.ts: buildDaemonHintItems",
|
||||
"src/cli/gateway-cli/run.ts: testing",
|
||||
"src/cli/nodes-camera.ts: CameraClipTarget",
|
||||
"src/cli/nodes-camera.ts: CameraSnapTarget",
|
||||
"src/cli/nodes-camera.ts: writeUrlToFile",
|
||||
"src/cli/plugins-install-command.ts: loadConfigForInstall",
|
||||
"src/cli/plugins-list-format.ts: formatPluginLine",
|
||||
"src/cli/ports.ts: listPortListeners",
|
||||
"src/cli/ports.ts: parseLsofOutput",
|
||||
"src/cli/ports.ts: probePortFree",
|
||||
"src/cli/startup-metadata.ts: testing",
|
||||
"src/cli/update-cli/progress.ts: inferUpdateFailureHints",
|
||||
"src/cli/update-cli/update-command-plugins.ts: buildInvalidConfigPostCoreUpdateResult",
|
||||
"src/cli/update-cli/update-command-plugins.ts: collectMissingPluginInstallPayloads",
|
||||
"src/cli/update-cli/update-command-plugins.ts: resolvePostSyncPluginUpdateSkipIds",
|
||||
@@ -467,20 +511,40 @@ export const KNIP_UNUSED_EXPORT_BASELINE = [
|
||||
"src/context-engine/registry.ts: ContextEngineRegistrationResult",
|
||||
"src/context-engine/registry.ts: getContextEngineFactory",
|
||||
"src/context-engine/registry.ts: listContextEngineIds",
|
||||
"src/crestodian/agent-turn.ts: CrestodianAgentTurnDeps",
|
||||
"src/crestodian/agent-turn.ts: CrestodianAgentTurnDirective",
|
||||
"src/crestodian/agent-turn.ts: runCrestodianAgentTurnWithDeps",
|
||||
"src/system-agent/agent-turn.ts: SystemAgentTurnDeps",
|
||||
"src/system-agent/agent-turn.ts: SystemAgentTurnDirective",
|
||||
"src/system-agent/agent-turn.ts: runSystemAgentTurnWithDeps",
|
||||
"src/cron/command-output-summary.ts: cronCommandSummaryNeedsExternalRedaction",
|
||||
"src/cron/delivery-context.ts: cronDeliveryFromContext",
|
||||
"src/cron/delivery-preview.ts: resolveCronDeliveryPreview",
|
||||
"src/cron/isolated-agent.ts: RunCronAgentTurnResult",
|
||||
"src/cron/isolated-agent/delivery-dispatch.ts: getCompletedDirectCronDeliveriesCountForTests",
|
||||
"src/cron/isolated-agent/delivery-dispatch.ts: resetCompletedDirectCronDeliveriesForTests",
|
||||
"src/cron/isolated-agent/helpers.ts: pickDeliverablePayloads",
|
||||
"src/cron/isolated-agent/helpers.ts: pickLastDeliverablePayload",
|
||||
"src/cron/isolated-agent/helpers.ts: pickSummaryFromPayloads",
|
||||
"src/cron/isolated-agent/run-executor.ts: createCronPromptExecutor",
|
||||
"src/cron/isolated-agent/run.ts: resolveCronDeliveryContext",
|
||||
"src/cron/isolated-agent/run.ts: RunCronAgentTurnResult",
|
||||
"src/cron/job-session-bindings.ts: CronJobSessionBinding",
|
||||
"src/cron/run-diagnostics.ts: MISSING_WEB_SEARCH_PROVIDER_DIAGNOSTIC_MESSAGE",
|
||||
"src/cron/schedule.ts: clearCronScheduleCacheForTest",
|
||||
"src/cron/schedule.ts: getCronScheduleCacheMaxForTest",
|
||||
"src/cron/schedule.ts: getCronScheduleCacheSizeForTest",
|
||||
"src/cron/schedule.ts: hasCronInCacheForTest",
|
||||
"src/cron/service/active-run-cancellation.ts: cancelActiveCronTaskRun",
|
||||
"src/cron/service/active-run-cancellation.ts: CRON_TASK_RUN_SETTLEMENT_TRACKING_MAX_MS",
|
||||
"src/cron/service/active-run-cancellation.ts: resetActiveCronTaskRunsForTests",
|
||||
"src/cron/service/timeout-policy.ts: AGENT_TURN_SAFETY_TIMEOUT_MS",
|
||||
"src/cron/service/timeout-policy.ts: DEFAULT_JOB_TIMEOUT_MS",
|
||||
"src/cron/service/timer.ts: executeJobCore",
|
||||
"src/cron/service/timer.ts: onTimer",
|
||||
"src/cron/session-reaper.ts: resetReaperThrottle",
|
||||
"src/cron/session-reaper.ts: resolveRetentionMs",
|
||||
"src/cron/stagger.ts: isRecurringTopOfHourCronExpr",
|
||||
"src/cron/store/row-codec.ts: bindScheduleColumns",
|
||||
"src/cron/store/row-codec.ts: scheduleFromRow",
|
||||
"src/cron/task-run-history.ts: ReadCronTaskRunHistoryPageOptions",
|
||||
"src/entry.compile-cache.ts: buildOpenClawCompileCacheRespawnPlan",
|
||||
"src/entry.compile-cache.ts: isNodeVersionAffectedByCompileCacheDeadlock",
|
||||
"src/entry.compile-cache.ts: isSourceCheckoutInstallRoot",
|
||||
@@ -509,10 +573,10 @@ export const KNIP_UNUSED_EXPORT_BASELINE = [
|
||||
"src/mcp/channel-bridge.ts: shouldRetryInitialMcpGatewayConnect",
|
||||
"src/mcp/channel-server.ts: createOpenClawChannelMcpServer",
|
||||
"src/mcp/channel-server.ts: OpenClawChannelBridge",
|
||||
"src/mcp/openclaw-tools-serve-config.ts: OPENCLAW_TOOLS_MCP_CRESTODIAN_SURFACE_ENV",
|
||||
"src/mcp/openclaw-tools-serve-config.ts: OPENCLAW_TOOLS_MCP_SYSTEM_AGENT_SURFACE_ENV",
|
||||
"src/mcp/openclaw-tools-serve-config.ts: OpenClawToolsMcpToolId",
|
||||
"src/mcp/openclaw-tools-serve-config.ts: resolveOpenClawToolsMcpCrestodianApproval",
|
||||
"src/mcp/openclaw-tools-serve-config.ts: resolveOpenClawToolsMcpCrestodianSurface",
|
||||
"src/mcp/openclaw-tools-serve-config.ts: resolveOpenClawToolsMcpSystemAgentApproval",
|
||||
"src/mcp/openclaw-tools-serve-config.ts: resolveOpenClawToolsMcpSystemAgentSurface",
|
||||
"src/mcp/openclaw-tools-serve-config.ts: resolveOpenClawToolsMcpToolSelection",
|
||||
"src/media/input-files.ts: fetchWithGuard",
|
||||
"src/media/parse.ts: SplitMediaFromOutputOptions",
|
||||
|
||||
+9
-9
@@ -1,13 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
# Runs the Crestodian first-run Docker smoke against the package-installed
|
||||
# Runs the OpenClaw first-run Docker smoke against the package-installed
|
||||
# functional E2E image, with only the test harness mounted from the checkout.
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh"
|
||||
IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-crestodian-first-run-e2e" OPENCLAW_CRESTODIAN_FIRST_RUN_E2E_IMAGE)"
|
||||
CONTAINER_NAME="openclaw-crestodian-first-run-e2e-$$"
|
||||
RUN_LOG="$(mktemp -t openclaw-crestodian-first-run-log.XXXXXX)"
|
||||
IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-system-agent-first-run-e2e" OPENCLAW_SYSTEM_AGENT_FIRST_RUN_E2E_IMAGE)"
|
||||
CONTAINER_NAME="openclaw-system-agent-first-run-e2e-$$"
|
||||
RUN_LOG="$(mktemp -t openclaw-system-agent-first-run-log.XXXXXX)"
|
||||
|
||||
cleanup() {
|
||||
docker_e2e_docker_cmd rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
@@ -15,10 +15,10 @@ cleanup() {
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
docker_e2e_build_or_reuse "$IMAGE_NAME" crestodian-first-run
|
||||
OPENCLAW_TEST_STATE_SCRIPT_B64="$(docker_e2e_test_state_shell_b64 crestodian-first-run empty)"
|
||||
docker_e2e_build_or_reuse "$IMAGE_NAME" system-agent-first-run
|
||||
OPENCLAW_TEST_STATE_SCRIPT_B64="$(docker_e2e_test_state_shell_b64 system-agent-first-run empty)"
|
||||
|
||||
echo "Running in-container Crestodian first-run smoke..."
|
||||
echo "Running in-container OpenClaw first-run smoke..."
|
||||
# Harness files are mounted read-only; the app under test comes from /app/dist.
|
||||
set +e
|
||||
docker_e2e_run_with_harness \
|
||||
@@ -28,13 +28,13 @@ docker_e2e_run_with_harness \
|
||||
bash -lc "set -euo pipefail
|
||||
source scripts/lib/openclaw-e2e-instance.sh
|
||||
openclaw_e2e_eval_test_state_from_b64 \"\${OPENCLAW_TEST_STATE_SCRIPT_B64:?missing OPENCLAW_TEST_STATE_SCRIPT_B64}\"
|
||||
tsx test/e2e/qa-lab/runtime/crestodian-first-run-docker-client.ts
|
||||
tsx test/e2e/qa-lab/runtime/system-agent-first-run-docker-client.ts
|
||||
" >"$RUN_LOG" 2>&1
|
||||
status=${PIPESTATUS[0]}
|
||||
set -e
|
||||
|
||||
if [ "$status" -ne 0 ]; then
|
||||
echo "Docker Crestodian first-run smoke failed"
|
||||
echo "Docker OpenClaw first-run smoke failed"
|
||||
docker_e2e_print_log "$RUN_LOG"
|
||||
exit "$status"
|
||||
fi
|
||||
+11
-11
@@ -1,43 +1,43 @@
|
||||
{
|
||||
"stateDirName": "crestodian-ring-zero-state",
|
||||
"defaultWorkspaceName": "crestodian-main-workspace",
|
||||
"agentWorkspaceName": "crestodian-reef-workspace",
|
||||
"dockerDefaultWorkspace": "/tmp/openclaw-first-run",
|
||||
"stateDirName": "system-agent-ring-zero-state",
|
||||
"defaultWorkspaceName": "openclaw-main-workspace",
|
||||
"agentWorkspaceName": "openclaw-reef-workspace",
|
||||
"dockerDefaultWorkspace": "/tmp/system-agent-first-run",
|
||||
"dockerAgentWorkspace": "/tmp/openclaw-reef",
|
||||
"agentId": "reef",
|
||||
"model": "claude-cli/claude-opus-4-8",
|
||||
"telegramEnv": "TELEGRAM_BOT_TOKEN",
|
||||
"telegramToken": "123456:openclaw-crestodian-telegram-e2e-token",
|
||||
"telegramToken": "123456:openclaw-openclaw-telegram-e2e-token",
|
||||
"commands": [
|
||||
{
|
||||
"id": "setup",
|
||||
"message": "please finish basic setup with workspace {defaultWorkspace}",
|
||||
"expectOutput": "[crestodian] done: crestodian.setup",
|
||||
"expectOutput": "[openclaw] done: openclaw.setup",
|
||||
"approve": true,
|
||||
"planner": true
|
||||
},
|
||||
{
|
||||
"id": "default-model",
|
||||
"message": "set default model {model}",
|
||||
"expectOutput": "[crestodian] done: config.setDefaultModel",
|
||||
"expectOutput": "[openclaw] done: config.setDefaultModel",
|
||||
"approve": true
|
||||
},
|
||||
{
|
||||
"id": "agent",
|
||||
"message": "create agent {agentId} workspace {agentWorkspace}",
|
||||
"expectOutput": "[crestodian] done: agents.create",
|
||||
"expectOutput": "[openclaw] done: agents.create",
|
||||
"approve": true
|
||||
},
|
||||
{
|
||||
"id": "telegram-token",
|
||||
"message": "config set-ref channels.telegram.botToken env {telegramEnv}",
|
||||
"expectOutput": "[crestodian] done: config.setRef",
|
||||
"expectOutput": "[openclaw] done: config.setRef",
|
||||
"approve": true
|
||||
},
|
||||
{
|
||||
"id": "telegram-enabled",
|
||||
"message": "config set channels.telegram.enabled true",
|
||||
"expectOutput": "[crestodian] done: config.set",
|
||||
"expectOutput": "[openclaw] done: config.set",
|
||||
"approve": true
|
||||
},
|
||||
{
|
||||
@@ -48,7 +48,7 @@
|
||||
}
|
||||
],
|
||||
"auditOperations": [
|
||||
"crestodian.setup",
|
||||
"openclaw.setup",
|
||||
"config.setDefaultModel",
|
||||
"agents.create",
|
||||
"config.setRef",
|
||||
+47
-47
@@ -1,15 +1,15 @@
|
||||
// Crestodian rescue-message Docker harness.
|
||||
// OpenClaw rescue-message Docker harness.
|
||||
// Imports packaged dist modules so the Docker lane verifies the npm tarball,
|
||||
// while this small test driver stays mounted from the checkout.
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { handleCrestodianCommand } from "../../dist/auto-reply/reply/commands-crestodian.js";
|
||||
import { handleSystemAgentCommand } from "../../dist/auto-reply/reply/commands-system-agent.js";
|
||||
import { clearConfigCache } from "../../dist/config/config.js";
|
||||
import type { OpenClawConfig } from "../../dist/config/types.openclaw.js";
|
||||
import { runCrestodianRescueMessage } from "../../dist/crestodian/rescue-message.js";
|
||||
import { runSystemAgentRescueMessage } from "../../dist/system-agent/rescue-message.js";
|
||||
import { createE2eStateDir } from "./lib/temp-state-dir.ts";
|
||||
|
||||
type CommandResult = Awaited<ReturnType<typeof handleCrestodianCommand>>;
|
||||
type CommandResult = Awaited<ReturnType<typeof handleSystemAgentCommand>>;
|
||||
|
||||
function assert(condition: unknown, message: string): asserts condition {
|
||||
if (!condition) {
|
||||
@@ -35,11 +35,11 @@ function makeParams(commandBody: string, cfg: OpenClawConfig, isGroup = false) {
|
||||
},
|
||||
agentId: "default",
|
||||
isGroup,
|
||||
} as Parameters<typeof handleCrestodianCommand>[0];
|
||||
} as Parameters<typeof handleSystemAgentCommand>[0];
|
||||
}
|
||||
|
||||
async function invoke(commandBody: string, cfg: OpenClawConfig, isGroup = false): Promise<string> {
|
||||
const result: CommandResult = await handleCrestodianCommand(
|
||||
const result: CommandResult = await handleSystemAgentCommand(
|
||||
makeParams(commandBody, cfg, isGroup),
|
||||
true,
|
||||
);
|
||||
@@ -51,7 +51,7 @@ async function invoke(commandBody: string, cfg: OpenClawConfig, isGroup = false)
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const tempState = await createE2eStateDir("openclaw-crestodian-");
|
||||
const tempState = await createE2eStateDir("openclaw-openclaw-");
|
||||
tempState.registerExitCleanup();
|
||||
const stateDir = tempState.stateDir;
|
||||
const configPath = process.env.OPENCLAW_CONFIG_PATH ?? path.join(stateDir, "openclaw.json");
|
||||
@@ -71,71 +71,71 @@ async function main() {
|
||||
);
|
||||
clearConfigCache();
|
||||
|
||||
const denied = await invoke("/crestodian status", {
|
||||
crestodian: { rescue: { enabled: true } },
|
||||
const denied = await invoke("/openclaw status", {
|
||||
systemAgent: { rescue: { enabled: true } },
|
||||
agents: { defaults: { sandbox: { mode: "all" } } },
|
||||
});
|
||||
assert(denied.includes("sandboxing is active"), "sandboxed rescue was not denied");
|
||||
|
||||
const cfg: OpenClawConfig = {};
|
||||
const refusedTui = await invoke("/crestodian talk to agent", cfg);
|
||||
const refusedTui = await invoke("/openclaw talk to agent", cfg);
|
||||
assert(
|
||||
refusedTui.includes("cannot open the local TUI"),
|
||||
"remote rescue TUI handoff was not refused",
|
||||
);
|
||||
|
||||
const plan = await invoke("/crestodian set default model openai/gpt-5.2", cfg);
|
||||
const plan = await invoke("/openclaw set default model openai/gpt-5.2", cfg);
|
||||
assert(
|
||||
plan.includes("Reply /crestodian yes to apply"),
|
||||
plan.includes("Reply /openclaw yes to apply"),
|
||||
"persistent change did not require approval",
|
||||
);
|
||||
const applied = await invoke("/crestodian yes", cfg);
|
||||
const applied = await invoke("/openclaw yes", cfg);
|
||||
assert(applied.includes("Default model: openai/gpt-5.2"), "approved change did not apply");
|
||||
|
||||
const configValid = await invoke("/crestodian validate config", cfg);
|
||||
const configValid = await invoke("/openclaw validate config", cfg);
|
||||
assert(configValid.includes("Config valid:"), "config validation did not report valid config");
|
||||
|
||||
const configSetPlan = await invoke("/crestodian config set gateway.port 19001", cfg);
|
||||
const configSetPlan = await invoke("/openclaw config set gateway.port 19001", cfg);
|
||||
assert(
|
||||
configSetPlan.includes("Reply /crestodian yes to apply"),
|
||||
configSetPlan.includes("Reply /openclaw yes to apply"),
|
||||
"generic config set did not require approval",
|
||||
);
|
||||
const configSetApplied = await invoke("/crestodian yes", cfg);
|
||||
assert(configSetApplied.includes("[crestodian] done: config.set"), "generic config set failed");
|
||||
const configSetApplied = await invoke("/openclaw yes", cfg);
|
||||
assert(configSetApplied.includes("[openclaw] done: config.set"), "generic config set failed");
|
||||
|
||||
const refPlan = await invoke(
|
||||
"/crestodian config set-ref gateway.auth.token env OPENCLAW_GATEWAY_TOKEN",
|
||||
"/openclaw config set-ref gateway.auth.token env OPENCLAW_GATEWAY_TOKEN",
|
||||
cfg,
|
||||
);
|
||||
assert(
|
||||
refPlan.includes("Reply /crestodian yes to apply"),
|
||||
refPlan.includes("Reply /openclaw yes to apply"),
|
||||
"SecretRef set did not require approval",
|
||||
);
|
||||
const refApplied = await invoke("/crestodian yes", cfg);
|
||||
assert(refApplied.includes("[crestodian] done: config.setRef"), "SecretRef set failed");
|
||||
const refApplied = await invoke("/openclaw yes", cfg);
|
||||
assert(refApplied.includes("[openclaw] done: config.setRef"), "SecretRef set failed");
|
||||
|
||||
const agentPlan = await invoke("/crestodian create agent work workspace /tmp/openclaw-work", cfg);
|
||||
const agentPlan = await invoke("/openclaw create agent work workspace /tmp/openclaw-work", cfg);
|
||||
assert(
|
||||
agentPlan.includes("Reply /crestodian yes to apply"),
|
||||
agentPlan.includes("Reply /openclaw yes to apply"),
|
||||
"agent creation did not require approval",
|
||||
);
|
||||
const agentApplied = await invoke("/crestodian yes", cfg);
|
||||
assert(agentApplied.includes("[crestodian] done: agents.create"), "agent creation did not apply");
|
||||
const agentApplied = await invoke("/openclaw yes", cfg);
|
||||
assert(agentApplied.includes("[openclaw] done: agents.create"), "agent creation did not apply");
|
||||
|
||||
const setupPlan = await invoke(
|
||||
"/crestodian setup workspace /tmp/openclaw-setup model openai/gpt-5.2",
|
||||
"/openclaw setup workspace /tmp/openclaw-setup model openai/gpt-5.2",
|
||||
cfg,
|
||||
);
|
||||
assert(setupPlan.includes("Reply /crestodian yes to apply"), "setup did not require approval");
|
||||
const setupApplied = await invoke("/crestodian yes", cfg);
|
||||
assert(setupApplied.includes("[crestodian] done: crestodian.setup"), "setup did not apply");
|
||||
assert(setupPlan.includes("Reply /openclaw yes to apply"), "setup did not require approval");
|
||||
const setupApplied = await invoke("/openclaw yes", cfg);
|
||||
assert(setupApplied.includes("[openclaw] done: openclaw.setup"), "setup did not apply");
|
||||
|
||||
const gatewayRestarts: string[] = [];
|
||||
const gatewayCommand = makeParams("/crestodian restart gateway", cfg).command;
|
||||
const gatewayPlan = await runCrestodianRescueMessage({
|
||||
const gatewayCommand = makeParams("/openclaw restart gateway", cfg).command;
|
||||
const gatewayPlan = await runSystemAgentRescueMessage({
|
||||
cfg,
|
||||
command: gatewayCommand,
|
||||
commandBody: "/crestodian restart gateway",
|
||||
commandBody: "/openclaw restart gateway",
|
||||
agentId: "default",
|
||||
isGroup: false,
|
||||
deps: {
|
||||
@@ -145,13 +145,13 @@ async function main() {
|
||||
},
|
||||
});
|
||||
assert(
|
||||
gatewayPlan?.includes("Reply /crestodian yes to apply"),
|
||||
gatewayPlan?.includes("Reply /openclaw yes to apply"),
|
||||
"gateway restart did not require approval",
|
||||
);
|
||||
const gatewayApplied = await runCrestodianRescueMessage({
|
||||
const gatewayApplied = await runSystemAgentRescueMessage({
|
||||
cfg,
|
||||
command: gatewayCommand,
|
||||
commandBody: "/crestodian yes",
|
||||
commandBody: "/openclaw yes",
|
||||
agentId: "default",
|
||||
isGroup: false,
|
||||
deps: {
|
||||
@@ -161,17 +161,17 @@ async function main() {
|
||||
},
|
||||
});
|
||||
assert(
|
||||
gatewayApplied?.includes("[crestodian] done: gateway.restart"),
|
||||
gatewayApplied?.includes("[openclaw] done: gateway.restart"),
|
||||
"gateway restart did not apply",
|
||||
);
|
||||
assert(gatewayRestarts.length === 1, "gateway restart dependency was not invoked once");
|
||||
|
||||
const doctorRuns: string[] = [];
|
||||
const doctorCommand = makeParams("/crestodian doctor fix", cfg).command;
|
||||
const doctorPlan = await runCrestodianRescueMessage({
|
||||
const doctorCommand = makeParams("/openclaw doctor fix", cfg).command;
|
||||
const doctorPlan = await runSystemAgentRescueMessage({
|
||||
cfg,
|
||||
command: doctorCommand,
|
||||
commandBody: "/crestodian doctor fix",
|
||||
commandBody: "/openclaw doctor fix",
|
||||
agentId: "default",
|
||||
isGroup: false,
|
||||
deps: {
|
||||
@@ -181,13 +181,13 @@ async function main() {
|
||||
},
|
||||
});
|
||||
assert(
|
||||
doctorPlan?.includes("Reply /crestodian yes to apply"),
|
||||
doctorPlan?.includes("Reply /openclaw yes to apply"),
|
||||
"doctor fix did not require approval",
|
||||
);
|
||||
const doctorApplied = await runCrestodianRescueMessage({
|
||||
const doctorApplied = await runSystemAgentRescueMessage({
|
||||
cfg,
|
||||
command: doctorCommand,
|
||||
commandBody: "/crestodian yes",
|
||||
commandBody: "/openclaw yes",
|
||||
agentId: "default",
|
||||
isGroup: false,
|
||||
deps: {
|
||||
@@ -196,7 +196,7 @@ async function main() {
|
||||
},
|
||||
},
|
||||
});
|
||||
assert(doctorApplied?.includes("[crestodian] done: doctor.fix"), "doctor fix did not apply");
|
||||
assert(doctorApplied?.includes("[openclaw] done: doctor.fix"), "doctor fix did not apply");
|
||||
assert(doctorRuns.join(",") === "repair", "doctor repair dependency was not invoked once");
|
||||
|
||||
const updatedConfig = JSON.parse(await fs.readFile(configPath, "utf8")) as OpenClawConfig;
|
||||
@@ -226,7 +226,7 @@ async function main() {
|
||||
"agent config was not updated",
|
||||
);
|
||||
|
||||
const auditPath = path.join(stateDir, "audit", "crestodian.jsonl");
|
||||
const auditPath = path.join(stateDir, "audit", "system-agent.jsonl");
|
||||
const auditLines = (await fs.readFile(auditPath, "utf8")).trim().split("\n");
|
||||
assert(auditLines.length >= 2, "audit log did not record both operations");
|
||||
const audits = auditLines.map((line) => JSON.parse(line));
|
||||
@@ -243,7 +243,7 @@ async function main() {
|
||||
"SecretRef config audit missing",
|
||||
);
|
||||
assert(
|
||||
audits.some((audit) => audit.operation === "crestodian.setup"),
|
||||
audits.some((audit) => audit.operation === "openclaw.setup"),
|
||||
"setup audit missing",
|
||||
);
|
||||
const agentAudit = audits.find((audit) => audit.operation === "agents.create");
|
||||
@@ -261,7 +261,7 @@ async function main() {
|
||||
"doctor fix audit missing",
|
||||
);
|
||||
|
||||
console.log("Crestodian rescue Docker E2E passed");
|
||||
console.log("OpenClaw rescue Docker E2E passed");
|
||||
}
|
||||
|
||||
main().catch((err: unknown) => {
|
||||
+10
-10
@@ -1,13 +1,13 @@
|
||||
#!/usr/bin/env bash
|
||||
# Runs the Crestodian rescue-message Docker smoke against the package-installed
|
||||
# Runs the OpenClaw rescue-message Docker smoke against the package-installed
|
||||
# functional E2E image, with only the test harness mounted from the checkout.
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
source "$ROOT_DIR/scripts/lib/docker-e2e-image.sh"
|
||||
IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-crestodian-rescue-e2e" OPENCLAW_CRESTODIAN_RESCUE_E2E_IMAGE)"
|
||||
CONTAINER_NAME="openclaw-crestodian-rescue-e2e-$$"
|
||||
RUN_LOG="$(mktemp -t openclaw-crestodian-rescue-log.XXXXXX)"
|
||||
IMAGE_NAME="$(docker_e2e_resolve_image "openclaw-system-agent-rescue-e2e" OPENCLAW_SYSTEM_AGENT_RESCUE_E2E_IMAGE)"
|
||||
CONTAINER_NAME="openclaw-system-agent-rescue-e2e-$$"
|
||||
RUN_LOG="$(mktemp -t openclaw-system-agent-rescue-log.XXXXXX)"
|
||||
|
||||
cleanup() {
|
||||
docker_e2e_docker_cmd rm -f "$CONTAINER_NAME" >/dev/null 2>&1 || true
|
||||
@@ -15,27 +15,27 @@ cleanup() {
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
docker_e2e_build_or_reuse "$IMAGE_NAME" crestodian-rescue
|
||||
OPENCLAW_TEST_STATE_SCRIPT_B64="$(docker_e2e_test_state_shell_b64 crestodian-rescue empty)"
|
||||
docker_e2e_build_or_reuse "$IMAGE_NAME" system-agent-rescue
|
||||
OPENCLAW_TEST_STATE_SCRIPT_B64="$(docker_e2e_test_state_shell_b64 system-agent-rescue empty)"
|
||||
|
||||
echo "Running in-container Crestodian rescue smoke..."
|
||||
echo "Running in-container OpenClaw rescue smoke..."
|
||||
# Harness files are mounted read-only; the app under test comes from /app/dist.
|
||||
set +e
|
||||
docker_e2e_run_with_harness \
|
||||
--name "$CONTAINER_NAME" \
|
||||
-e "OPENCLAW_TEST_STATE_SCRIPT_B64=$OPENCLAW_TEST_STATE_SCRIPT_B64" \
|
||||
-e "OPENCLAW_GATEWAY_TOKEN=crestodian-rescue-token" \
|
||||
-e "OPENCLAW_GATEWAY_TOKEN=system-agent-rescue-token" \
|
||||
"$IMAGE_NAME" \
|
||||
bash -lc "set -euo pipefail
|
||||
source scripts/lib/openclaw-e2e-instance.sh
|
||||
openclaw_e2e_eval_test_state_from_b64 \"\${OPENCLAW_TEST_STATE_SCRIPT_B64:?missing OPENCLAW_TEST_STATE_SCRIPT_B64}\"
|
||||
tsx scripts/e2e/crestodian-rescue-docker-client.ts
|
||||
tsx scripts/e2e/system-agent-rescue-docker-client.ts
|
||||
" >"$RUN_LOG" 2>&1
|
||||
status=${PIPESTATUS[0]}
|
||||
set -e
|
||||
|
||||
if [ "$status" -ne 0 ]; then
|
||||
echo "Docker Crestodian rescue smoke failed"
|
||||
echo "Docker OpenClaw rescue smoke failed"
|
||||
docker_e2e_print_log "$RUN_LOG"
|
||||
exit "$status"
|
||||
fi
|
||||
@@ -456,7 +456,7 @@ export const mainLanes = [
|
||||
stateScenario: "empty",
|
||||
},
|
||||
),
|
||||
lane("crestodian-rescue", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:crestodian-rescue", {
|
||||
lane("system-agent-rescue", "OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:system-agent-rescue", {
|
||||
stateScenario: "empty",
|
||||
}),
|
||||
serviceLane(
|
||||
@@ -523,8 +523,8 @@ export const mainLanes = [
|
||||
stateScenario: "empty",
|
||||
}),
|
||||
lane(
|
||||
"crestodian-first-run",
|
||||
"OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:crestodian-first-run",
|
||||
"system-agent-first-run",
|
||||
"OPENCLAW_SKIP_DOCKER_BUILD=1 pnpm test:docker:system-agent-first-run",
|
||||
{ stateScenario: "empty" },
|
||||
),
|
||||
lane(
|
||||
|
||||
@@ -30,7 +30,7 @@ const OPTIONAL_LIVE_SHARD_FILE_ENVS = new Map([
|
||||
["src/agents/tools/image-tool.ollama.live.test.ts", ["OPENCLAW_LIVE_OLLAMA_IMAGE"]],
|
||||
["src/agents/tools/image-tool.providers.live.test.ts", ["OPENCLAW_LIVE_IMAGE_TOOL_TEST"]],
|
||||
["src/skills/workshop/experience-review.live.test.ts", ["OPENCLAW_LIVE_SKILL_EXPERIENCE_REVIEW"]],
|
||||
["src/crestodian/rescue-channel.live.test.ts", ["OPENCLAW_LIVE_CRESTODIAN_RESCUE_CHANNEL"]],
|
||||
["src/system-agent/rescue-channel.live.test.ts", ["OPENCLAW_LIVE_SYSTEM_AGENT_RESCUE_CHANNEL"]],
|
||||
["src/gateway/android-node.capabilities.live.test.ts", ["OPENCLAW_LIVE_ANDROID_NODE"]],
|
||||
["src/gateway/gateway-acp-bind.live.test.ts", ["OPENCLAW_LIVE_ACP_BIND"]],
|
||||
["src/gateway/gateway-acp-spawn-defaults.live.test.ts", ["OPENCLAW_LIVE_ACP_SPAWN_DEFAULTS"]],
|
||||
@@ -267,12 +267,12 @@ export function selectLiveShardFiles(shard, files = collectAllLiveTestFiles()) {
|
||||
return files.filter((file) => file === "src/agents/zai.live.test.ts");
|
||||
case "native-live-src-gateway":
|
||||
return files.filter(
|
||||
(file) => file.startsWith("src/gateway/") || file.startsWith("src/crestodian/"),
|
||||
(file) => file.startsWith("src/gateway/") || file.startsWith("src/system-agent/"),
|
||||
);
|
||||
case "native-live-src-gateway-core":
|
||||
return files.filter(
|
||||
(file) =>
|
||||
(file.startsWith("src/gateway/") || file.startsWith("src/crestodian/")) &&
|
||||
(file.startsWith("src/gateway/") || file.startsWith("src/system-agent/")) &&
|
||||
!isGatewayBackendLiveTest(file) &&
|
||||
!isGatewayProfilesLiveTest(file),
|
||||
);
|
||||
|
||||
@@ -855,54 +855,54 @@ const TOOLING_SOURCE_TEST_TARGETS = new Map([
|
||||
["test/scripts/docker-build-helper.test.ts", "test/scripts/docker-e2e-plan.test.ts"],
|
||||
],
|
||||
[
|
||||
"scripts/e2e/crestodian-first-run-docker.sh",
|
||||
"scripts/e2e/system-agent-first-run-docker.sh",
|
||||
[
|
||||
"test/scripts/docker-build-helper.test.ts",
|
||||
"test/scripts/docker-e2e-plan.test.ts",
|
||||
"test/scripts/docker-e2e-crestodian.test.ts",
|
||||
"test/scripts/docker-e2e-system-agent.test.ts",
|
||||
],
|
||||
],
|
||||
[
|
||||
"test/e2e/qa-lab/runtime/crestodian-first-run-docker-client.ts",
|
||||
"test/e2e/qa-lab/runtime/system-agent-first-run-docker-client.ts",
|
||||
[
|
||||
"test/scripts/docker-e2e-crestodian.test.ts",
|
||||
"test/scripts/docker-e2e-system-agent.test.ts",
|
||||
"src/cli/program/register.onboard.test.ts",
|
||||
"src/cli/run-main.test.ts",
|
||||
"src/cli/run-main.exit.test.ts",
|
||||
"src/commands/crestodian-with-inference.test.ts",
|
||||
"src/crestodian/assistant.configured.test.ts",
|
||||
"src/crestodian/assistant.test.ts",
|
||||
"src/crestodian/crestodian.test.ts",
|
||||
"src/crestodian/operations.test.ts",
|
||||
"src/crestodian/overview.test.ts",
|
||||
"src/crestodian/setup-inference.test.ts",
|
||||
"src/crestodian/audit.test.ts",
|
||||
"src/commands/system-agent-with-inference.test.ts",
|
||||
"src/system-agent/assistant.configured.test.ts",
|
||||
"src/system-agent/assistant.test.ts",
|
||||
"src/system-agent/system-agent.test.ts",
|
||||
"src/system-agent/operations.test.ts",
|
||||
"src/system-agent/overview.test.ts",
|
||||
"src/system-agent/setup-inference.test.ts",
|
||||
"src/system-agent/audit.test.ts",
|
||||
],
|
||||
],
|
||||
[
|
||||
"scripts/e2e/crestodian-first-run-spec.json",
|
||||
"scripts/e2e/system-agent-first-run-spec.json",
|
||||
[
|
||||
"test/scripts/docker-e2e-crestodian.test.ts",
|
||||
"src/crestodian/operations.test.ts",
|
||||
"src/crestodian/audit.test.ts",
|
||||
"test/scripts/docker-e2e-system-agent.test.ts",
|
||||
"src/system-agent/operations.test.ts",
|
||||
"src/system-agent/audit.test.ts",
|
||||
],
|
||||
],
|
||||
[
|
||||
"scripts/e2e/crestodian-rescue-docker.sh",
|
||||
"scripts/e2e/system-agent-rescue-docker.sh",
|
||||
[
|
||||
"test/scripts/docker-build-helper.test.ts",
|
||||
"test/scripts/docker-e2e-plan.test.ts",
|
||||
"test/scripts/docker-e2e-crestodian.test.ts",
|
||||
"test/scripts/docker-e2e-system-agent.test.ts",
|
||||
],
|
||||
],
|
||||
[
|
||||
"scripts/e2e/crestodian-rescue-docker-client.ts",
|
||||
"scripts/e2e/system-agent-rescue-docker-client.ts",
|
||||
[
|
||||
"test/scripts/docker-e2e-crestodian.test.ts",
|
||||
"src/crestodian/rescue-policy.test.ts",
|
||||
"src/crestodian/rescue-message.test.ts",
|
||||
"src/crestodian/operations.test.ts",
|
||||
"src/crestodian/audit.test.ts",
|
||||
"test/scripts/docker-e2e-system-agent.test.ts",
|
||||
"src/system-agent/rescue-policy.test.ts",
|
||||
"src/system-agent/rescue-message.test.ts",
|
||||
"src/system-agent/operations.test.ts",
|
||||
"src/system-agent/audit.test.ts",
|
||||
],
|
||||
],
|
||||
[
|
||||
|
||||
@@ -418,14 +418,14 @@ describe("createOpenClawCodingTools", () => {
|
||||
|
||||
it("keeps the injected ring-zero tool under policy and rejects a same-name replacement", () => {
|
||||
const injectedTool = {
|
||||
...stubTool("crestodian"),
|
||||
label: "Crestodian",
|
||||
...stubTool("openclaw"),
|
||||
label: "OpenClaw",
|
||||
description: "trusted ring-zero tool",
|
||||
execute: async () => ({ content: [], details: {} }),
|
||||
};
|
||||
const duplicateTool = {
|
||||
...stubTool("crestodian"),
|
||||
label: "Crestodian",
|
||||
...stubTool("openclaw"),
|
||||
label: "OpenClaw",
|
||||
description: "duplicate plugin tool",
|
||||
execute: async () => ({ content: [], details: {} }),
|
||||
};
|
||||
@@ -433,8 +433,8 @@ describe("createOpenClawCodingTools", () => {
|
||||
|
||||
const tools = runWithAgentRingZeroTools([injectedTool], () =>
|
||||
createOpenClawCodingTools({
|
||||
config: { tools: { allow: ["read"], deny: ["crestodian"] } },
|
||||
runtimeToolAllowlist: ["crestodian"],
|
||||
config: { tools: { allow: ["read"], deny: ["openclaw"] } },
|
||||
runtimeToolAllowlist: ["openclaw"],
|
||||
toolConstructionPlan: {
|
||||
includeBaseCodingTools: false,
|
||||
includeShellTools: false,
|
||||
@@ -446,7 +446,7 @@ describe("createOpenClawCodingTools", () => {
|
||||
);
|
||||
|
||||
expect(tools).toHaveLength(1);
|
||||
expect(tools[0]?.name).toBe("crestodian");
|
||||
expect(tools[0]?.name).toBe("openclaw");
|
||||
expect(tools[0]?.description).toBe("trusted ring-zero tool");
|
||||
});
|
||||
|
||||
|
||||
@@ -424,8 +424,8 @@ type OpenClawCodingToolsOptions = {
|
||||
toolSearchCatalogRef?: ToolSearchCatalogRef;
|
||||
/** Limits which tool families are materialized before the shared policy pipeline runs. */
|
||||
toolConstructionPlan?: OpenClawCodingToolConstructionPlan;
|
||||
/** Ring-zero Crestodian tool; set only by the Crestodian agent runner. */
|
||||
crestodianTool?: import("./tools/crestodian-tool.js").CrestodianToolOptions;
|
||||
/** Ring-zero OpenClaw tool; set only by the OpenClaw agent runner. */
|
||||
systemAgentTool?: import("./tools/system-agent-tool.js").SystemAgentToolOptions;
|
||||
/** Trusted sender identity bit for command/channel-action auth and owner-gated plugin tools. */
|
||||
senderIsOwner?: boolean;
|
||||
/** Auth profiles already loaded for this run; used for prompt-time tool availability. */
|
||||
@@ -928,7 +928,7 @@ function createOpenClawCodingToolsInternal(options?: OpenClawCodingToolsOptions)
|
||||
? mergeAgentRingZeroTools(
|
||||
ringZeroTools,
|
||||
createOpenClawTools({
|
||||
...(options?.crestodianTool ? { crestodianTool: options.crestodianTool } : {}),
|
||||
...(options?.systemAgentTool ? { systemAgentTool: options.systemAgentTool } : {}),
|
||||
sandboxBrowserBridgeUrl: sandbox?.browser?.bridgeUrl,
|
||||
allowHostBrowserControl: sandbox ? sandbox.browserAllowHostControl : true,
|
||||
agentSessionKey: options?.sessionKey,
|
||||
|
||||
@@ -1001,7 +1001,7 @@ describe("resolveCliAuthEpoch", () => {
|
||||
const fingerprint = await resolveCliRuntimeOwnerFingerprint({
|
||||
provider: "claude-cli",
|
||||
config,
|
||||
agentId: "crestodian",
|
||||
agentId: "openclaw",
|
||||
});
|
||||
|
||||
expectCliAuthEpoch(fingerprint);
|
||||
@@ -1009,7 +1009,7 @@ describe("resolveCliAuthEpoch", () => {
|
||||
resolveCliRuntimeOwnerFingerprint({
|
||||
provider: "claude-cli",
|
||||
config,
|
||||
agentId: "crestodian",
|
||||
agentId: "openclaw",
|
||||
runtimeOwnerId: "replacement-backend",
|
||||
}),
|
||||
).resolves.toBeUndefined();
|
||||
@@ -1029,13 +1029,13 @@ describe("resolveCliAuthEpoch", () => {
|
||||
const first = await resolveCliRuntimeOwnerFingerprint({
|
||||
provider: "claude-cli",
|
||||
config,
|
||||
agentId: "crestodian",
|
||||
agentId: "openclaw",
|
||||
env: { PATH: firstBin },
|
||||
});
|
||||
const second = await resolveCliRuntimeOwnerFingerprint({
|
||||
provider: "claude-cli",
|
||||
config,
|
||||
agentId: "crestodian",
|
||||
agentId: "openclaw",
|
||||
env: { PATH: secondBin },
|
||||
});
|
||||
|
||||
@@ -1056,7 +1056,7 @@ describe("resolveCliAuthEpoch", () => {
|
||||
const first = await resolveCliRuntimeOwnerFingerprint({
|
||||
provider: "claude-cli",
|
||||
config,
|
||||
agentId: "crestodian",
|
||||
agentId: "openclaw",
|
||||
});
|
||||
copyNativeExecutable(executable, nativeUtility("false"));
|
||||
if (nativeUtility("true") === nativeUtility("false")) {
|
||||
@@ -1065,7 +1065,7 @@ describe("resolveCliAuthEpoch", () => {
|
||||
const second = await resolveCliRuntimeOwnerFingerprint({
|
||||
provider: "claude-cli",
|
||||
config,
|
||||
agentId: "crestodian",
|
||||
agentId: "openclaw",
|
||||
});
|
||||
|
||||
expectCliAuthEpoch(first);
|
||||
@@ -1110,7 +1110,7 @@ describe("resolveCliAuthEpoch", () => {
|
||||
resolveCliRuntimeOwnerFingerprint({
|
||||
provider: "claude-cli",
|
||||
config: cliConfig("./claude"),
|
||||
agentId: "crestodian",
|
||||
agentId: "openclaw",
|
||||
cwd: dir,
|
||||
}),
|
||||
).resolves.toBeUndefined();
|
||||
@@ -1128,7 +1128,7 @@ describe("resolveCliAuthEpoch", () => {
|
||||
resolveCliRuntimeOwnerFingerprint({
|
||||
provider: "claude-cli",
|
||||
config: cliConfig(process.execPath),
|
||||
agentId: "crestodian",
|
||||
agentId: "openclaw",
|
||||
authProfileId: "anthropic:missing",
|
||||
}),
|
||||
).resolves.toBeUndefined();
|
||||
|
||||
@@ -161,7 +161,7 @@ function resolveCommandPath(params: {
|
||||
return undefined;
|
||||
}
|
||||
if (hasPathSeparator(params.command) && !isDurableRootedCommand(params.command)) {
|
||||
// The setup probe and later Crestodian turns intentionally use different
|
||||
// The setup probe and later OpenClaw turns intentionally use different
|
||||
// workspaces. A cwd-relative executable cannot name one durable owner.
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -1058,7 +1058,7 @@ describe("runCliAgent spawn path", () => {
|
||||
mockSuccessfulClaudeJsonlRun();
|
||||
const toolAvailability: NonNullable<PreparedCliRunContext["params"]["cliToolAvailability"]> = {
|
||||
native: [],
|
||||
mcp: ["mcp__openclaw__crestodian"],
|
||||
mcp: ["mcp__openclaw__openclaw"],
|
||||
};
|
||||
const resolveExecutionArgs = vi.fn(({ baseArgs }) => baseArgs);
|
||||
|
||||
@@ -1088,7 +1088,7 @@ describe("runCliAgent spawn path", () => {
|
||||
runId: "run-claude-tool-policy-refused",
|
||||
cliToolAvailability: {
|
||||
native: [],
|
||||
mcp: ["mcp__openclaw__crestodian"],
|
||||
mcp: ["mcp__openclaw__openclaw"],
|
||||
},
|
||||
resolveExecutionArgs,
|
||||
}),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/** Tests bundle-MCP resume hash stability across loopback endpoint changes. */
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildCrestodianToolsMcpServerConfig } from "../../mcp/openclaw-tools-serve-config.js";
|
||||
import { buildSystemAgentToolsMcpServerConfig } from "../../mcp/openclaw-tools-serve-config.js";
|
||||
import { resolveCliSessionReuse } from "../cli-session.js";
|
||||
import { prepareCliBundleMcpConfig } from "./bundle-mcp.js";
|
||||
import {
|
||||
@@ -83,14 +83,14 @@ describe("prepareCliBundleMcpConfig resume hash", () => {
|
||||
await second.cleanup?.();
|
||||
});
|
||||
|
||||
it("keeps Crestodian approval state out of the resume identity", async () => {
|
||||
const prepare = async (options: Parameters<typeof buildCrestodianToolsMcpServerConfig>[0]) =>
|
||||
it("keeps OpenClaw approval state out of the resume identity", async () => {
|
||||
const prepare = async (options: Parameters<typeof buildSystemAgentToolsMcpServerConfig>[0]) =>
|
||||
await prepareCliBundleMcpConfig({
|
||||
enabled: true,
|
||||
mode: "claude-config-file",
|
||||
backend: { command: "node", args: ["./fake-claude.mjs"] },
|
||||
workspaceDir: cliBundleMcpHarness.bundleProbeWorkspaceDir,
|
||||
exclusiveConfig: buildCrestodianToolsMcpServerConfig(options),
|
||||
exclusiveConfig: buildSystemAgentToolsMcpServerConfig(options),
|
||||
});
|
||||
const proposed = "proposal-sha256";
|
||||
const first = await prepare({ surface: "cli", proposalRef: {} });
|
||||
|
||||
@@ -62,7 +62,7 @@ describe("prepareCliBundleMcpConfig", () => {
|
||||
workspaceDir,
|
||||
config: { plugins: { enabled: false } },
|
||||
exclusiveConfig: {
|
||||
mcpServers: { openclaw: { command: "node", args: ["crestodian.mjs"] } },
|
||||
mcpServers: { openclaw: { command: "node", args: ["openclaw.mjs"] } },
|
||||
},
|
||||
});
|
||||
|
||||
@@ -72,7 +72,7 @@ describe("prepareCliBundleMcpConfig", () => {
|
||||
mcpServers?: Record<string, { args?: string[] }>;
|
||||
};
|
||||
expect(Object.keys(raw.mcpServers ?? {})).toEqual(["openclaw"]);
|
||||
expect(raw.mcpServers?.openclaw?.args).toEqual(["crestodian.mjs"]);
|
||||
expect(raw.mcpServers?.openclaw?.args).toEqual(["openclaw.mjs"]);
|
||||
expect(prepared.mcpConfigHash).toMatch(/^[0-9a-f]{64}$/);
|
||||
|
||||
await prepared.cleanup?.();
|
||||
|
||||
@@ -11,8 +11,8 @@ import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import { formatErrorMessage } from "../../infra/errors.js";
|
||||
import { tryReadJson } from "../../infra/json-files.js";
|
||||
import {
|
||||
OPENCLAW_TOOLS_MCP_CRESTODIAN_APPROVAL_ARMED_ENV,
|
||||
OPENCLAW_TOOLS_MCP_CRESTODIAN_PROPOSAL_ENV,
|
||||
OPENCLAW_TOOLS_MCP_SYSTEM_AGENT_APPROVAL_ARMED_ENV,
|
||||
OPENCLAW_TOOLS_MCP_SYSTEM_AGENT_PROPOSAL_ENV,
|
||||
OPENCLAW_TOOLS_MCP_TOOLS_ENV,
|
||||
} from "../../mcp/openclaw-tools-serve-config.js";
|
||||
import { extractMcpServerMap, type BundleMcpConfig } from "../../plugins/bundle-mcp.js";
|
||||
@@ -69,10 +69,10 @@ function normalizeOpenClawLoopbackUrl(value: string): string {
|
||||
return `${match[1]}:<openclaw-loopback>${match[2]}`;
|
||||
}
|
||||
|
||||
function canonicalizeCrestodianTurnStateForResume(
|
||||
function canonicalizeSystemAgentTurnStateForResume(
|
||||
server: BundleMcpConfig["mcpServers"][string],
|
||||
): BundleMcpConfig["mcpServers"][string] {
|
||||
if (!isRecord(server.env) || server.env[OPENCLAW_TOOLS_MCP_TOOLS_ENV] !== "crestodian") {
|
||||
if (!isRecord(server.env) || server.env[OPENCLAW_TOOLS_MCP_TOOLS_ENV] !== "openclaw") {
|
||||
return server;
|
||||
}
|
||||
// The host reissues approval authority through a fresh stdio server each turn.
|
||||
@@ -81,8 +81,8 @@ function canonicalizeCrestodianTurnStateForResume(
|
||||
...server,
|
||||
env: {
|
||||
...server.env,
|
||||
[OPENCLAW_TOOLS_MCP_CRESTODIAN_APPROVAL_ARMED_ENV]: "<crestodian-turn-state>",
|
||||
[OPENCLAW_TOOLS_MCP_CRESTODIAN_PROPOSAL_ENV]: "<crestodian-turn-state>",
|
||||
[OPENCLAW_TOOLS_MCP_SYSTEM_AGENT_APPROVAL_ARMED_ENV]: "<openclaw-turn-state>",
|
||||
[OPENCLAW_TOOLS_MCP_SYSTEM_AGENT_PROPOSAL_ENV]: "<openclaw-turn-state>",
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -92,7 +92,7 @@ function canonicalizeBundleMcpConfigForResume(config: BundleMcpConfig): BundleMc
|
||||
// hashing so resume compatibility tracks config shape, not ephemeral ports.
|
||||
const canonicalServers = Object.fromEntries(
|
||||
Object.entries(config.mcpServers).map(([name, server]) => {
|
||||
const canonicalServer = canonicalizeCrestodianTurnStateForResume(server);
|
||||
const canonicalServer = canonicalizeSystemAgentTurnStateForResume(server);
|
||||
if (name !== "openclaw" || typeof canonicalServer.url !== "string") {
|
||||
return [name, sortJsonValue(canonicalServer)];
|
||||
}
|
||||
@@ -212,8 +212,8 @@ export async function prepareCliBundleMcpConfig(params: {
|
||||
additionalConfig?: BundleMcpConfig;
|
||||
/**
|
||||
* Serve exactly these servers, skipping user/plugin/additional merges.
|
||||
* Ring-zero Crestodian runs use this so the CLI harness sees only the
|
||||
* crestodian MCP server instead of the normal openclaw tool surface.
|
||||
* Ring-zero OpenClaw runs use this so the CLI harness sees only the
|
||||
* openclaw MCP server instead of the normal openclaw tool surface.
|
||||
*/
|
||||
exclusiveConfig?: BundleMcpConfig;
|
||||
env?: Record<string, string>;
|
||||
|
||||
@@ -39,7 +39,7 @@ import { resetContextWindowCacheForTest } from "../context.js";
|
||||
import { buildActiveImageGenerationTaskPromptContextForSession } from "../image-generation-task-status.js";
|
||||
import { buildActiveMusicGenerationTaskPromptContextForSession } from "../music-generation-task-status.js";
|
||||
import type { SandboxWorkspaceInfo } from "../sandbox/types.js";
|
||||
import type { CrestodianToolOptions } from "../tools/crestodian-tool.js";
|
||||
import type { SystemAgentToolOptions } from "../tools/system-agent-tool.js";
|
||||
import { buildActiveVideoGenerationTaskPromptContextForSession } from "../video-generation-task-status.js";
|
||||
import {
|
||||
prepareCliRunContext,
|
||||
@@ -500,7 +500,7 @@ describe("shouldSkipLocalCliCredentialEpoch", () => {
|
||||
it("honors an explicit auth agent directory independently of session identity", async () => {
|
||||
const { dir, sessionFile } = createSessionFile();
|
||||
const modelOwnerAgentDir = path.join(dir, "ops-agent");
|
||||
const crestodianAgentDir = path.join(dir, "crestodian-agent");
|
||||
const systemAgentDir = path.join(dir, "openclaw-agent");
|
||||
const prepareExecution = vi.fn(async () => undefined);
|
||||
fs.mkdirSync(modelOwnerAgentDir, { recursive: true });
|
||||
cliBackendsTesting.setDepsForTest({
|
||||
@@ -525,8 +525,8 @@ describe("shouldSkipLocalCliCredentialEpoch", () => {
|
||||
try {
|
||||
const context = await prepareCliRunContext({
|
||||
sessionId: "session-test",
|
||||
sessionKey: "agent:crestodian:main",
|
||||
agentId: "crestodian",
|
||||
sessionKey: "agent:openclaw:main",
|
||||
agentId: "openclaw",
|
||||
sessionFile,
|
||||
workspaceDir: dir,
|
||||
agentDir: modelOwnerAgentDir,
|
||||
@@ -540,7 +540,7 @@ describe("shouldSkipLocalCliCredentialEpoch", () => {
|
||||
agents: {
|
||||
list: [
|
||||
{ id: "ops", default: true, agentDir: modelOwnerAgentDir },
|
||||
{ id: "crestodian", agentDir: crestodianAgentDir },
|
||||
{ id: "openclaw", agentDir: systemAgentDir },
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -3561,7 +3561,7 @@ describe("shouldSkipLocalCliCredentialEpoch", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("serves only the crestodian MCP server for ring-zero runs", async () => {
|
||||
it("serves only the openclaw MCP server for ring-zero runs", async () => {
|
||||
const { dir, sessionFile } = createSessionFile();
|
||||
try {
|
||||
const getActiveMcpLoopbackRuntime = vi.fn(() => undefined);
|
||||
@@ -3601,7 +3601,7 @@ describe("shouldSkipLocalCliCredentialEpoch", () => {
|
||||
],
|
||||
});
|
||||
|
||||
const params: RunCliAgentParams & { crestodianTool: CrestodianToolOptions } = {
|
||||
const params: RunCliAgentParams & { systemAgentTool: SystemAgentToolOptions } = {
|
||||
sessionId: "session-test",
|
||||
sessionFile,
|
||||
workspaceDir: dir,
|
||||
@@ -3609,12 +3609,12 @@ describe("shouldSkipLocalCliCredentialEpoch", () => {
|
||||
provider: "claude-cli",
|
||||
model: "test-model",
|
||||
timeoutMs: 1_000,
|
||||
runId: "run-test-crestodian-mcp",
|
||||
runId: "run-test-openclaw-mcp",
|
||||
config: createCliBackendConfig(),
|
||||
crestodianTool: { surface: "cli" },
|
||||
systemAgentTool: { surface: "cli" },
|
||||
cliToolAvailability: {
|
||||
native: [],
|
||||
mcp: ["mcp__openclaw__crestodian"],
|
||||
mcp: ["mcp__openclaw__openclaw"],
|
||||
},
|
||||
};
|
||||
const context = await prepareCliRunContext(params);
|
||||
@@ -3632,7 +3632,7 @@ describe("shouldSkipLocalCliCredentialEpoch", () => {
|
||||
expect(resolveExecutionArgs).not.toHaveBeenCalled();
|
||||
expect(context.params.cliToolAvailability).toEqual({
|
||||
native: [],
|
||||
mcp: ["mcp__openclaw__crestodian"],
|
||||
mcp: ["mcp__openclaw__openclaw"],
|
||||
});
|
||||
const mcpConfigPath = expectDefined(
|
||||
args[args.indexOf("--mcp-config") + 1],
|
||||
@@ -3643,8 +3643,8 @@ describe("shouldSkipLocalCliCredentialEpoch", () => {
|
||||
};
|
||||
expect(Object.keys(raw.mcpServers ?? {})).toEqual(["openclaw"]);
|
||||
expect(raw.mcpServers?.openclaw?.env).toMatchObject({
|
||||
OPENCLAW_TOOLS_MCP_TOOLS: "crestodian",
|
||||
OPENCLAW_TOOLS_MCP_CRESTODIAN_SURFACE: "cli",
|
||||
OPENCLAW_TOOLS_MCP_TOOLS: "openclaw",
|
||||
OPENCLAW_TOOLS_MCP_SYSTEM_AGENT_SURFACE: "cli",
|
||||
});
|
||||
|
||||
await context.preparedBackend.cleanup?.();
|
||||
@@ -3969,14 +3969,14 @@ describe("shouldSkipLocalCliCredentialEpoch", () => {
|
||||
|
||||
const context = await prepareCliRunContext({
|
||||
sessionId: "session-test",
|
||||
sessionKey: "agent:crestodian:main",
|
||||
sessionKey: "agent:openclaw:main",
|
||||
sessionFile,
|
||||
workspaceDir: dir,
|
||||
prompt: "approve the proposal",
|
||||
provider: "claude-cli",
|
||||
model: "opus",
|
||||
timeoutMs: 1_000,
|
||||
runId: "run-crestodian-process-per-turn",
|
||||
runId: "run-openclaw-process-per-turn",
|
||||
cliSessionBinding: { sessionId: "native-claude-sid" },
|
||||
config: createCliBackendConfig(),
|
||||
disableCliLiveSession: true,
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
getActiveMcpLoopbackRuntime,
|
||||
} from "../../gateway/mcp-http.loopback-runtime.js";
|
||||
import { resolveMcpLoopbackScopedTools } from "../../gateway/mcp-http.runtime.js";
|
||||
import { buildCrestodianToolsMcpServerConfig } from "../../mcp/openclaw-tools-serve-config.js";
|
||||
import { buildSystemAgentToolsMcpServerConfig } from "../../mcp/openclaw-tools-serve-config.js";
|
||||
import { isClaudeCliProvider } from "../../plugin-sdk/anthropic-cli.js";
|
||||
import type {
|
||||
CliBackendAuthEpochMode,
|
||||
@@ -122,8 +122,8 @@ function resolveClaudeCliContextModelId(modelId: string): string {
|
||||
return CLAUDE_CLI_CONTEXT_MODEL_ALIASES[lower] ?? trimmed;
|
||||
}
|
||||
type RunCliAgentPrepareParams = RunCliAgentParams & {
|
||||
/** Ring-zero tool transport supplied only by the Crestodian orchestrator. */
|
||||
crestodianTool?: import("../tools/crestodian-tool.js").CrestodianToolOptions;
|
||||
/** Ring-zero tool transport supplied only by the OpenClaw orchestrator. */
|
||||
systemAgentTool?: import("../tools/system-agent-tool.js").SystemAgentToolOptions;
|
||||
};
|
||||
|
||||
const prepareDeps = {
|
||||
@@ -775,16 +775,16 @@ export async function prepareCliRunContext(
|
||||
bootstrapMode === "none"
|
||||
? toolBoundExtraSystemPromptHash
|
||||
: hashCliSessionText(JSON.stringify([toolBoundExtraSystemPromptHash ?? null, bootstrapMode]));
|
||||
// Ring-zero Crestodian runs replace the bundle MCP surface entirely: no
|
||||
// Ring-zero OpenClaw runs replace the bundle MCP surface entirely: no
|
||||
// loopback server, no plugin/user servers. A selectable backend also removes
|
||||
// its native tools, leaving only this crestodian stdio server.
|
||||
const crestodianMcpConfig = internalParams.crestodianTool
|
||||
? buildCrestodianToolsMcpServerConfig(internalParams.crestodianTool)
|
||||
// its native tools, leaving only this openclaw stdio server.
|
||||
const systemAgentMcpConfig = internalParams.systemAgentTool
|
||||
? buildSystemAgentToolsMcpServerConfig(internalParams.systemAgentTool)
|
||||
: undefined;
|
||||
const bundleMcpEnabled =
|
||||
!nodeClaudePlacement &&
|
||||
!isSideQuestion &&
|
||||
!crestodianMcpConfig &&
|
||||
!systemAgentMcpConfig &&
|
||||
backendResolved.bundleMcp &&
|
||||
params.disableTools !== true;
|
||||
let mcpLoopbackRuntime = bundleMcpEnabled ? prepareDeps.getActiveMcpLoopbackRuntime() : undefined;
|
||||
@@ -856,13 +856,13 @@ export async function prepareCliRunContext(
|
||||
: undefined;
|
||||
cleanupPreparedResources = cleanupMcpClientGrant;
|
||||
const preparedBackend = await prepareCliBundleMcpConfig({
|
||||
enabled: bundleMcpEnabled || crestodianMcpConfig !== undefined,
|
||||
enabled: bundleMcpEnabled || systemAgentMcpConfig !== undefined,
|
||||
mode: backendResolved.bundleMcpMode,
|
||||
backend: backendResolved.config,
|
||||
workspaceDir,
|
||||
config: params.config,
|
||||
agentDir,
|
||||
...(crestodianMcpConfig ? { exclusiveConfig: crestodianMcpConfig } : {}),
|
||||
...(systemAgentMcpConfig ? { exclusiveConfig: systemAgentMcpConfig } : {}),
|
||||
additionalConfig: mcpLoopbackRuntime
|
||||
? prepareDeps.createMcpLoopbackServerConfig(mcpLoopbackRuntime.port)
|
||||
: undefined,
|
||||
|
||||
@@ -17,7 +17,7 @@ export const CORE_TOOL_FACTORY_DESCRIPTORS = [
|
||||
{ name: "exec", family: "shell" },
|
||||
{ name: "process", family: "shell" },
|
||||
{ name: "agents_list", family: "openclaw" },
|
||||
{ name: "crestodian", family: "openclaw" },
|
||||
{ name: "openclaw", family: "openclaw" },
|
||||
{ name: "computer", family: "openclaw" },
|
||||
{ name: "cron", family: "openclaw" },
|
||||
{ name: "gateway", family: "openclaw" },
|
||||
|
||||
@@ -284,8 +284,8 @@ type RunEmbeddedAgentInternalParams = RunEmbeddedAgentParams & {
|
||||
binding: import("../execution-auth-binding.js").AgentExecutionAuthBinding,
|
||||
) => void;
|
||||
authProfileStateMode?: "read-write" | "read-only";
|
||||
/** Ring-zero tool override, supplied only by the Crestodian orchestrator. */
|
||||
crestodianTool?: import("../tools/crestodian-tool.js").CrestodianToolOptions;
|
||||
/** Ring-zero tool override, supplied only by the OpenClaw orchestrator. */
|
||||
systemAgentTool?: import("../tools/system-agent-tool.js").SystemAgentToolOptions;
|
||||
};
|
||||
type RunEmbeddedAgentParamsWithSessionFile = RunEmbeddedAgentInternalParams & {
|
||||
sessionFile: string;
|
||||
@@ -1857,7 +1857,7 @@ async function runEmbeddedAgentInternal(
|
||||
bootstrapContextRunKind: params.bootstrapContextRunKind,
|
||||
jobId: params.jobId,
|
||||
toolsAllow: params.toolsAllow,
|
||||
...(params.crestodianTool ? { crestodianTool: params.crestodianTool } : {}),
|
||||
...(params.systemAgentTool ? { systemAgentTool: params.systemAgentTool } : {}),
|
||||
cleanupBundleMcpOnRunEnd: params.cleanupBundleMcpOnRunEnd,
|
||||
disableMessageTool: params.disableMessageTool,
|
||||
forceRestartSafeTools: params.forceRestartSafeTools,
|
||||
|
||||
@@ -12,7 +12,7 @@ import type {
|
||||
EmbeddedRunAttemptParams,
|
||||
EmbeddedRunAttemptResult,
|
||||
} from "../embedded-agent-runner/run/types.js";
|
||||
import type { CrestodianToolOptions } from "../tools/crestodian-tool.js";
|
||||
import type { SystemAgentToolOptions } from "../tools/system-agent-tool.js";
|
||||
import { maybeCompactAgentHarnessSession } from "./compaction.js";
|
||||
import { clearAgentHarnesses, registerAgentHarness } from "./registry.js";
|
||||
import {
|
||||
@@ -380,18 +380,18 @@ function registerTestCompactor(
|
||||
|
||||
describe("runAgentHarnessAttempt", () => {
|
||||
it.each(["codex", "copilot"] as const)(
|
||||
"binds the host Crestodian tool to the %s SDK construction path without leaking authority",
|
||||
"binds the host OpenClaw tool to the %s SDK construction path without leaking authority",
|
||||
async (harnessId) => {
|
||||
let receivedPrivateAuthority = true;
|
||||
let hostScopeActive = false;
|
||||
let toolNames: string[] = [];
|
||||
const pluginRunAttempt = vi.fn<AgentHarness["runAttempt"]>(async (attemptParams) => {
|
||||
receivedPrivateAuthority = "crestodianTool" in attemptParams;
|
||||
receivedPrivateAuthority = "systemAgentTool" in attemptParams;
|
||||
await Promise.resolve();
|
||||
hostScopeActive = isHostScopedAgentToolActive("crestodian");
|
||||
hostScopeActive = isHostScopedAgentToolActive("openclaw");
|
||||
toolNames = createOpenClawCodingTools({
|
||||
config: { tools: { allow: ["read"], deny: ["crestodian"], toolSearch: true } },
|
||||
runtimeToolAllowlist: ["crestodian"],
|
||||
config: { tools: { allow: ["read"], deny: ["openclaw"], toolSearch: true } },
|
||||
runtimeToolAllowlist: ["openclaw"],
|
||||
toolConstructionPlan: {
|
||||
includeBaseCodingTools: false,
|
||||
includeShellTools: false,
|
||||
@@ -413,25 +413,25 @@ describe("runAgentHarnessAttempt", () => {
|
||||
);
|
||||
const params = createAttemptParams(
|
||||
providerRuntimeConfig("codex", harnessId),
|
||||
) as EmbeddedRunAttemptParams & { crestodianTool?: CrestodianToolOptions };
|
||||
params.toolsAllow = ["crestodian"];
|
||||
params.crestodianTool = { surface: "cli", proposalRef: {}, directiveRef: {} };
|
||||
) as EmbeddedRunAttemptParams & { systemAgentTool?: SystemAgentToolOptions };
|
||||
params.toolsAllow = ["openclaw"];
|
||||
params.systemAgentTool = { surface: "cli", proposalRef: {}, directiveRef: {} };
|
||||
|
||||
await runAgentHarnessAttempt(params);
|
||||
|
||||
expect(pluginRunAttempt).toHaveBeenCalledTimes(1);
|
||||
expect(receivedPrivateAuthority).toBe(false);
|
||||
expect(hostScopeActive).toBe(true);
|
||||
expect(toolNames).toEqual(["crestodian"]);
|
||||
expect(createOpenClawCodingTools().some((tool) => tool.name === "crestodian")).toBe(false);
|
||||
expect(isHostScopedAgentToolActive("crestodian")).toBe(false);
|
||||
expect(toolNames).toEqual(["openclaw"]);
|
||||
expect(createOpenClawCodingTools().some((tool) => tool.name === "openclaw")).toBe(false);
|
||||
expect(isHostScopedAgentToolActive("openclaw")).toBe(false);
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{ name: "missing", toolsAllow: undefined },
|
||||
{ name: "broad", toolsAllow: ["crestodian", "read"] },
|
||||
])("rejects $name allowlists for private Crestodian authority", async ({ toolsAllow }) => {
|
||||
{ name: "broad", toolsAllow: ["openclaw", "read"] },
|
||||
])("rejects $name allowlists for private OpenClaw authority", async ({ toolsAllow }) => {
|
||||
const pluginRunAttempt = vi.fn<AgentHarness["runAttempt"]>(async () =>
|
||||
createAttemptResult("codex"),
|
||||
);
|
||||
@@ -446,18 +446,18 @@ describe("runAgentHarnessAttempt", () => {
|
||||
);
|
||||
const params = createAttemptParams(
|
||||
providerRuntimeConfig("codex", "codex"),
|
||||
) as EmbeddedRunAttemptParams & { crestodianTool?: CrestodianToolOptions };
|
||||
) as EmbeddedRunAttemptParams & { systemAgentTool?: SystemAgentToolOptions };
|
||||
params.toolsAllow = toolsAllow;
|
||||
params.crestodianTool = { surface: "cli", proposalRef: {}, directiveRef: {} };
|
||||
params.systemAgentTool = { surface: "cli", proposalRef: {}, directiveRef: {} };
|
||||
|
||||
await expect(runAgentHarnessAttempt(params)).rejects.toThrow(
|
||||
'Crestodian host authority requires toolsAllow: ["crestodian"]',
|
||||
'OpenClaw host authority requires toolsAllow: ["openclaw"]',
|
||||
);
|
||||
expect(pluginRunAttempt).not.toHaveBeenCalled();
|
||||
expect(isHostScopedAgentToolActive("crestodian")).toBe(false);
|
||||
expect(isHostScopedAgentToolActive("openclaw")).toBe(false);
|
||||
});
|
||||
|
||||
it("keeps the host Crestodian allowlist across global, agent, and sandbox deny-all policy", async () => {
|
||||
it("keeps the host OpenClaw allowlist across global, agent, and sandbox deny-all policy", async () => {
|
||||
const received: Array<{
|
||||
toolsAllow: string[] | undefined;
|
||||
extraSystemPrompt: string | undefined;
|
||||
@@ -467,7 +467,7 @@ describe("runAgentHarnessAttempt", () => {
|
||||
received.push({
|
||||
toolsAllow: attemptParams.toolsAllow,
|
||||
extraSystemPrompt: attemptParams.extraSystemPrompt,
|
||||
hostScopeActive: isHostScopedAgentToolActive("crestodian"),
|
||||
hostScopeActive: isHostScopedAgentToolActive("openclaw"),
|
||||
});
|
||||
return createAttemptResult("codex");
|
||||
});
|
||||
@@ -503,32 +503,32 @@ describe("runAgentHarnessAttempt", () => {
|
||||
|
||||
for (const [index, testCase] of cases.entries()) {
|
||||
const params = createAttemptParams(testCase.config) as EmbeddedRunAttemptParams & {
|
||||
crestodianTool?: CrestodianToolOptions;
|
||||
systemAgentTool?: SystemAgentToolOptions;
|
||||
};
|
||||
params.sessionId = `session-${index}`;
|
||||
params.agentHarnessRuntimeOverride = "codex";
|
||||
params.agentId = testCase.agentId;
|
||||
params.sessionKey = testCase.sessionKey;
|
||||
params.toolsAllow = ["crestodian"];
|
||||
params.crestodianTool = { surface: "cli", proposalRef: {}, directiveRef: {} };
|
||||
params.toolsAllow = ["openclaw"];
|
||||
params.systemAgentTool = { surface: "cli", proposalRef: {}, directiveRef: {} };
|
||||
await runAgentHarnessAttempt(params);
|
||||
}
|
||||
|
||||
expect(received).toEqual([
|
||||
{ toolsAllow: ["crestodian"], extraSystemPrompt: undefined, hostScopeActive: true },
|
||||
{ toolsAllow: ["crestodian"], extraSystemPrompt: undefined, hostScopeActive: true },
|
||||
{ toolsAllow: ["crestodian"], extraSystemPrompt: undefined, hostScopeActive: true },
|
||||
{ toolsAllow: ["openclaw"], extraSystemPrompt: undefined, hostScopeActive: true },
|
||||
{ toolsAllow: ["openclaw"], extraSystemPrompt: undefined, hostScopeActive: true },
|
||||
{ toolsAllow: ["openclaw"], extraSystemPrompt: undefined, hostScopeActive: true },
|
||||
]);
|
||||
expect(isHostScopedAgentToolActive("crestodian")).toBe(false);
|
||||
expect(isHostScopedAgentToolActive("openclaw")).toBe(false);
|
||||
});
|
||||
|
||||
it("binds the same host Crestodian scope to the built-in OpenClaw harness", async () => {
|
||||
it("binds the same host OpenClaw scope to the built-in OpenClaw harness", async () => {
|
||||
let toolNames: string[] = [];
|
||||
agentRunAttempt.mockImplementationOnce(async () => {
|
||||
await Promise.resolve();
|
||||
toolNames = createOpenClawCodingTools({
|
||||
config: { tools: { allow: ["read"], deny: ["crestodian"], toolSearch: true } },
|
||||
runtimeToolAllowlist: ["crestodian"],
|
||||
config: { tools: { allow: ["read"], deny: ["openclaw"], toolSearch: true } },
|
||||
runtimeToolAllowlist: ["openclaw"],
|
||||
toolConstructionPlan: {
|
||||
includeBaseCodingTools: false,
|
||||
includeShellTools: false,
|
||||
@@ -541,16 +541,16 @@ describe("runAgentHarnessAttempt", () => {
|
||||
});
|
||||
const params = createAttemptParams(
|
||||
providerRuntimeConfig("codex", "openclaw"),
|
||||
) as EmbeddedRunAttemptParams & { crestodianTool?: CrestodianToolOptions };
|
||||
params.toolsAllow = ["crestodian"];
|
||||
params.crestodianTool = { surface: "gateway", proposalRef: {}, directiveRef: {} };
|
||||
) as EmbeddedRunAttemptParams & { systemAgentTool?: SystemAgentToolOptions };
|
||||
params.toolsAllow = ["openclaw"];
|
||||
params.systemAgentTool = { surface: "gateway", proposalRef: {}, directiveRef: {} };
|
||||
|
||||
const result = await runAgentHarnessAttempt(params);
|
||||
|
||||
expect(result.sessionIdUsed).toBe("openclaw");
|
||||
expect(toolNames).toEqual(["crestodian"]);
|
||||
expect(createOpenClawCodingTools().some((tool) => tool.name === "crestodian")).toBe(false);
|
||||
expect(isHostScopedAgentToolActive("crestodian")).toBe(false);
|
||||
expect(toolNames).toEqual(["openclaw"]);
|
||||
expect(createOpenClawCodingTools().some((tool) => tool.name === "openclaw")).toBe(false);
|
||||
expect(isHostScopedAgentToolActive("openclaw")).toBe(false);
|
||||
});
|
||||
|
||||
it("unwraps sentinels only at the plugin harness handoff", async () => {
|
||||
|
||||
@@ -30,7 +30,7 @@ import {
|
||||
} from "../provider-secret-egress.js";
|
||||
import { resolveSandboxRuntimeStatus } from "../sandbox/runtime-status.js";
|
||||
import { expandToolGroups, mergeAlsoAllowPolicy, normalizeToolName } from "../tool-policy.js";
|
||||
import type { CrestodianToolOptions } from "../tools/crestodian-tool.js";
|
||||
import type { SystemAgentToolOptions } from "../tools/system-agent-tool.js";
|
||||
import { createOpenClawAgentHarness } from "./builtin-openclaw.js";
|
||||
import { MissingAgentHarnessError } from "./errors.js";
|
||||
import { runAgentHarnessLifecycleAttempt } from "./lifecycle.js";
|
||||
@@ -444,7 +444,7 @@ export async function runAgentHarnessAttempt(
|
||||
params: EmbeddedRunAttemptParams,
|
||||
): Promise<EmbeddedRunAttemptResult> {
|
||||
const internalParams = params as EmbeddedRunAttemptParams & {
|
||||
crestodianTool?: CrestodianToolOptions;
|
||||
systemAgentTool?: SystemAgentToolOptions;
|
||||
};
|
||||
const activeTrace = getActiveDiagnosticTraceContext();
|
||||
const harnessTrace = freezeDiagnosticTraceContext(
|
||||
@@ -467,13 +467,13 @@ export async function runAgentHarnessAttempt(
|
||||
preparedModelProvider: params.runtimePlan?.auth !== undefined,
|
||||
});
|
||||
const harness = selection.harness;
|
||||
if (internalParams.crestodianTool && !isCrestodianOnlyAllowlist(internalParams.toolsAllow)) {
|
||||
throw new Error('Crestodian host authority requires toolsAllow: ["crestodian"]');
|
||||
if (internalParams.systemAgentTool && !isSystemAgentOnlyAllowlist(internalParams.toolsAllow)) {
|
||||
throw new Error('OpenClaw host authority requires toolsAllow: ["openclaw"]');
|
||||
}
|
||||
const ringZeroTools = internalParams.crestodianTool
|
||||
const ringZeroTools = internalParams.systemAgentTool
|
||||
? [
|
||||
(await import("../tools/crestodian-tool.js")).createCrestodianTool(
|
||||
internalParams.crestodianTool,
|
||||
(await import("../tools/system-agent-tool.js")).createSystemAgentTool(
|
||||
internalParams.systemAgentTool,
|
||||
),
|
||||
]
|
||||
: [];
|
||||
@@ -509,17 +509,17 @@ export async function runAgentHarnessAttempt(
|
||||
}
|
||||
}
|
||||
|
||||
function isCrestodianOnlyAllowlist(toolsAllow: readonly string[] | undefined): boolean {
|
||||
return toolsAllow?.length === 1 && normalizeToolName(toolsAllow[0] ?? "") === "crestodian";
|
||||
function isSystemAgentOnlyAllowlist(toolsAllow: readonly string[] | undefined): boolean {
|
||||
return toolsAllow?.length === 1 && normalizeToolName(toolsAllow[0] ?? "") === "openclaw";
|
||||
}
|
||||
|
||||
function withoutInternalHarnessAuthority(
|
||||
params: EmbeddedRunAttemptParams & { crestodianTool?: CrestodianToolOptions },
|
||||
params: EmbeddedRunAttemptParams & { systemAgentTool?: SystemAgentToolOptions },
|
||||
): EmbeddedRunAttemptParams {
|
||||
if (!Object.hasOwn(params, "crestodianTool")) {
|
||||
if (!Object.hasOwn(params, "systemAgentTool")) {
|
||||
return params;
|
||||
}
|
||||
const { crestodianTool: _crestodianTool, ...pluginParams } = params;
|
||||
const { systemAgentTool: _systemAgentTool, ...pluginParams } = params;
|
||||
return pluginParams;
|
||||
}
|
||||
|
||||
@@ -543,9 +543,9 @@ function applyPluginHarnessDenyAllToolPolicy(
|
||||
params: EmbeddedRunAttemptParams,
|
||||
): EmbeddedRunAttemptParams {
|
||||
if (
|
||||
isHostScopedAgentToolActive("crestodian") &&
|
||||
isHostScopedAgentToolActive("openclaw") &&
|
||||
params.toolsAllow?.length === 1 &&
|
||||
normalizeToolName(params.toolsAllow[0] ?? "") === "crestodian"
|
||||
normalizeToolName(params.toolsAllow[0] ?? "") === "openclaw"
|
||||
) {
|
||||
return params;
|
||||
}
|
||||
|
||||
@@ -25,25 +25,25 @@ function createRuntime(config: OpenClawConfig) {
|
||||
|
||||
describe("createAgentHarnessToolSurfaceRuntime", () => {
|
||||
it("suppresses catalog controls for a host-scoped ring-zero run", () => {
|
||||
const crestodian = {
|
||||
...createStubTool("crestodian"),
|
||||
const openclaw = {
|
||||
...createStubTool("openclaw"),
|
||||
catalogMode: "direct-only" as const,
|
||||
};
|
||||
|
||||
runWithAgentRingZeroTools([crestodian], () => {
|
||||
runWithAgentRingZeroTools([openclaw], () => {
|
||||
const runtime = createAgentHarnessToolSurfaceRuntime({
|
||||
config: { tools: { toolSearch: true } },
|
||||
executeTool: async () => ({ content: [], details: {} }),
|
||||
modelToolsEnabled: true,
|
||||
runtimeToolAllowlist: ["crestodian"],
|
||||
toolsAllow: ["crestodian"],
|
||||
runtimeToolAllowlist: ["openclaw"],
|
||||
toolsAllow: ["openclaw"],
|
||||
});
|
||||
|
||||
expect(runtime.codeModeControlsEnabled).toBe(false);
|
||||
expect(runtime.toolSearchControlsEnabled).toBe(false);
|
||||
expect(runtime.includeToolSearchControls).toBe(false);
|
||||
expect(runtime.runtimeToolAllowlist).toEqual(["crestodian"]);
|
||||
expect(runtime.compactTools([crestodian]).tools).toEqual([crestodian]);
|
||||
expect(runtime.runtimeToolAllowlist).toEqual(["openclaw"]);
|
||||
expect(runtime.compactTools([openclaw]).tools).toEqual([openclaw]);
|
||||
runtime.cleanup();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -305,9 +305,9 @@ export const TOOL_DISPLAY_CONFIG: ToolDisplayConfig = {
|
||||
title: "Skill Workshop",
|
||||
detailKeys: ["action", "name", "proposal_id"],
|
||||
},
|
||||
crestodian: {
|
||||
openclaw: {
|
||||
emoji: "🦀",
|
||||
title: "Crestodian",
|
||||
title: "OpenClaw",
|
||||
detailKeys: ["action", "path", "model"],
|
||||
},
|
||||
gateway: {
|
||||
|
||||
+70
-70
@@ -1,18 +1,20 @@
|
||||
// Crestodian ring-zero tool tests: approval gating, action mapping, verification.
|
||||
// OpenClaw ring-zero tool tests: approval gating, action mapping, verification.
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
createCrestodianTool,
|
||||
hashCrestodianOperation,
|
||||
resolveCrestodianDirectiveTransition,
|
||||
resolveCrestodianProposalTransition,
|
||||
type CrestodianToolDirective,
|
||||
} from "./crestodian-tool.js";
|
||||
createSystemAgentTool,
|
||||
hashSystemAgentOperation,
|
||||
resolveSystemAgentDirectiveTransition,
|
||||
resolveSystemAgentProposalTransition,
|
||||
type SystemAgentToolDirective,
|
||||
} from "./system-agent-tool.js";
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
executeCrestodianOperation: vi.fn(async (_op: unknown, runtime: { log: (m: string) => void }) => {
|
||||
runtime.log("op-output");
|
||||
return { applied: false };
|
||||
}),
|
||||
executeSystemAgentOperation: vi.fn(
|
||||
async (_op: unknown, runtime: { log: (m: string) => void }) => {
|
||||
runtime.log("op-output");
|
||||
return { applied: false };
|
||||
},
|
||||
),
|
||||
readConfigFileSnapshot: vi.fn(async () => ({
|
||||
exists: true,
|
||||
valid: true,
|
||||
@@ -24,9 +26,9 @@ const mocks = vi.hoisted(() => ({
|
||||
})),
|
||||
}));
|
||||
|
||||
vi.mock("../../crestodian/operations.js", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("../../crestodian/operations.js")>()),
|
||||
executeCrestodianOperation: mocks.executeCrestodianOperation,
|
||||
vi.mock("../../system-agent/operations.js", async (importOriginal) => ({
|
||||
...(await importOriginal<typeof import("../../system-agent/operations.js")>()),
|
||||
executeSystemAgentOperation: mocks.executeSystemAgentOperation,
|
||||
}));
|
||||
|
||||
vi.mock("../../config/config.js", async (importOriginal) => ({
|
||||
@@ -46,27 +48,25 @@ function toolText(result: unknown): string {
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
describe("crestodian tool", () => {
|
||||
describe("openclaw tool", () => {
|
||||
it("stays directly callable instead of entering tool catalogs", () => {
|
||||
const tool = createCrestodianTool({ surface: "cli" });
|
||||
const tool = createSystemAgentTool({ surface: "cli" });
|
||||
expect(tool.catalogMode).toBe("direct-only");
|
||||
expect(tool.description).toContain(
|
||||
"approved=true only after user clearly agrees to exact change in this conversation",
|
||||
);
|
||||
expect(tool.description).toContain("Exact user approval required; then approved=true.");
|
||||
});
|
||||
|
||||
it("runs read actions immediately", async () => {
|
||||
const tool = createCrestodianTool({ surface: "cli" });
|
||||
const tool = createSystemAgentTool({ surface: "cli" });
|
||||
const result = await tool.execute("t1", { action: "status" });
|
||||
expect(toolText(result)).toContain("op-output");
|
||||
expect(mocks.executeCrestodianOperation).toHaveBeenCalledWith(
|
||||
expect(mocks.executeSystemAgentOperation).toHaveBeenCalledWith(
|
||||
{ kind: "status" },
|
||||
expect.anything(),
|
||||
expect.objectContaining({ approved: false }),
|
||||
);
|
||||
|
||||
await tool.execute("t1b", { action: "channel_info", channel: "Slack" });
|
||||
expect(mocks.executeCrestodianOperation).toHaveBeenCalledWith(
|
||||
expect(mocks.executeSystemAgentOperation).toHaveBeenCalledWith(
|
||||
{ kind: "channel-info", channel: "slack" },
|
||||
expect.anything(),
|
||||
expect.objectContaining({ approved: false }),
|
||||
@@ -75,7 +75,7 @@ describe("crestodian tool", () => {
|
||||
|
||||
it("refuses mutating actions without the approved assertion", async () => {
|
||||
const proposalRef: { current?: string } = {};
|
||||
const tool = createCrestodianTool({ surface: "cli", approvalArmed: true, proposalRef });
|
||||
const tool = createSystemAgentTool({ surface: "cli", approvalArmed: true, proposalRef });
|
||||
const result = await tool.execute("t2", {
|
||||
action: "config_set",
|
||||
path: "gateway.port",
|
||||
@@ -84,13 +84,13 @@ describe("crestodian tool", () => {
|
||||
// An armed turn can never mint its own proposal.
|
||||
expect(toolText(result)).toContain("approval-mismatch");
|
||||
expect(proposalRef.current).toBeUndefined();
|
||||
expect(mocks.executeCrestodianOperation).not.toHaveBeenCalled();
|
||||
expect(mocks.executeSystemAgentOperation).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("refuses model-asserted approval without host-verified consent", async () => {
|
||||
// approved=true from the model alone must never mutate: the host arms
|
||||
// approval only when the user's actual message was an explicit yes.
|
||||
const tool = createCrestodianTool({ surface: "cli" });
|
||||
const tool = createSystemAgentTool({ surface: "cli" });
|
||||
const result = await tool.execute("t2b", {
|
||||
action: "config_set",
|
||||
path: "gateway.port",
|
||||
@@ -98,7 +98,7 @@ describe("crestodian tool", () => {
|
||||
approved: true,
|
||||
});
|
||||
expect(toolText(result)).toContain("needs-approval");
|
||||
expect(mocks.executeCrestodianOperation).not.toHaveBeenCalled();
|
||||
expect(mocks.executeSystemAgentOperation).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects arbitrary plugin installs before creating an approval proposal", async () => {
|
||||
@@ -119,7 +119,7 @@ describe("crestodian tool", () => {
|
||||
it("defers an approved mutation to the host after the full proposal handshake", async () => {
|
||||
const proposalRef: { current?: string } = {};
|
||||
// Phase 1: unarmed proposal is denied and records the exact operation.
|
||||
const proposingTool = createCrestodianTool({ surface: "gateway", proposalRef });
|
||||
const proposingTool = createSystemAgentTool({ surface: "gateway", proposalRef });
|
||||
const denied = await proposingTool.execute("t3a", {
|
||||
action: "set_default_model",
|
||||
model: "openai/gpt-5.5",
|
||||
@@ -127,12 +127,12 @@ describe("crestodian tool", () => {
|
||||
});
|
||||
expect(toolText(denied)).toContain("needs-approval");
|
||||
expect(proposalRef.current).toBeDefined();
|
||||
expect(mocks.executeCrestodianOperation).not.toHaveBeenCalled();
|
||||
expect(mocks.executeSystemAgentOperation).not.toHaveBeenCalled();
|
||||
|
||||
// Phase 2: the user's yes arms the turn; the identical call becomes one
|
||||
// host-owned directive so the inference binding can be checked again.
|
||||
const directiveRef: { current?: CrestodianToolDirective } = {};
|
||||
const armedTool = createCrestodianTool({
|
||||
const directiveRef: { current?: SystemAgentToolDirective } = {};
|
||||
const armedTool = createSystemAgentTool({
|
||||
surface: "gateway",
|
||||
approvalArmed: true,
|
||||
proposalRef,
|
||||
@@ -148,7 +148,7 @@ describe("crestodian tool", () => {
|
||||
kind: "approved-operation",
|
||||
operation: { kind: "set-default-model", model: "openai/gpt-5.5" },
|
||||
});
|
||||
expect(mocks.executeCrestodianOperation).not.toHaveBeenCalled();
|
||||
expect(mocks.executeSystemAgentOperation).not.toHaveBeenCalled();
|
||||
await armedTool.execute("t3c", { action: "connect_channel", channel: "telegram" });
|
||||
expect(directiveRef.current).toEqual({
|
||||
kind: "approved-operation",
|
||||
@@ -165,21 +165,21 @@ describe("crestodian tool", () => {
|
||||
workspace: "/tmp/work",
|
||||
model: "openai/gpt-5.5",
|
||||
};
|
||||
const result = await createCrestodianTool({ surface: "gateway", proposalRef }).execute(
|
||||
const result = await createSystemAgentTool({ surface: "gateway", proposalRef }).execute(
|
||||
"setup-proposal",
|
||||
args,
|
||||
);
|
||||
|
||||
expect(toolText(result)).toContain("needs-approval");
|
||||
expect(proposalRef.current).toBe(
|
||||
hashCrestodianOperation({
|
||||
hashSystemAgentOperation({
|
||||
kind: "setup",
|
||||
workspace: "/tmp/work",
|
||||
model: "openai/gpt-5.5",
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
resolveCrestodianProposalTransition({
|
||||
resolveSystemAgentProposalTransition({
|
||||
args: { action: "setup", workspace: "/tmp/work" },
|
||||
resultText: toolText(result),
|
||||
}),
|
||||
@@ -188,12 +188,12 @@ describe("crestodian tool", () => {
|
||||
|
||||
it("voids setup approval when the requested model changes", async () => {
|
||||
const proposalRef = {
|
||||
current: hashCrestodianOperation({
|
||||
current: hashSystemAgentOperation({
|
||||
kind: "setup",
|
||||
model: "openai/gpt-5.5",
|
||||
}),
|
||||
};
|
||||
const tool = createCrestodianTool({
|
||||
const tool = createSystemAgentTool({
|
||||
surface: "gateway",
|
||||
approvalArmed: true,
|
||||
proposalRef,
|
||||
@@ -207,18 +207,18 @@ describe("crestodian tool", () => {
|
||||
|
||||
expect(toolText(result)).toContain("approval-mismatch");
|
||||
expect(proposalRef.current).toBeUndefined();
|
||||
expect(mocks.executeCrestodianOperation).not.toHaveBeenCalled();
|
||||
expect(mocks.executeSystemAgentOperation).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("refuses an armed call that differs from the proposed operation", async () => {
|
||||
const proposalRef: { current?: string } = {};
|
||||
const proposingTool = createCrestodianTool({ surface: "cli", proposalRef });
|
||||
const proposingTool = createSystemAgentTool({ surface: "cli", proposalRef });
|
||||
await proposingTool.execute("t3c", {
|
||||
action: "set_default_model",
|
||||
model: "openai/gpt-5.5",
|
||||
approved: true,
|
||||
});
|
||||
const armedTool = createCrestodianTool({ surface: "cli", approvalArmed: true, proposalRef });
|
||||
const armedTool = createSystemAgentTool({ surface: "cli", approvalArmed: true, proposalRef });
|
||||
const result = await armedTool.execute("t3d", {
|
||||
action: "config_set",
|
||||
path: "gateway.port",
|
||||
@@ -236,19 +236,19 @@ describe("crestodian tool", () => {
|
||||
approved: true,
|
||||
});
|
||||
expect(toolText(retry)).toContain("approval-mismatch");
|
||||
expect(mocks.executeCrestodianOperation).not.toHaveBeenCalled();
|
||||
expect(mocks.executeSystemAgentOperation).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("never performs an approved write inside the model tool process", async () => {
|
||||
const proposalRef: { current?: string } = {};
|
||||
await createCrestodianTool({ surface: "cli", proposalRef }).execute("t4a", {
|
||||
await createSystemAgentTool({ surface: "cli", proposalRef }).execute("t4a", {
|
||||
action: "config_set",
|
||||
path: "gateway.port",
|
||||
value: "banana",
|
||||
approved: true,
|
||||
});
|
||||
const directiveRef: { current?: CrestodianToolDirective } = {};
|
||||
const tool = createCrestodianTool({
|
||||
const directiveRef: { current?: SystemAgentToolDirective } = {};
|
||||
const tool = createSystemAgentTool({
|
||||
surface: "cli",
|
||||
approvalArmed: true,
|
||||
proposalRef,
|
||||
@@ -265,20 +265,20 @@ describe("crestodian tool", () => {
|
||||
kind: "approved-operation",
|
||||
operation: { kind: "config-set", path: "gateway.port", value: "banana" },
|
||||
});
|
||||
expect(mocks.executeCrestodianOperation).not.toHaveBeenCalled();
|
||||
expect(mocks.executeSystemAgentOperation).not.toHaveBeenCalled();
|
||||
expect(mocks.readConfigFileSnapshot).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("maps create_agent with optional workspace and model", async () => {
|
||||
const proposalRef: { current?: string } = {};
|
||||
await createCrestodianTool({ surface: "cli", proposalRef }).execute("t6a", {
|
||||
await createSystemAgentTool({ surface: "cli", proposalRef }).execute("t6a", {
|
||||
action: "create_agent",
|
||||
agentId: "work",
|
||||
workspace: "/tmp/work",
|
||||
approved: true,
|
||||
});
|
||||
const directiveRef: { current?: CrestodianToolDirective } = {};
|
||||
const tool = createCrestodianTool({
|
||||
const directiveRef: { current?: SystemAgentToolDirective } = {};
|
||||
const tool = createSystemAgentTool({
|
||||
surface: "cli",
|
||||
approvalArmed: true,
|
||||
proposalRef,
|
||||
@@ -294,17 +294,17 @@ describe("crestodian tool", () => {
|
||||
kind: "approved-operation",
|
||||
operation: { kind: "create-agent", agentId: "work", workspace: "/tmp/work" },
|
||||
});
|
||||
expect(mocks.executeCrestodianOperation).not.toHaveBeenCalled();
|
||||
expect(mocks.executeSystemAgentOperation).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("rejects unknown or underspecified actions as input errors", async () => {
|
||||
const tool = createCrestodianTool({ surface: "cli" });
|
||||
const tool = createSystemAgentTool({ surface: "cli" });
|
||||
await expect(tool.execute("t5", { action: "config_get" })).rejects.toThrow(/path/);
|
||||
});
|
||||
|
||||
it("records interactive directives for the host without executing operations", async () => {
|
||||
const directiveRef: { current?: CrestodianToolDirective } = {};
|
||||
const tool = createCrestodianTool({ surface: "cli", directiveRef });
|
||||
const directiveRef: { current?: SystemAgentToolDirective } = {};
|
||||
const tool = createSystemAgentTool({ surface: "cli", directiveRef });
|
||||
|
||||
const connect = await tool.execute("t5", { action: "connect_channel", channel: "Telegram" });
|
||||
expect(toolText(connect)).toContain("directive:");
|
||||
@@ -316,7 +316,7 @@ describe("crestodian tool", () => {
|
||||
});
|
||||
expect(toolText(configureModel)).toContain("directive:");
|
||||
expect(toolText(configureModel)).toContain(
|
||||
"active inference route cannot be changed inside Crestodian",
|
||||
"active inference route cannot be changed inside OpenClaw",
|
||||
);
|
||||
expect(toolText(configureModel)).toContain("openclaw onboard");
|
||||
expect(directiveRef.current).toEqual({ kind: "model-setup", workspace: "/tmp/work" });
|
||||
@@ -341,17 +341,17 @@ describe("crestodian tool", () => {
|
||||
action: "open_setup",
|
||||
target: "guided",
|
||||
});
|
||||
expect(toolText(guidedSetup)).toContain("cannot run inside Crestodian");
|
||||
expect(toolText(guidedSetup)).toContain("cannot run inside OpenClaw");
|
||||
expect(toolText(guidedSetup)).toContain("openclaw onboard");
|
||||
expect(directiveRef.current).toEqual({ kind: "open-setup", target: "guided" });
|
||||
|
||||
// Directives are host handoffs, never operation executions.
|
||||
expect(mocks.executeCrestodianOperation).not.toHaveBeenCalled();
|
||||
expect(mocks.executeSystemAgentOperation).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("mirrors directive transitions for out-of-process (CLI MCP) hosts", () => {
|
||||
expect(
|
||||
resolveCrestodianDirectiveTransition({
|
||||
resolveSystemAgentDirectiveTransition({
|
||||
args: {
|
||||
action: "config_set",
|
||||
path: "gateway.port",
|
||||
@@ -365,36 +365,36 @@ describe("crestodian tool", () => {
|
||||
operation: { kind: "config-set", path: "gateway.port", value: "19001" },
|
||||
});
|
||||
expect(
|
||||
resolveCrestodianDirectiveTransition({
|
||||
resolveSystemAgentDirectiveTransition({
|
||||
args: { action: "connect_channel", channel: "telegram" },
|
||||
resultText: "directive: the host chat now starts the guided telegram setup.",
|
||||
}),
|
||||
).toEqual({ kind: "channel-setup", channel: "telegram" });
|
||||
expect(
|
||||
resolveCrestodianDirectiveTransition({
|
||||
resolveSystemAgentDirectiveTransition({
|
||||
args: { action: "open_agent" },
|
||||
resultText: "directive: the host now hands the user over.",
|
||||
}),
|
||||
).toEqual({ kind: "open-tui" });
|
||||
expect(
|
||||
resolveCrestodianDirectiveTransition({
|
||||
resolveSystemAgentDirectiveTransition({
|
||||
args: { action: "configure_model_provider", workspace: "/tmp/work" },
|
||||
resultText:
|
||||
"directive: the active inference route cannot be changed inside Crestodian; run openclaw onboard.",
|
||||
"directive: the active inference route cannot be changed inside OpenClaw; run openclaw onboard.",
|
||||
}),
|
||||
).toEqual({ kind: "model-setup", workspace: "/tmp/work" });
|
||||
expect(
|
||||
resolveCrestodianDirectiveTransition({
|
||||
resolveSystemAgentDirectiveTransition({
|
||||
args: { action: "open_setup", target: "classic" },
|
||||
resultText: "directive: classic setup cannot run inside Crestodian; run openclaw onboard.",
|
||||
resultText: "directive: classic setup cannot run inside OpenClaw; run openclaw onboard.",
|
||||
}),
|
||||
).toEqual({ kind: "open-setup", target: "classic" });
|
||||
// Non-directive results and other actions never mirror.
|
||||
expect(
|
||||
resolveCrestodianDirectiveTransition({ args: { action: "status" }, resultText: "ok" }),
|
||||
resolveSystemAgentDirectiveTransition({ args: { action: "status" }, resultText: "ok" }),
|
||||
).toBeNull();
|
||||
expect(
|
||||
resolveCrestodianDirectiveTransition({
|
||||
resolveSystemAgentDirectiveTransition({
|
||||
args: { action: "connect_channel", channel: "telegram" },
|
||||
resultText: "error: boom",
|
||||
}),
|
||||
@@ -403,38 +403,38 @@ describe("crestodian tool", () => {
|
||||
|
||||
it("mirrors proposal transitions for out-of-process (CLI MCP) hosts", () => {
|
||||
const args = { action: "set_default_model", model: "openai/gpt-5.5" };
|
||||
const hash = hashCrestodianOperation({ kind: "set-default-model", model: "openai/gpt-5.5" });
|
||||
const hash = hashSystemAgentOperation({ kind: "set-default-model", model: "openai/gpt-5.5" });
|
||||
|
||||
// Denial registers the exact-operation hash on the host.
|
||||
expect(
|
||||
resolveCrestodianProposalTransition({
|
||||
resolveSystemAgentProposalTransition({
|
||||
args,
|
||||
resultText: "needs-approval: this action changes state.",
|
||||
}),
|
||||
).toEqual({ proposal: hash });
|
||||
expect(
|
||||
resolveCrestodianProposalTransition({
|
||||
resolveSystemAgentProposalTransition({
|
||||
args,
|
||||
resultText: `needs-approval:${hash}\nThis action changes state.`,
|
||||
}),
|
||||
).toEqual({ proposal: hash });
|
||||
// A voided approval clears it.
|
||||
expect(
|
||||
resolveCrestodianProposalTransition({
|
||||
resolveSystemAgentProposalTransition({
|
||||
args,
|
||||
resultText: "approval-mismatch: this call is not the operation the user approved.",
|
||||
}),
|
||||
).toEqual({ proposal: undefined });
|
||||
// An executed mutation consumes it.
|
||||
expect(
|
||||
resolveCrestodianProposalTransition({ args, resultText: "Default model updated." }),
|
||||
resolveSystemAgentProposalTransition({ args, resultText: "Default model updated." }),
|
||||
).toEqual({ proposal: undefined });
|
||||
// Read actions and unparsable calls never touch the proposal.
|
||||
expect(
|
||||
resolveCrestodianProposalTransition({ args: { action: "status" }, resultText: "ok" }),
|
||||
resolveSystemAgentProposalTransition({ args: { action: "status" }, resultText: "ok" }),
|
||||
).toBeNull();
|
||||
expect(
|
||||
resolveCrestodianProposalTransition({ args: { action: "bogus" }, resultText: "ok" }),
|
||||
resolveSystemAgentProposalTransition({ args: { action: "bogus" }, resultText: "ok" }),
|
||||
).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -1,23 +1,23 @@
|
||||
/**
|
||||
* crestodian built-in tool: ring-zero setup/repair actions for the Crestodian
|
||||
* openclaw built-in tool: ring-zero setup/repair actions for the OpenClaw
|
||||
* agent. Never exposed to normal agents — construction is bound to a host-owned
|
||||
* per-run scope, and every action funnels through Crestodian's typed operation
|
||||
* per-run scope, and every action funnels through OpenClaw's typed operation
|
||||
* union with approval assertions and the audit log.
|
||||
*/
|
||||
import { createHash } from "node:crypto";
|
||||
import { Type } from "typebox";
|
||||
import {
|
||||
executeCrestodianOperation,
|
||||
isPersistentCrestodianOperation,
|
||||
type CrestodianOperation,
|
||||
} from "../../crestodian/operations.js";
|
||||
import { validateCrestodianPluginInstallSpec } from "../../crestodian/plugin-install.js";
|
||||
import type { RuntimeEnv } from "../../runtime.js";
|
||||
import {
|
||||
executeSystemAgentOperation,
|
||||
isPersistentSystemAgentOperation,
|
||||
type SystemAgentOperation,
|
||||
} from "../../system-agent/operations.js";
|
||||
import { validateSystemAgentPluginInstallSpec } from "../../system-agent/plugin-install.js";
|
||||
import { stringEnum } from "../schema/typebox.js";
|
||||
import { stableStringify } from "../stable-stringify.js";
|
||||
import { textResult, ToolInputError, readStringParam, type AnyAgentTool } from "./common.js";
|
||||
|
||||
export type CrestodianToolOptions = {
|
||||
export type SystemAgentToolOptions = {
|
||||
/** Where setup side effects run; the gateway surface never manages its own daemon. */
|
||||
surface: "cli" | "gateway";
|
||||
/**
|
||||
@@ -39,50 +39,50 @@ export type CrestodianToolOptions = {
|
||||
* agent TUI). The engine reads it after the turn; CLI MCP hosts mirror it
|
||||
* from tool events.
|
||||
*/
|
||||
directiveRef?: { current?: CrestodianToolDirective };
|
||||
directiveRef?: { current?: SystemAgentToolDirective };
|
||||
};
|
||||
|
||||
/** Host directives the hosting chat engine handles after the turn. */
|
||||
export type CrestodianToolDirective =
|
||||
export type SystemAgentToolDirective =
|
||||
| { kind: "channel-setup"; channel: string }
|
||||
| { kind: "model-setup"; workspace?: string }
|
||||
| { kind: "open-tui"; agentId?: string; workspace?: string }
|
||||
| Extract<CrestodianOperation, { kind: "open-setup" }>
|
||||
| { kind: "approved-operation"; operation: CrestodianOperation };
|
||||
| Extract<SystemAgentOperation, { kind: "open-setup" }>
|
||||
| { kind: "approved-operation"; operation: SystemAgentOperation };
|
||||
|
||||
type CrestodianHostNavigationDirective = Exclude<
|
||||
CrestodianToolDirective,
|
||||
type SystemAgentHostNavigationDirective = Exclude<
|
||||
SystemAgentToolDirective,
|
||||
{ kind: "approved-operation" }
|
||||
>;
|
||||
|
||||
/** Canonical operation fingerprint used to bind "yes" to one exact mutation. */
|
||||
export function hashCrestodianOperation(operation: CrestodianOperation): string {
|
||||
export function hashSystemAgentOperation(operation: SystemAgentOperation): string {
|
||||
return createHash("sha256").update(stableStringify(operation)).digest("hex");
|
||||
}
|
||||
|
||||
/** Result markers shared with out-of-process hosts (CLI MCP runs). */
|
||||
const CRESTODIAN_NEEDS_APPROVAL_PREFIX = "needs-approval:";
|
||||
const CRESTODIAN_APPROVAL_MISMATCH_PREFIX = "approval-mismatch:";
|
||||
const CRESTODIAN_DIRECTIVE_PREFIX = "directive:";
|
||||
const CRESTODIAN_APPROVED_OPERATION_PREFIX = `${CRESTODIAN_DIRECTIVE_PREFIX}approved-operation:`;
|
||||
const SYSTEM_AGENT_NEEDS_APPROVAL_PREFIX = "needs-approval:";
|
||||
const SYSTEM_AGENT_APPROVAL_MISMATCH_PREFIX = "approval-mismatch:";
|
||||
const SYSTEM_AGENT_DIRECTIVE_PREFIX = "directive:";
|
||||
const SYSTEM_AGENT_APPROVED_OPERATION_PREFIX = `${SYSTEM_AGENT_DIRECTIVE_PREFIX}approved-operation:`;
|
||||
|
||||
/**
|
||||
* Reconstruct a host directive from an out-of-process tool result. Directive
|
||||
* actions run inside the MCP subprocess on CLI-harness runs, so the host
|
||||
* replays them from harness tool events the same way proposals are mirrored.
|
||||
*/
|
||||
export function resolveCrestodianDirectiveTransition(params: {
|
||||
export function resolveSystemAgentDirectiveTransition(params: {
|
||||
args: Record<string, unknown>;
|
||||
resultText: string;
|
||||
}): CrestodianToolDirective | null {
|
||||
if (!params.resultText.startsWith(CRESTODIAN_DIRECTIVE_PREFIX)) {
|
||||
}): SystemAgentToolDirective | null {
|
||||
if (!params.resultText.startsWith(SYSTEM_AGENT_DIRECTIVE_PREFIX)) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const operation = operationForAction(params.args);
|
||||
if (
|
||||
params.resultText.startsWith(CRESTODIAN_APPROVED_OPERATION_PREFIX) &&
|
||||
isPersistentCrestodianOperation(operation)
|
||||
params.resultText.startsWith(SYSTEM_AGENT_APPROVED_OPERATION_PREFIX) &&
|
||||
isPersistentSystemAgentOperation(operation)
|
||||
) {
|
||||
return { kind: "approved-operation", operation };
|
||||
}
|
||||
@@ -93,8 +93,8 @@ export function resolveCrestodianDirectiveTransition(params: {
|
||||
}
|
||||
|
||||
function directiveForOperation(
|
||||
operation: CrestodianOperation,
|
||||
): CrestodianHostNavigationDirective | null {
|
||||
operation: SystemAgentOperation,
|
||||
): SystemAgentHostNavigationDirective | null {
|
||||
if (operation.kind === "channel-setup") {
|
||||
return { kind: "channel-setup", channel: operation.channel };
|
||||
}
|
||||
@@ -123,36 +123,36 @@ function directiveForOperation(
|
||||
* run; the host replays the same lifecycle from harness tool events: denial
|
||||
* registers the exact-operation hash, mismatch voids it, execution consumes it.
|
||||
*/
|
||||
export function resolveCrestodianProposalTransition(params: {
|
||||
export function resolveSystemAgentProposalTransition(params: {
|
||||
args: Record<string, unknown>;
|
||||
resultText: string;
|
||||
}): { proposal: string | undefined } | null {
|
||||
let operation: CrestodianOperation;
|
||||
let operation: SystemAgentOperation;
|
||||
try {
|
||||
operation = operationForAction(params.args);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
if (!isPersistentCrestodianOperation(operation)) {
|
||||
if (!isPersistentSystemAgentOperation(operation)) {
|
||||
return null;
|
||||
}
|
||||
if (params.resultText.startsWith(CRESTODIAN_APPROVAL_MISMATCH_PREFIX)) {
|
||||
if (params.resultText.startsWith(SYSTEM_AGENT_APPROVAL_MISMATCH_PREFIX)) {
|
||||
return { proposal: undefined };
|
||||
}
|
||||
if (params.resultText.startsWith(CRESTODIAN_NEEDS_APPROVAL_PREFIX)) {
|
||||
if (params.resultText.startsWith(SYSTEM_AGENT_NEEDS_APPROVAL_PREFIX)) {
|
||||
const markerLine = params.resultText.split("\n", 1)[0] ?? "";
|
||||
const carriedHash = markerLine.slice(CRESTODIAN_NEEDS_APPROVAL_PREFIX.length).trim();
|
||||
const carriedHash = markerLine.slice(SYSTEM_AGENT_NEEDS_APPROVAL_PREFIX.length).trim();
|
||||
return {
|
||||
proposal: /^[a-f0-9]{64}$/.test(carriedHash)
|
||||
? carriedHash
|
||||
: hashCrestodianOperation(operation),
|
||||
: hashSystemAgentOperation(operation),
|
||||
};
|
||||
}
|
||||
// Executed or errored mutation: an armed approval is single-use either way.
|
||||
return { proposal: undefined };
|
||||
}
|
||||
|
||||
const CRESTODIAN_TOOL_ACTIONS = [
|
||||
const SYSTEM_AGENT_TOOL_ACTIONS = [
|
||||
"status",
|
||||
"models",
|
||||
"agents",
|
||||
@@ -183,8 +183,8 @@ const CRESTODIAN_TOOL_ACTIONS = [
|
||||
"plugin_uninstall",
|
||||
] as const;
|
||||
|
||||
const CrestodianToolSchema = Type.Object({
|
||||
action: stringEnum([...CRESTODIAN_TOOL_ACTIONS]),
|
||||
const SystemAgentToolSchema = Type.Object({
|
||||
action: stringEnum([...SYSTEM_AGENT_TOOL_ACTIONS]),
|
||||
path: Type.Optional(Type.String({ description: "Config path for config_* actions" })),
|
||||
value: Type.Optional(Type.String({ description: "Value for config_set (JSON5 or string)" })),
|
||||
envVar: Type.Optional(Type.String({ description: "Env var name for config_set_ref" })),
|
||||
@@ -199,7 +199,7 @@ const CrestodianToolSchema = Type.Object({
|
||||
target: Type.Optional(
|
||||
stringEnum(["guided", "classic", "channels"], {
|
||||
description:
|
||||
"Setup target for open_setup. channels runs in this chat; guided/classic require exiting Crestodian and running openclaw onboard.",
|
||||
"Setup target for open_setup. channels runs in this chat; guided/classic require exiting OpenClaw and running openclaw onboard.",
|
||||
}),
|
||||
),
|
||||
query: Type.Optional(Type.String({ description: "Search query for plugin_search" })),
|
||||
@@ -219,7 +219,7 @@ function createCaptureRuntime(): RuntimeEnv & { read: () => string } {
|
||||
log: (...args) => lines.push(args.join(" ")),
|
||||
error: (...args) => lines.push(args.join(" ")),
|
||||
exit: (code) => {
|
||||
throw new Error(`crestodian operation exited with code ${String(code)}`);
|
||||
throw new Error(`openclaw operation exited with code ${String(code)}`);
|
||||
},
|
||||
read: () => lines.join("\n").trim(),
|
||||
};
|
||||
@@ -228,7 +228,7 @@ function createCaptureRuntime(): RuntimeEnv & { read: () => string } {
|
||||
function requireParam(params: Record<string, unknown>, name: string): string {
|
||||
const value = readStringParam(params, name);
|
||||
if (!value?.trim()) {
|
||||
throw new ToolInputError(`crestodian: "${name}" is required for this action`);
|
||||
throw new ToolInputError(`openclaw: "${name}" is required for this action`);
|
||||
}
|
||||
return value.trim();
|
||||
}
|
||||
@@ -238,10 +238,10 @@ function readSetupTarget(params: Record<string, unknown>): "guided" | "classic"
|
||||
if (target === "guided" || target === "classic" || target === "channels") {
|
||||
return target;
|
||||
}
|
||||
throw new ToolInputError(`crestodian: unknown setup target "${target}"`);
|
||||
throw new ToolInputError(`openclaw: unknown setup target "${target}"`);
|
||||
}
|
||||
|
||||
function operationForAction(params: Record<string, unknown>): CrestodianOperation {
|
||||
function operationForAction(params: Record<string, unknown>): SystemAgentOperation {
|
||||
const action = readStringParam(params, "action", { required: true });
|
||||
switch (action) {
|
||||
case "status":
|
||||
@@ -302,9 +302,9 @@ function operationForAction(params: Record<string, unknown>): CrestodianOperatio
|
||||
return { kind: "plugin-search", query: requireParam(params, "query") };
|
||||
case "plugin_install": {
|
||||
const spec = requireParam(params, "spec");
|
||||
const validationError = validateCrestodianPluginInstallSpec(spec);
|
||||
const validationError = validateSystemAgentPluginInstallSpec(spec);
|
||||
if (validationError) {
|
||||
throw new ToolInputError(`crestodian: ${validationError}`);
|
||||
throw new ToolInputError(`openclaw: ${validationError}`);
|
||||
}
|
||||
return { kind: "plugin-install", spec };
|
||||
}
|
||||
@@ -345,28 +345,28 @@ function operationForAction(params: Record<string, unknown>): CrestodianOperatio
|
||||
id: requireParam(params, "envVar"),
|
||||
};
|
||||
default:
|
||||
throw new ToolInputError(`crestodian: unknown action "${action}"`);
|
||||
throw new ToolInputError(`openclaw: unknown action "${action}"`);
|
||||
}
|
||||
}
|
||||
|
||||
export function createCrestodianTool(options: CrestodianToolOptions): AnyAgentTool {
|
||||
export function createSystemAgentTool(options: SystemAgentToolOptions): AnyAgentTool {
|
||||
return {
|
||||
name: "crestodian",
|
||||
label: "Crestodian",
|
||||
name: "openclaw",
|
||||
label: "OpenClaw",
|
||||
// Setup authority is never discoverable through tool catalogs: the host
|
||||
// scopes it to this run and the model must receive it directly.
|
||||
catalogMode: "direct-only",
|
||||
description: [
|
||||
"Ring-zero setup/repair. Reads (status/models/agents/channels/channel_info/config_get/config_schema/gateway_status/plugin_search/validate_config/doctor/audit) run now.",
|
||||
"connect_channel/open_setup(target=channels) starts guided chat setup; open_agent hands off normal agent.",
|
||||
"Cannot change active inference route here. configure_model_provider/open_setup guided|classic, provider/credential/default-model change: exit and `openclaw onboard`; never ask credentials here.",
|
||||
"Writes (setup/set_default_model/config_set/config_set_ref/create_agent/gateway_*/plugin_install) need approved=true only after user clearly agrees to exact change in this conversation. Host applies after turn and rechecks live inference owner.",
|
||||
"plugin_install accepts only ClawHub, bundled, or official-catalog plugins; direct arbitrary-source installs belong in a trusted shell.",
|
||||
"Unknown config path: config_schema first; schema is truth. Secret: config_set_ref env, never plaintext. Raw auth/models/env/secrets/plugins/tools/agent-route/$include writes refused; typed workflow only.",
|
||||
"Plugin uninstall refused: exit, CLI. Doctor repair refused: exit, `openclaw doctor --fix`.",
|
||||
"Every write validated/audited. CONFIG INVALID => fix immediately.",
|
||||
"System agent. Setup, config, channels, plugins, agents, repair.",
|
||||
"Read now: status, models, agents, channels, channel_info, config_get, config_schema, gateway_status, plugin_search, validate_config, doctor, audit.",
|
||||
"Handoff: connect_channel; open_setup target=channels; open_agent.",
|
||||
"Inference, provider, auth, credentials: exit; run `openclaw onboard`. Never request credentials.",
|
||||
"Write: setup, set_default_model, config_set, config_set_ref, create_agent, gateway_*, plugin_install. Exact user approval required; then approved=true. Host applies after turn; rechecks inference owner.",
|
||||
"plugin_install: ClawHub/bundled/official only. Arbitrary source: exit, trusted shell.",
|
||||
"Unknown config: config_schema first. Secrets: config_set_ref env. No plaintext. No raw auth/models/env/secrets/plugins/tools/agent-route/$include; typed workflows.",
|
||||
"No plugin uninstall. No doctor repair. Writes validated, audited. Invalid config: fix now.",
|
||||
].join(" "),
|
||||
parameters: CrestodianToolSchema,
|
||||
parameters: SystemAgentToolSchema,
|
||||
execute: async (_toolCallId, args) => {
|
||||
const params = (args ?? {}) as Record<string, unknown>;
|
||||
const operation = operationForAction(params);
|
||||
@@ -379,20 +379,20 @@ export function createCrestodianTool(options: CrestodianToolOptions): AnyAgentTo
|
||||
}
|
||||
return textResult(
|
||||
directive.kind === "channel-setup"
|
||||
? `${CRESTODIAN_DIRECTIVE_PREFIX} the host chat now starts the guided ${directive.channel} setup with the user. Tell the user the setup questions come next; do not describe steps yourself.`
|
||||
? `${SYSTEM_AGENT_DIRECTIVE_PREFIX} the host chat now starts the guided ${directive.channel} setup with the user. Tell the user the setup questions come next; do not describe steps yourself.`
|
||||
: directive.kind === "model-setup"
|
||||
? `${CRESTODIAN_DIRECTIVE_PREFIX} the active inference route cannot be changed inside Crestodian. Tell the user to exit Crestodian and run \`openclaw onboard\`; do not ask for provider credentials here.`
|
||||
? `${SYSTEM_AGENT_DIRECTIVE_PREFIX} the active inference route cannot be changed inside OpenClaw. Tell the user to exit OpenClaw and run \`openclaw onboard\`; do not ask for provider credentials here.`
|
||||
: directive.kind === "open-tui"
|
||||
? `${CRESTODIAN_DIRECTIVE_PREFIX} the host now hands the user over to their normal agent. Say goodbye briefly.`
|
||||
? `${SYSTEM_AGENT_DIRECTIVE_PREFIX} the host now hands the user over to their normal agent. Say goodbye briefly.`
|
||||
: directive.target === "channels"
|
||||
? `${CRESTODIAN_DIRECTIVE_PREFIX} the host now opens channel setup${directive.channel ? ` for ${directive.channel}` : ""}. Tell the user the channel setup questions come next.`
|
||||
: `${CRESTODIAN_DIRECTIVE_PREFIX} ${directive.target} setup cannot run inside Crestodian because it may change the active inference route. Tell the user to exit Crestodian and run \`openclaw onboard\`.`,
|
||||
? `${SYSTEM_AGENT_DIRECTIVE_PREFIX} the host now opens channel setup${directive.channel ? ` for ${directive.channel}` : ""}. Tell the user the channel setup questions come next.`
|
||||
: `${SYSTEM_AGENT_DIRECTIVE_PREFIX} ${directive.target} setup cannot run inside OpenClaw because it may change the active inference route. Tell the user to exit OpenClaw and run \`openclaw onboard\`.`,
|
||||
{},
|
||||
);
|
||||
}
|
||||
const persistent = isPersistentCrestodianOperation(operation);
|
||||
const persistent = isPersistentSystemAgentOperation(operation);
|
||||
if (persistent) {
|
||||
const operationHash = hashCrestodianOperation(operation);
|
||||
const operationHash = hashSystemAgentOperation(operation);
|
||||
const armedForThisOperation =
|
||||
params.approved === true &&
|
||||
options.approvalArmed === true &&
|
||||
@@ -409,7 +409,7 @@ export function createCrestodianTool(options: CrestodianToolOptions): AnyAgentTo
|
||||
options.proposalRef.current = undefined;
|
||||
}
|
||||
return textResult(
|
||||
`${CRESTODIAN_APPROVAL_MISMATCH_PREFIX} this call is not the operation the user approved. The approval is void; describe the new change and get a fresh yes before retrying.`,
|
||||
`${SYSTEM_AGENT_APPROVAL_MISMATCH_PREFIX} this call is not the operation the user approved. The approval is void; describe the new change and get a fresh yes before retrying.`,
|
||||
{ needsApproval: true },
|
||||
);
|
||||
}
|
||||
@@ -417,7 +417,7 @@ export function createCrestodianTool(options: CrestodianToolOptions): AnyAgentTo
|
||||
options.proposalRef.current = operationHash;
|
||||
}
|
||||
return textResult(
|
||||
`${CRESTODIAN_NEEDS_APPROVAL_PREFIX}${operationHash}\nThis action changes state. The proposal is registered; describe this exact change and ask the user to reply yes (their approval unlocks THIS action only — then retry the exact registered operation with approved=true).`,
|
||||
`${SYSTEM_AGENT_NEEDS_APPROVAL_PREFIX}${operationHash}\nThis action changes state. The proposal is registered; describe this exact change and ask the user to reply yes (their approval unlocks THIS action only — then retry the exact registered operation with approved=true).`,
|
||||
{ needsApproval: true },
|
||||
);
|
||||
}
|
||||
@@ -425,7 +425,7 @@ export function createCrestodianTool(options: CrestodianToolOptions): AnyAgentTo
|
||||
// One approval, one mutation: re-proposals need a fresh yes.
|
||||
options.proposalRef.current = undefined;
|
||||
}
|
||||
const approvedDirective: CrestodianToolDirective = {
|
||||
const approvedDirective: SystemAgentToolDirective = {
|
||||
kind: "approved-operation",
|
||||
operation,
|
||||
};
|
||||
@@ -436,13 +436,13 @@ export function createCrestodianTool(options: CrestodianToolOptions): AnyAgentTo
|
||||
// its out-of-process MCP server. The host rechecks the verified
|
||||
// inference binding immediately before applying this exact operation.
|
||||
return textResult(
|
||||
`${CRESTODIAN_APPROVED_OPERATION_PREFIX} the host accepted this exact approved action and will apply it after this turn. Do not call it again.`,
|
||||
`${SYSTEM_AGENT_APPROVED_OPERATION_PREFIX} the host accepted this exact approved action and will apply it after this turn. Do not call it again.`,
|
||||
{},
|
||||
);
|
||||
}
|
||||
const capture = createCaptureRuntime();
|
||||
try {
|
||||
await executeCrestodianOperation(operation, capture, {
|
||||
await executeSystemAgentOperation(operation, capture, {
|
||||
approved: false,
|
||||
deps: { setupSurface: options.surface },
|
||||
});
|
||||
@@ -317,9 +317,9 @@ export function buildBuiltinChatCommands(
|
||||
],
|
||||
}),
|
||||
defineChatCommand({
|
||||
key: "crestodian",
|
||||
description: "Run the Crestodian setup and repair helper.",
|
||||
textAlias: "/crestodian",
|
||||
key: "openclaw",
|
||||
description: "Run the OpenClaw setup and repair helper.",
|
||||
textAlias: "/openclaw",
|
||||
acceptsArgs: true,
|
||||
scope: "text",
|
||||
category: "management",
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
// Implements maintenance commands for Crestodian-backed session cleanup.
|
||||
import { logVerbose } from "../../globals.js";
|
||||
import type { CommandHandler } from "./commands-types.js";
|
||||
|
||||
export const handleCrestodianCommand: CommandHandler = async (params, allowTextCommands) => {
|
||||
if (!allowTextCommands) {
|
||||
return null;
|
||||
}
|
||||
const { extractCrestodianRescueMessage, runCrestodianRescueMessage } =
|
||||
await import("../../crestodian/rescue-message.js");
|
||||
if (extractCrestodianRescueMessage(params.command.commandBodyNormalized) === null) {
|
||||
return null;
|
||||
}
|
||||
if (!params.command.isAuthorizedSender) {
|
||||
logVerbose(
|
||||
`Ignoring /crestodian from unauthorized sender: ${params.command.senderId || "<unknown>"}`,
|
||||
);
|
||||
return { shouldContinue: false };
|
||||
}
|
||||
return {
|
||||
shouldContinue: false,
|
||||
reply: {
|
||||
text:
|
||||
(await runCrestodianRescueMessage({
|
||||
cfg: params.cfg,
|
||||
command: params.command,
|
||||
commandBody: params.command.commandBodyNormalized,
|
||||
agentId: params.agentId,
|
||||
isGroup: params.isGroup,
|
||||
})) ?? "Crestodian did not find a rescue request.",
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -7,7 +7,6 @@ import { handleBtwCommand } from "./commands-btw.js";
|
||||
import { handleCompactCommand } from "./commands-compact.js";
|
||||
import { handleConfigCommand, handleDebugCommand } from "./commands-config.js";
|
||||
import { handleContextCommand } from "./commands-context-command.js";
|
||||
import { handleCrestodianCommand } from "./commands-crestodian.js";
|
||||
import { handleDiagnosticsCommand } from "./commands-diagnostics.js";
|
||||
import { handleDockCommand } from "./commands-dock.js";
|
||||
import { handleGoalCommand } from "./commands-goal.js";
|
||||
@@ -39,6 +38,7 @@ import {
|
||||
} from "./commands-session.js";
|
||||
import { handleSteerCommand } from "./commands-steer.js";
|
||||
import { handleSubagentsCommand } from "./commands-subagents.js";
|
||||
import { handleSystemAgentCommand } from "./commands-system-agent.js";
|
||||
import { handleTasksCommand } from "./commands-tasks.js";
|
||||
import { handleTtsCommands } from "./commands-tts.js";
|
||||
import type { CommandHandler } from "./commands-types.js";
|
||||
@@ -77,7 +77,7 @@ export function loadCommandHandlers(): CommandHandler[] {
|
||||
handleExportSessionCommand,
|
||||
handleExportTrajectoryCommand,
|
||||
handleWhoamiCommand,
|
||||
handleCrestodianCommand,
|
||||
handleSystemAgentCommand,
|
||||
handleSubagentsCommand,
|
||||
handleAcpCommand,
|
||||
handleMcpCommand,
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
// Implements maintenance commands for OpenClaw-backed session cleanup.
|
||||
import { logVerbose } from "../../globals.js";
|
||||
import type { CommandHandler } from "./commands-types.js";
|
||||
|
||||
export const handleSystemAgentCommand: CommandHandler = async (params, allowTextCommands) => {
|
||||
if (!allowTextCommands) {
|
||||
return null;
|
||||
}
|
||||
const { extractSystemAgentRescueMessage, runSystemAgentRescueMessage } =
|
||||
await import("../../system-agent/rescue-message.js");
|
||||
if (extractSystemAgentRescueMessage(params.command.commandBodyNormalized) === null) {
|
||||
return null;
|
||||
}
|
||||
if (!params.command.isAuthorizedSender) {
|
||||
logVerbose(
|
||||
`Ignoring /openclaw from unauthorized sender: ${params.command.senderId || "<unknown>"}`,
|
||||
);
|
||||
return { shouldContinue: false };
|
||||
}
|
||||
return {
|
||||
shouldContinue: false,
|
||||
reply: {
|
||||
text:
|
||||
(await runSystemAgentRescueMessage({
|
||||
cfg: params.cfg,
|
||||
command: params.command,
|
||||
commandBody: params.command.commandBodyNormalized,
|
||||
agentId: params.agentId,
|
||||
isGroup: params.isGroup,
|
||||
})) ?? "OpenClaw did not find a rescue request.",
|
||||
},
|
||||
};
|
||||
};
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user