Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,20 @@

All notable changes to this project are documented in this file.

## 0.5.7 - 2026-04-01

### Native macOS SpiderApp Support
- Fixed Spiderweb’s workspace-capability routing so the new native macOS SpiderApp can load packages, binds, and terminal capabilities through the same shared-core backend path as the CLI.
- Corrected local capability binding resolution and stale bind refresh behavior so workspace aliases track the real local venom directories instead of getting stuck on outdated short names.

### Native Terminal Routing
- Fixed terminal venom namespace seeding and control-file routing so native SpiderApp terminal exec reaches the real local terminal service instead of dead-ending on missing or misrouted control files.
- Added explicit host-versus-workspace shell support in the terminal backend so SpiderApp can steer agents and users into the correct execution root.

### macOS Runtime Packaging
- Hardened the signed macOS release path around the bundled native SpiderApp/Spiderweb tools, including notarized package validation for the updated suite packaging flow.
- Fixed the macOS terminal runtime to avoid assuming GNU `timeout` is present on user machines.

## 0.5.6 - 2026-03-27

### Hosted Registry E2E
Expand Down
2 changes: 1 addition & 1 deletion build.zig.zon
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
.{
.name = .ziggy_spiderweb,
.version = "0.5.6",
.version = "0.5.7",
.fingerprint = 0xe46867958f0d9203,
.minimum_zig_version = "0.13.0",
.dependencies = .{
Expand Down
8 changes: 4 additions & 4 deletions platform/macos/SpiderwebFSKit.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 26.0;
MARKETING_VERSION = 0.5.6;
MARKETING_VERSION = 0.5.7;
PRODUCT_BUNDLE_IDENTIFIER = com.deanoc.spiderweb.fskit.app;
PRODUCT_NAME = Spiderweb;
PROVISIONING_PROFILE_SPECIFIER = "";
Expand Down Expand Up @@ -439,7 +439,7 @@
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 26.0;
MARKETING_VERSION = 0.5.6;
MARKETING_VERSION = 0.5.7;
PRODUCT_BUNDLE_IDENTIFIER = com.deanoc.spiderweb.fskit.app;
PRODUCT_NAME = Spiderweb;
PROVISIONING_PROFILE_SPECIFIER = "";
Expand Down Expand Up @@ -469,7 +469,7 @@
"@executable_path/../../../../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 26.0;
MARKETING_VERSION = 0.5.6;
MARKETING_VERSION = 0.5.7;
PRODUCT_BUNDLE_IDENTIFIER = com.deanoc.spiderweb.fskit.app.extension;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
Expand Down Expand Up @@ -500,7 +500,7 @@
"@executable_path/../../../../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 26.0;
MARKETING_VERSION = 0.5.6;
MARKETING_VERSION = 0.5.7;
PRODUCT_BUNDLE_IDENTIFIER = com.deanoc.spiderweb.fskit.app.extension;
PRODUCT_NAME = "$(TARGET_NAME)";
PROVISIONING_PROFILE_SPECIFIER = "";
Expand Down
16 changes: 13 additions & 3 deletions platform/macos/SpiderwebFSKitApp/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -203,8 +203,8 @@ struct ContentView: View {
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible()), GridItem(.flexible())], spacing: 12) {
HeroFactCard(
title: "Default Path",
value: "Just Try It",
detail: "The fastest way to see Spiderweb as a drive on this Mac."
value: "Start Local Workspace",
detail: "The fastest way to create a local workspace and see it as a drive on this Mac."
)
HeroFactCard(
title: "Drive Location",
Expand Down Expand Up @@ -1225,10 +1225,13 @@ private enum SpiderwebRecipe: String, Identifiable {
case .mountExistingSpiderweb:
return controller.savedMounts.contains(where: { $0.kind == .remote }) ? .done : .guide
case .shareThisSpiderweb:
if controller.hasCompletedAnySpiderAppWorkflow(.addSecondDevice) {
return .done
}
if controller.serviceStatus?.loaded == true,
controller.authStatus?.accessPresent == true,
controller.serviceStatus?.remoteReachable != false {
return .done
return .ready
}
if controller.serviceStatus?.loaded == true, controller.authStatus?.accessPresent == true {
return .ready
Expand All @@ -1243,6 +1246,9 @@ private enum SpiderwebRecipe: String, Identifiable {
}
return .guide
case .setupUsefulWorkspace:
if controller.hasCompletedAnySpiderAppWorkflow(.startLocalWorkspace) {
return .done
}
if !controller.mountableLocalWorkspaces.isEmpty {
return .done
}
Expand All @@ -1251,6 +1257,10 @@ private enum SpiderwebRecipe: String, Identifiable {
}
return .guide
case .packagesAndServices:
if controller.hasCompletedAnySpiderAppWorkflow(.installPackage) ||
controller.hasCompletedAnySpiderAppWorkflow(.runRemoteService) {
return .done
}
if controller.quickstartState?.isComplete == true, controller.quickstartCanOpenSpiderApp {
return .done
}
Expand Down
183 changes: 183 additions & 0 deletions platform/macos/SpiderwebFSKitApp/SpiderAppWorkflowStore.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
import Foundation

enum SpiderAppWorkflowID: String {
case startLocalWorkspace = "start_local_workspace"
case addSecondDevice = "add_second_device"
case installPackage = "install_package"
case runRemoteService = "run_remote_service"
case connectToAnotherSpiderweb = "connect_to_another_spiderweb"
case spiderwebHandoffCompleted = "spiderweb_handoff_completed"
}

enum SpiderAppDeepLinkRoute: String {
case workspace
case devices
case capabilities
case explore
case settings
}

enum SpiderAppDeepLinkAction: String {
case openWorkspace = "open_workspace"
case openDevices = "open_devices"
case openCapabilities = "open_capabilities"
case openExplore = "open_explore"
case openSettings = "open_settings"
}

private struct SpiderAppWorkflowEntry {
var profileID: String
var workspaceID: String?
var workflowID: String
var completedAtMS: Int64
}

enum SpiderAppWorkflowStore {
static let defaultProfileID = "default"

static func hasCompletion(_ workflowID: SpiderAppWorkflowID, profileID: String = defaultProfileID, workspaceID: String? = nil) -> Bool {
loadEntries().contains { entry in
guard entry.profileID == profileID else { return false }
guard entry.workflowID == workflowID.rawValue else { return false }
return normalize(entry.workspaceID) == normalize(workspaceID)
}
}

static func hasAnyCompletion(_ workflowID: SpiderAppWorkflowID, profileID: String = defaultProfileID) -> Bool {
loadEntries().contains { entry in
entry.profileID == profileID && entry.workflowID == workflowID.rawValue
}
}

static func markCompleted(_ workflowID: SpiderAppWorkflowID, profileID: String = defaultProfileID, workspaceID: String? = nil) {
updateRootObject { rootObject in
var entries = parseEntries(from: rootObject)
let normalizedWorkspaceID = normalize(workspaceID)
let nowMS = Int64(Date().timeIntervalSince1970 * 1000)

if let index = entries.firstIndex(where: {
$0.profileID == profileID &&
$0.workflowID == workflowID.rawValue &&
normalize($0.workspaceID) == normalizedWorkspaceID
}) {
entries[index].completedAtMS = nowMS
} else {
entries.append(
SpiderAppWorkflowEntry(
profileID: profileID,
workspaceID: normalizedWorkspaceID,
workflowID: workflowID.rawValue,
completedAtMS: nowMS
)
)
}

rootObject["onboarding_workflows"] = entries.map { entry in
var dict: [String: Any] = [
"profile_id": entry.profileID,
"workflow_id": entry.workflowID,
"completed_at_ms": entry.completedAtMS
]
if let workspaceID = normalize(entry.workspaceID) {
dict["workspace_id"] = workspaceID
}
return dict
}
}
}

static func deepLinkURL(
profileID: String = defaultProfileID,
workspaceID: String? = nil,
route: SpiderAppDeepLinkRoute,
action: SpiderAppDeepLinkAction? = nil,
mountpoint: String? = nil,
degraded: Bool = false,
handoff: Bool = false
) -> URL? {
var components = URLComponents()
components.scheme = "spiderapp"
components.host = "open"

var queryItems = [URLQueryItem(name: "profile_id", value: profileID)]
if let workspaceID = normalize(workspaceID) {
queryItems.append(URLQueryItem(name: "workspace_id", value: workspaceID))
}
queryItems.append(URLQueryItem(name: "route", value: route.rawValue))
if let action {
queryItems.append(URLQueryItem(name: "action", value: action.rawValue))
}
if let mountpoint = normalize(mountpoint) {
queryItems.append(URLQueryItem(name: "mountpoint", value: mountpoint))
}
if degraded {
queryItems.append(URLQueryItem(name: "degraded", value: "1"))
}
if handoff {
queryItems.append(URLQueryItem(name: "handoff", value: "1"))
}
components.queryItems = queryItems
return components.url
}

private static func configURL() -> URL {
FileManager.default.homeDirectoryForCurrentUser
.appendingPathComponent(".config/spider", isDirectory: true)
.appendingPathComponent("config.json")
}

private static func loadEntries() -> [SpiderAppWorkflowEntry] {
guard let data = try? Data(contentsOf: configURL()),
let rootObject = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any] else {
return []
}
return parseEntries(from: rootObject)
}

private static func parseEntries(from rootObject: [String: Any]) -> [SpiderAppWorkflowEntry] {
guard let rawEntries = rootObject["onboarding_workflows"] as? [[String: Any]] else { return [] }
return rawEntries.compactMap { raw in
guard let profileID = normalize(raw["profile_id"] as? String),
let workflowID = normalize(raw["workflow_id"] as? String) else {
return nil
}
let completedAtMS = (raw["completed_at_ms"] as? NSNumber)?.int64Value ?? 0
return SpiderAppWorkflowEntry(
profileID: profileID,
workspaceID: normalize(raw["workspace_id"] as? String),
workflowID: workflowID,
completedAtMS: completedAtMS
)
}
}

private static func updateRootObject(_ mutate: (inout [String: Any]) -> Void) {
let url = configURL()
let configDirectory = url.deletingLastPathComponent()

var rootObject: [String: Any] = [:]
if let data = try? Data(contentsOf: url),
let loaded = (try? JSONSerialization.jsonObject(with: data)) as? [String: Any] {
rootObject = loaded
}
if rootObject["schema_version"] == nil {
rootObject["schema_version"] = 2
}

mutate(&rootObject)

do {
try FileManager.default.createDirectory(at: configDirectory, withIntermediateDirectories: true)
let data = try JSONSerialization.data(withJSONObject: rootObject, options: [.prettyPrinted, .sortedKeys])
try data.write(to: url, options: .atomic)
} catch {
NSLog("SpiderAppWorkflowStore save failed: %@", error.localizedDescription)
}
}

private static func normalize(_ value: String?) -> String? {
guard let value else { return nil }
let trimmed = value.trimmingCharacters(in: .whitespacesAndNewlines)
return trimmed.isEmpty ? nil : trimmed
}
}
44 changes: 39 additions & 5 deletions platform/macos/SpiderwebFSKitApp/SpiderwebAppController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ enum QuickstartPreset: String, Codable, CaseIterable, Identifiable {

var title: String {
switch self {
case .justTryIt: return "Just Try It"
case .justTryIt: return "Start Local Workspace"
case .connectMachines: return "Connect Machines"
case .agentLab: return "Agent Lab"
}
Expand Down Expand Up @@ -516,12 +516,36 @@ final class SpiderwebAppController: ObservableObject {
Self.findSpiderAppApplicationURL() != nil || Self.findSpiderAppExecutablePath() != nil
}

func hasCompletedSpiderAppWorkflow(_ workflowID: SpiderAppWorkflowID, workspaceID: String? = nil) -> Bool {
SpiderAppWorkflowStore.hasCompletion(workflowID, workspaceID: workspaceID)
}

func hasCompletedAnySpiderAppWorkflow(_ workflowID: SpiderAppWorkflowID) -> Bool {
SpiderAppWorkflowStore.hasAnyCompletion(workflowID)
}

private func preferredSpiderAppLaunchURL() -> URL? {
let workspaceID = quickstartState?.result?.workspaceID ?? quickstartState?.workspaceID
let mountpoint = quickstartState?.result?.mountpoint ?? quickstartState?.mountpoint
let degraded = quickstartState?.result?.driveAvailable == false || quickstartState?.result?.driveIssueSummary != nil
let handoff = quickstartState?.isComplete == true

return SpiderAppWorkflowStore.deepLinkURL(
workspaceID: workspaceID,
route: .workspace,
action: nil,
mountpoint: mountpoint,
degraded: degraded,
handoff: handoff
)
}

var quickstartNextStepDetail: String {
if let driveIssueSummary = quickstartState?.result?.driveIssueSummary, !driveIssueSummary.isEmpty {
return "Open SpiderApp to keep working while the drive mount is blocked, then retry the drive from Spiderweb after macOS clears the stuck FSKit state."
return "Open SpiderApp to keep working while the drive mount is blocked, then use Remote Terminal or the workspace shell until macOS clears the stuck FSKit state."
}
if quickstartCanOpenSpiderApp {
return "Open SpiderApp’s workspace shell and keep Devices, Capabilities, Explore, and Settings close by."
return "Open SpiderApp’s native shell, then jump into Remote Terminal, Devices, Capabilities, Explore, or Settings from a cleaner Mac-first starting point."
}
return "SpiderApp is not available on this Mac yet, so the mounted drive remains the fastest next step."
}
Expand Down Expand Up @@ -1606,6 +1630,7 @@ final class SpiderwebAppController: ObservableObject {
return next
}
let completedState = quickstart
SpiderAppWorkflowStore.markCompleted(.startLocalWorkspace, workspaceID: workspace.summary.id)

await MainActor.run {
self.savedMounts = mergedMounts
Expand Down Expand Up @@ -1655,8 +1680,18 @@ final class SpiderwebAppController: ObservableObject {
func openSpiderApp() {
lastError = nil

statusMessage = "Opening SpiderApp..."

if let deepLinkURL = preferredSpiderAppLaunchURL(),
NSWorkspace.shared.open(deepLinkURL) {
if quickstartState?.isComplete == true {
SpiderAppWorkflowStore.markCompleted(.spiderwebHandoffCompleted)
}
statusMessage = "Opened SpiderApp"
return
}

if let appURL = Self.findSpiderAppApplicationURL() {
statusMessage = "Opening SpiderApp..."
let configuration = NSWorkspace.OpenConfiguration()
configuration.activates = true
let applyOpenResult: @MainActor (String?) -> Void = { [weak self] errorDescription in
Expand All @@ -1677,7 +1712,6 @@ final class SpiderwebAppController: ObservableObject {
}

if let executablePath = Self.findSpiderAppExecutablePath() {
statusMessage = "Opening SpiderApp..."
Task.detached(priority: .userInitiated) {
do {
try Self.launchDetachedProcess(executableURL: URL(fileURLWithPath: executablePath), arguments: [])
Expand Down
Loading
Loading