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:
Peter Steinberger
2026-07-14 11:03:02 -07:00
parent 8456719a90
commit a6a0716486
223 changed files with 4961 additions and 4484 deletions
@@ -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>
+1 -1
View File
@@ -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,
+9 -9
View File
@@ -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,
+11 -11
View File
@@ -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)
}
@@ -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)
}
@@ -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
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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>
+353
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+7 -7
View File
@@ -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`
+2 -2
View File
@@ -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.
+1 -1
View File
@@ -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
+3 -3
View File
@@ -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
+16 -16
View File
@@ -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
+5 -5
View File
@@ -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
View File
@@ -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
+1 -1
View File
@@ -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
View File
@@ -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.
+6 -6
View File
@@ -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",
+15 -15
View File
@@ -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
+11 -11
View File
@@ -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 () => {
+9 -9
View File
@@ -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(
+11 -11
View File
@@ -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 () => {
+1 -1
View File
@@ -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
View File
@@ -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",
+37 -37
View File
@@ -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,
+1 -1
View File
@@ -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;
/**
@@ -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",
}),
@@ -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,
@@ -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
@@ -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,
+70 -6
View File
@@ -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",
@@ -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
@@ -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",
@@ -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) => {
@@ -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
+3 -3
View File
@@ -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(
+3 -3
View File
@@ -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),
);
+24 -24
View 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");
});
+3 -3
View File
@@ -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,
+8 -8
View File
@@ -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();
+1 -1
View File
@@ -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;
}
+2 -2
View File
@@ -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: {} });
+2 -2
View File
@@ -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?.();
+9 -9
View File
@@ -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>;
+15 -15
View File
@@ -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,
+10 -10
View File
@@ -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,
+1 -1
View File
@@ -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" },
+3 -3
View File
@@ -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,
+36 -36
View File
@@ -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 () => {
+14 -14
View File
@@ -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();
});
});
+2 -2
View File
@@ -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: {
@@ -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 },
});
+3 -3
View File
@@ -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