mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
fix(mobile): detect New Workspace agents in the selected repo runtime
Pass the selected repository to New Workspace agent detection through the host-authoritative runtime resolver, including WSL paths and Windows project preferences. Preserve old-client request shape, SSH routing, and strict ownership/refusal behavior. This merge credits @welldoingk’s original WSL agent-detection investigation and community PR #20101, adapted to the current host-resolved contract. Co-authored-by: welldoingk <thkwon96@gmail.com> Fixes #20060
This commit is contained in:
+287
-287
@@ -4,12 +4,12 @@
|
||||
"family": "tasks.workspace-ssh-local",
|
||||
"namedDeltas": [],
|
||||
"values": {
|
||||
"11405363c62d": {
|
||||
"name": "workspaceAgent",
|
||||
"ordinal": 1,
|
||||
"value": "claude"
|
||||
"057cfe2200b5": {
|
||||
"name": "preflight.detectAgents#1",
|
||||
"ordinal": 5,
|
||||
"json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"preflight.detectAgents\",\"params\":{\"repoId\":\"repo-1\"}}"
|
||||
},
|
||||
"1feb12bfbf62": {
|
||||
"110ef4b3eb5b": {
|
||||
"name": "preflight.detectAgents#1",
|
||||
"ordinal": 4,
|
||||
"args": [
|
||||
@@ -19,9 +19,239 @@
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"repoId": "repo-1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "rejected",
|
||||
"startedAt": 0,
|
||||
"settledAt": 0,
|
||||
"error": {
|
||||
"category": "Error",
|
||||
"message": "transport failure",
|
||||
"isRpcDeliveryUnknown": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"11405363c62d": {
|
||||
"name": "workspaceAgent",
|
||||
"ordinal": 1,
|
||||
"value": "claude"
|
||||
},
|
||||
"12f65d55aa1a": {
|
||||
"name": "preflight.detectAgents#1",
|
||||
"ordinal": 4,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "preflight.detectAgents"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"repoId": "repo-1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "fulfilled",
|
||||
"startedAt": 0,
|
||||
"settledAt": 0,
|
||||
"value": {
|
||||
"error": {
|
||||
"code": "refused",
|
||||
"message": "outer refused"
|
||||
},
|
||||
"id": "frame-1",
|
||||
"ok": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"34cbfeeb5c49": {
|
||||
"name": "preflight.detectAgents#1",
|
||||
"ordinal": 4,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "preflight.detectAgents"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"repoId": "repo-1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "rejected",
|
||||
"startedAt": 0,
|
||||
"settledAt": 0,
|
||||
"error": {
|
||||
"category": "Error",
|
||||
"message": "",
|
||||
"isRpcDeliveryUnknown": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"43111f57966a": {
|
||||
"name": "preflight.detectAgents#1",
|
||||
"ordinal": 4,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "preflight.detectAgents"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"repoId": "repo-1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "fulfilled",
|
||||
"startedAt": 0,
|
||||
"settledAt": 0,
|
||||
"value": {
|
||||
"id": "frame-1",
|
||||
"ok": true,
|
||||
"result": {
|
||||
"error": "refused"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"5786215332fb": {
|
||||
"name": "preflight.detectAgents#1",
|
||||
"ordinal": 4,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "preflight.detectAgents"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"repoId": "repo-1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "fulfilled",
|
||||
"startedAt": 0,
|
||||
"settledAt": 0,
|
||||
"value": {
|
||||
"id": "frame-1",
|
||||
"ok": true,
|
||||
"result": {
|
||||
"$rpc": "null"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"630e2f27face": {
|
||||
"name": "workspaceDetectedAgentIds",
|
||||
"ordinal": 6,
|
||||
"value": []
|
||||
},
|
||||
"63751bc69233": {
|
||||
"name": "preflight.detectAgents#1",
|
||||
"ordinal": 4,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "preflight.detectAgents"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"repoId": "repo-1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "fulfilled",
|
||||
"startedAt": 0,
|
||||
"settledAt": 0,
|
||||
"value": {
|
||||
"error": {
|
||||
"code": "refused",
|
||||
"message": ""
|
||||
},
|
||||
"id": "frame-1",
|
||||
"ok": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"71225024ccf5": {
|
||||
"agent": "claude",
|
||||
"connecting": false,
|
||||
"detected": [],
|
||||
"setup": "unresolved",
|
||||
"ssh": {
|
||||
"$rpc": "null"
|
||||
}
|
||||
},
|
||||
"7400f4eebe66": {
|
||||
"agent": "claude",
|
||||
"connecting": false,
|
||||
"detected": ["codex", "claude"],
|
||||
"setup": "unresolved",
|
||||
"ssh": {
|
||||
"$rpc": "null"
|
||||
}
|
||||
},
|
||||
"8172e74bc69e": {
|
||||
"name": "preflight.detectAgents#1",
|
||||
"ordinal": 4,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "preflight.detectAgents"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"repoId": "repo-1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
@@ -44,7 +274,7 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"24a0184fc2f5": {
|
||||
"8f4b7427ef27": {
|
||||
"name": "preflight.detectAgents#1",
|
||||
"ordinal": 4,
|
||||
"args": [
|
||||
@@ -55,7 +285,7 @@
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
"repoId": "repo-1"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -81,35 +311,14 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"5da628eecd2f": {
|
||||
"name": "preflight.detectAgents#1",
|
||||
"ordinal": 5,
|
||||
"json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"preflight.detectAgents\"}"
|
||||
},
|
||||
"630e2f27face": {
|
||||
"9637248df9cb": {
|
||||
"name": "workspaceDetectedAgentIds",
|
||||
"ordinal": 6,
|
||||
"value": []
|
||||
},
|
||||
"71225024ccf5": {
|
||||
"agent": "claude",
|
||||
"connecting": false,
|
||||
"detected": [],
|
||||
"setup": "unresolved",
|
||||
"ssh": {
|
||||
"ordinal": 3,
|
||||
"value": {
|
||||
"$rpc": "null"
|
||||
}
|
||||
},
|
||||
"7400f4eebe66": {
|
||||
"agent": "claude",
|
||||
"connecting": false,
|
||||
"detected": ["codex", "claude"],
|
||||
"setup": "unresolved",
|
||||
"ssh": {
|
||||
"$rpc": "null"
|
||||
}
|
||||
},
|
||||
"78870501ce99": {
|
||||
"a3447a38f180": {
|
||||
"name": "preflight.detectAgents#1",
|
||||
"ordinal": 4,
|
||||
"args": [
|
||||
@@ -120,7 +329,7 @@
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
"repoId": "repo-1"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -144,7 +353,12 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"90ac803be31a": {
|
||||
"a9ec92743b9e": {
|
||||
"name": "workspaceAgentOverridden",
|
||||
"ordinal": 2,
|
||||
"value": false
|
||||
},
|
||||
"c988ed31a0bf": {
|
||||
"name": "preflight.detectAgents#1",
|
||||
"ordinal": 4,
|
||||
"args": [
|
||||
@@ -155,78 +369,7 @@
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "rejected",
|
||||
"startedAt": 0,
|
||||
"settledAt": 0,
|
||||
"error": {
|
||||
"category": "Error",
|
||||
"message": "transport failure",
|
||||
"isRpcDeliveryUnknown": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"9637248df9cb": {
|
||||
"name": "workspaceDetectedAgentIds",
|
||||
"ordinal": 3,
|
||||
"value": {
|
||||
"$rpc": "null"
|
||||
}
|
||||
},
|
||||
"9a93035b19e1": {
|
||||
"name": "preflight.detectAgents#1",
|
||||
"ordinal": 4,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "preflight.detectAgents"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "rejected",
|
||||
"startedAt": 0,
|
||||
"settledAt": 0,
|
||||
"error": {
|
||||
"category": "Error",
|
||||
"message": "",
|
||||
"isRpcDeliveryUnknown": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"9bee832e6c84": {
|
||||
"name": "preflight.detectAgents#1",
|
||||
"ordinal": 4,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "preflight.detectAgents"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
"repoId": "repo-1"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -241,16 +384,25 @@
|
||||
"startedAt": 0,
|
||||
"settledAt": 0,
|
||||
"value": {
|
||||
"error": {
|
||||
"code": "refused",
|
||||
"message": ""
|
||||
},
|
||||
"id": "frame-1",
|
||||
"ok": false
|
||||
"ok": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"9fdd4a87eb0b": {
|
||||
"d68cba3b57e1": {
|
||||
"name": "workspaceDetectedAgentIds",
|
||||
"ordinal": 6,
|
||||
"value": ["codex", "claude"]
|
||||
},
|
||||
"eb79a9b3682a": {
|
||||
"status": "fulfilled",
|
||||
"startedAt": 0,
|
||||
"settledAt": 0,
|
||||
"value": {
|
||||
"$rpc": "undefined"
|
||||
}
|
||||
},
|
||||
"ebcf6c6d8d86": {
|
||||
"name": "preflight.detectAgents#1",
|
||||
"ordinal": 4,
|
||||
"args": [
|
||||
@@ -261,7 +413,7 @@
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
"repoId": "repo-1"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -281,158 +433,6 @@
|
||||
"result": ["codex", "claude"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"a2765967bb49": {
|
||||
"name": "preflight.detectAgents#1",
|
||||
"ordinal": 4,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "preflight.detectAgents"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "fulfilled",
|
||||
"startedAt": 0,
|
||||
"settledAt": 0,
|
||||
"value": {
|
||||
"id": "frame-1",
|
||||
"ok": true,
|
||||
"result": {
|
||||
"error": "refused"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"a9ec92743b9e": {
|
||||
"name": "workspaceAgentOverridden",
|
||||
"ordinal": 2,
|
||||
"value": false
|
||||
},
|
||||
"c1f59c7b2afa": {
|
||||
"name": "preflight.detectAgents#1",
|
||||
"ordinal": 4,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "preflight.detectAgents"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "fulfilled",
|
||||
"startedAt": 0,
|
||||
"settledAt": 0,
|
||||
"value": {
|
||||
"id": "frame-1",
|
||||
"ok": true,
|
||||
"result": {
|
||||
"$rpc": "null"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"c8028d1dd40a": {
|
||||
"name": "preflight.detectAgents#1",
|
||||
"ordinal": 4,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "preflight.detectAgents"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "fulfilled",
|
||||
"startedAt": 0,
|
||||
"settledAt": 0,
|
||||
"value": {
|
||||
"error": {
|
||||
"code": "refused",
|
||||
"message": "outer refused"
|
||||
},
|
||||
"id": "frame-1",
|
||||
"ok": false
|
||||
}
|
||||
}
|
||||
},
|
||||
"d68cba3b57e1": {
|
||||
"name": "workspaceDetectedAgentIds",
|
||||
"ordinal": 6,
|
||||
"value": ["codex", "claude"]
|
||||
},
|
||||
"eb79a9b3682a": {
|
||||
"status": "fulfilled",
|
||||
"startedAt": 0,
|
||||
"settledAt": 0,
|
||||
"value": {
|
||||
"$rpc": "undefined"
|
||||
}
|
||||
},
|
||||
"f8eb8a4a9917": {
|
||||
"name": "preflight.detectAgents#1",
|
||||
"ordinal": 4,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "preflight.detectAgents"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "fulfilled",
|
||||
"startedAt": 0,
|
||||
"settledAt": 0,
|
||||
"value": {
|
||||
"id": "frame-1",
|
||||
"ok": true
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"recording": {
|
||||
@@ -441,8 +441,8 @@
|
||||
{
|
||||
"id": "tw-workspace-ssh-local-agents.normal:local-agents-detected",
|
||||
"observation": {
|
||||
"sender": ["9fdd4a87eb0b"],
|
||||
"payloads": ["5da628eecd2f"],
|
||||
"sender": ["ebcf6c6d8d86"],
|
||||
"payloads": ["057cfe2200b5"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a"
|
||||
},
|
||||
@@ -453,8 +453,8 @@
|
||||
{
|
||||
"id": "tw-workspace-ssh-local-agents.result-absent:local-agents-detected",
|
||||
"observation": {
|
||||
"sender": ["f8eb8a4a9917"],
|
||||
"payloads": ["5da628eecd2f"],
|
||||
"sender": ["c988ed31a0bf"],
|
||||
"payloads": ["057cfe2200b5"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a"
|
||||
},
|
||||
@@ -465,8 +465,8 @@
|
||||
{
|
||||
"id": "tw-workspace-ssh-local-agents.result-null:local-agents-detected",
|
||||
"observation": {
|
||||
"sender": ["c1f59c7b2afa"],
|
||||
"payloads": ["5da628eecd2f"],
|
||||
"sender": ["5786215332fb"],
|
||||
"payloads": ["057cfe2200b5"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a"
|
||||
},
|
||||
@@ -477,8 +477,8 @@
|
||||
{
|
||||
"id": "tw-workspace-ssh-local-agents.inner-ok-missing:local-agents-detected",
|
||||
"observation": {
|
||||
"sender": ["a2765967bb49"],
|
||||
"payloads": ["5da628eecd2f"],
|
||||
"sender": ["43111f57966a"],
|
||||
"payloads": ["057cfe2200b5"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a"
|
||||
},
|
||||
@@ -489,8 +489,8 @@
|
||||
{
|
||||
"id": "tw-workspace-ssh-local-agents.inner-false-string-error:local-agents-detected",
|
||||
"observation": {
|
||||
"sender": ["78870501ce99"],
|
||||
"payloads": ["5da628eecd2f"],
|
||||
"sender": ["a3447a38f180"],
|
||||
"payloads": ["057cfe2200b5"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a"
|
||||
},
|
||||
@@ -501,8 +501,8 @@
|
||||
{
|
||||
"id": "tw-workspace-ssh-local-agents.inner-false-object-error:local-agents-detected",
|
||||
"observation": {
|
||||
"sender": ["24a0184fc2f5"],
|
||||
"payloads": ["5da628eecd2f"],
|
||||
"sender": ["8f4b7427ef27"],
|
||||
"payloads": ["057cfe2200b5"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a"
|
||||
},
|
||||
@@ -513,8 +513,8 @@
|
||||
{
|
||||
"id": "tw-workspace-ssh-local-agents.outer-refused:local-agents-detected",
|
||||
"observation": {
|
||||
"sender": ["c8028d1dd40a"],
|
||||
"payloads": ["5da628eecd2f"],
|
||||
"sender": ["12f65d55aa1a"],
|
||||
"payloads": ["057cfe2200b5"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a"
|
||||
},
|
||||
@@ -525,8 +525,8 @@
|
||||
{
|
||||
"id": "tw-workspace-ssh-local-agents.outer-refused-no-message:local-agents-detected",
|
||||
"observation": {
|
||||
"sender": ["9bee832e6c84"],
|
||||
"payloads": ["5da628eecd2f"],
|
||||
"sender": ["63751bc69233"],
|
||||
"payloads": ["057cfe2200b5"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a"
|
||||
},
|
||||
@@ -537,8 +537,8 @@
|
||||
{
|
||||
"id": "tw-workspace-ssh-local-agents.method-not-found:local-agents-detected",
|
||||
"observation": {
|
||||
"sender": ["1feb12bfbf62"],
|
||||
"payloads": ["5da628eecd2f"],
|
||||
"sender": ["8172e74bc69e"],
|
||||
"payloads": ["057cfe2200b5"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a"
|
||||
},
|
||||
@@ -549,8 +549,8 @@
|
||||
{
|
||||
"id": "tw-workspace-ssh-local-agents.transport-rejection:local-agents-detected",
|
||||
"observation": {
|
||||
"sender": ["90ac803be31a"],
|
||||
"payloads": ["5da628eecd2f"],
|
||||
"sender": ["110ef4b3eb5b"],
|
||||
"payloads": ["057cfe2200b5"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a"
|
||||
},
|
||||
@@ -561,8 +561,8 @@
|
||||
{
|
||||
"id": "tw-workspace-ssh-local-agents.transport-rejection-no-message:local-agents-detected",
|
||||
"observation": {
|
||||
"sender": ["9a93035b19e1"],
|
||||
"payloads": ["5da628eecd2f"],
|
||||
"sender": ["34cbfeeb5c49"],
|
||||
"payloads": ["057cfe2200b5"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a"
|
||||
},
|
||||
|
||||
@@ -4,16 +4,16 @@
|
||||
"family": "tasks.workspace-ssh-local",
|
||||
"namedDeltas": [],
|
||||
"values": {
|
||||
"057cfe2200b5": {
|
||||
"name": "preflight.detectAgents#1",
|
||||
"ordinal": 5,
|
||||
"json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"preflight.detectAgents\",\"params\":{\"repoId\":\"repo-1\"}}"
|
||||
},
|
||||
"11405363c62d": {
|
||||
"name": "workspaceAgent",
|
||||
"ordinal": 1,
|
||||
"value": "claude"
|
||||
},
|
||||
"5da628eecd2f": {
|
||||
"name": "preflight.detectAgents#1",
|
||||
"ordinal": 5,
|
||||
"json": "{\"id\":\"frame-1\",\"deviceToken\":\"recording-device\",\"method\":\"preflight.detectAgents\"}"
|
||||
},
|
||||
"7400f4eebe66": {
|
||||
"agent": "claude",
|
||||
"connecting": false,
|
||||
@@ -30,38 +30,6 @@
|
||||
"$rpc": "null"
|
||||
}
|
||||
},
|
||||
"9fdd4a87eb0b": {
|
||||
"name": "preflight.detectAgents#1",
|
||||
"ordinal": 4,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "preflight.detectAgents"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "fulfilled",
|
||||
"startedAt": 0,
|
||||
"settledAt": 0,
|
||||
"value": {
|
||||
"id": "frame-1",
|
||||
"ok": true,
|
||||
"result": ["codex", "claude"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"a9ec92743b9e": {
|
||||
"name": "workspaceAgentOverridden",
|
||||
"ordinal": 2,
|
||||
@@ -79,6 +47,38 @@
|
||||
"value": {
|
||||
"$rpc": "undefined"
|
||||
}
|
||||
},
|
||||
"ebcf6c6d8d86": {
|
||||
"name": "preflight.detectAgents#1",
|
||||
"ordinal": 4,
|
||||
"args": [
|
||||
{
|
||||
"name": "method",
|
||||
"value": "preflight.detectAgents"
|
||||
},
|
||||
{
|
||||
"name": "params",
|
||||
"value": {
|
||||
"repoId": "repo-1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "options",
|
||||
"value": {
|
||||
"$rpc": "absent"
|
||||
}
|
||||
}
|
||||
],
|
||||
"settlement": {
|
||||
"status": "fulfilled",
|
||||
"startedAt": 0,
|
||||
"settledAt": 0,
|
||||
"value": {
|
||||
"id": "frame-1",
|
||||
"ok": true,
|
||||
"result": ["codex", "claude"]
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"recording": {
|
||||
@@ -87,8 +87,8 @@
|
||||
{
|
||||
"id": "local-agents-detected",
|
||||
"observation": {
|
||||
"sender": ["9fdd4a87eb0b"],
|
||||
"payloads": ["5da628eecd2f"],
|
||||
"sender": ["ebcf6c6d8d86"],
|
||||
"payloads": ["057cfe2200b5"],
|
||||
"settlements": {
|
||||
"mount": "eb79a9b3682a"
|
||||
},
|
||||
|
||||
@@ -7219,7 +7219,7 @@
|
||||
{
|
||||
"complete": "preflight.detectAgents#1",
|
||||
"params": {
|
||||
"$undefined": true
|
||||
"repoId": "repo-1"
|
||||
},
|
||||
"reply": {
|
||||
"ok": true,
|
||||
|
||||
@@ -323,7 +323,7 @@ describe('NewWorktreeModal project targets', () => {
|
||||
await flushUpdates()
|
||||
|
||||
expect(renderer.root.findAll((node) => node.props.label === 'Workspace name')).toHaveLength(1)
|
||||
expect(sendRequest).toHaveBeenCalledWith('preflight.detectAgents')
|
||||
expect(sendRequest).toHaveBeenCalledWith('preflight.detectAgents', { repoId: 'folder-1' })
|
||||
expect(sendRequest).not.toHaveBeenCalledWith('ssh.getState', expect.anything())
|
||||
expect(sendRequest.mock.calls.map(([method]) => method)).not.toContain(
|
||||
'worktree.prefetchCreateBase'
|
||||
|
||||
@@ -100,6 +100,7 @@ function NewWorktreeModalContent(props: NewWorktreeModalProps) {
|
||||
const executionTarget = useNewWorkspaceExecutionTarget({
|
||||
client,
|
||||
connectionId: selectedRepoConnectionId,
|
||||
repoId: selectedRepo?.id,
|
||||
visible
|
||||
})
|
||||
const setupScript = useNewWorkspaceSetupScript({ client, selectedRepo })
|
||||
|
||||
@@ -0,0 +1,161 @@
|
||||
import { createElement } from 'react'
|
||||
import { act, create, type ReactTestRenderer } from 'react-test-renderer'
|
||||
import { afterEach, describe, expect, it } from 'vitest'
|
||||
import { FakeSession } from '../transport/mobile-endpoint-supervisor-test-fakes'
|
||||
import type { RpcClient } from '../transport/rpc-client'
|
||||
import type { RpcResponse } from '../transport/types'
|
||||
import { useNewWorkspaceExecutionTarget } from './use-new-workspace-execution-target'
|
||||
|
||||
const mounted: ReactTestRenderer[] = []
|
||||
afterEach(() => {
|
||||
for (const renderer of mounted.splice(0)) {
|
||||
act(() => renderer.unmount())
|
||||
}
|
||||
})
|
||||
|
||||
describe('New Workspace agent detection', () => {
|
||||
it('targets the picked repo and clears the previous repo agents while switching', async () => {
|
||||
const client = new FakeSession('connected')
|
||||
let resolveNext: ((response: { id: string; ok: true; result: string[] }) => void) | undefined
|
||||
client.sendRequest.mockImplementation(async (_method: string, params?: unknown) => {
|
||||
if (JSON.stringify(params) === JSON.stringify({ repoId: 'wsl-repo' })) {
|
||||
return { id: 'reply', ok: true, result: ['claude'] }
|
||||
}
|
||||
return new Promise((resolve) => {
|
||||
resolveNext = resolve
|
||||
})
|
||||
})
|
||||
let detected: Set<string> | null = null
|
||||
function Harness({
|
||||
repoId,
|
||||
selectedClient = client
|
||||
}: {
|
||||
repoId: string
|
||||
selectedClient?: RpcClient
|
||||
}) {
|
||||
detected = useNewWorkspaceExecutionTarget({
|
||||
client: selectedClient,
|
||||
repoId,
|
||||
connectionId: null,
|
||||
visible: true
|
||||
}).detectedAgentIds
|
||||
return null
|
||||
}
|
||||
let renderer: ReactTestRenderer | undefined
|
||||
await act(async () => {
|
||||
renderer = create(createElement(Harness, { repoId: 'wsl-repo' }))
|
||||
mounted.push(renderer)
|
||||
})
|
||||
expect(client.sendRequest).toHaveBeenCalledWith('preflight.detectAgents', {
|
||||
repoId: 'wsl-repo'
|
||||
})
|
||||
expect(detected).toEqual(new Set(['claude']))
|
||||
await act(async () => {
|
||||
renderer?.update(createElement(Harness, { repoId: 'host-repo' }))
|
||||
})
|
||||
expect(detected).toBeNull()
|
||||
expect(client.sendRequest).toHaveBeenLastCalledWith('preflight.detectAgents', {
|
||||
repoId: 'host-repo'
|
||||
})
|
||||
await act(async () => {
|
||||
resolveNext?.({ id: 'reply', ok: true, result: ['codex'] })
|
||||
})
|
||||
expect(detected).toEqual(new Set(['codex']))
|
||||
const otherHost = new FakeSession('connected')
|
||||
otherHost.sendRequest.mockImplementation(
|
||||
() =>
|
||||
new Promise((resolve) => {
|
||||
resolveNext = resolve
|
||||
})
|
||||
)
|
||||
await act(async () => {
|
||||
renderer?.update(createElement(Harness, { repoId: 'host-repo', selectedClient: otherHost }))
|
||||
})
|
||||
expect(detected).toBeNull()
|
||||
await act(async () => {
|
||||
resolveNext?.({ id: 'reply', ok: true, result: ['gemini'] })
|
||||
})
|
||||
expect(detected).toEqual(new Set(['gemini']))
|
||||
})
|
||||
|
||||
it.each([
|
||||
{ switchHost: false, rejectLate: false },
|
||||
{ switchHost: false, rejectLate: true },
|
||||
{ switchHost: true, rejectLate: false },
|
||||
{ switchHost: true, rejectLate: true }
|
||||
])(
|
||||
'ignores late results after switching ($switchHost host, $rejectLate rejection)',
|
||||
async ({ switchHost, rejectLate }) => {
|
||||
const originalClient = new FakeSession('connected')
|
||||
const currentClient = switchHost ? new FakeSession('connected') : originalClient
|
||||
let settleLate: (() => void) | undefined
|
||||
originalClient.sendRequest.mockImplementationOnce(
|
||||
() =>
|
||||
new Promise<RpcResponse>((resolve, reject) => {
|
||||
settleLate = () =>
|
||||
rejectLate
|
||||
? reject(new Error('old host disconnected'))
|
||||
: resolve({ id: 'late', ok: true, result: ['claude'] })
|
||||
})
|
||||
)
|
||||
currentClient.sendRequest.mockResolvedValue({ id: 'current', ok: true, result: ['codex'] })
|
||||
let detected: Set<string> | null = null
|
||||
function Harness({ client, repoId }: { client: RpcClient; repoId: string }) {
|
||||
detected = useNewWorkspaceExecutionTarget({
|
||||
client,
|
||||
repoId,
|
||||
connectionId: null,
|
||||
visible: true
|
||||
}).detectedAgentIds
|
||||
return null
|
||||
}
|
||||
let renderer: ReactTestRenderer | undefined
|
||||
await act(async () => {
|
||||
renderer = create(createElement(Harness, { client: originalClient, repoId: 'repo-1' }))
|
||||
mounted.push(renderer)
|
||||
})
|
||||
expect(detected).toBeNull()
|
||||
await act(async () => {
|
||||
renderer?.update(
|
||||
createElement(Harness, {
|
||||
client: currentClient,
|
||||
repoId: switchHost ? 'repo-1' : 'repo-2'
|
||||
})
|
||||
)
|
||||
})
|
||||
expect(detected).toEqual(new Set(['codex']))
|
||||
await act(async () => settleLate?.())
|
||||
expect(detected).toEqual(new Set(['codex']))
|
||||
}
|
||||
)
|
||||
|
||||
it('keeps the SSH request and connection gate', async () => {
|
||||
const client = new FakeSession('connected')
|
||||
client.sendRequest.mockImplementation(async (method: string) => ({
|
||||
id: 'reply',
|
||||
ok: true,
|
||||
result:
|
||||
method === 'ssh.getState'
|
||||
? { state: { targetId: 'ssh-1', status: 'connected', error: null, reconnectAttempt: 0 } }
|
||||
: ['codex']
|
||||
}))
|
||||
function Harness() {
|
||||
useNewWorkspaceExecutionTarget({
|
||||
client,
|
||||
repoId: 'remote-repo',
|
||||
connectionId: 'ssh-1',
|
||||
visible: true
|
||||
})
|
||||
return null
|
||||
}
|
||||
await act(async () => {
|
||||
mounted.push(create(createElement(Harness)))
|
||||
})
|
||||
expect(client.sendRequest).toHaveBeenCalledWith('preflight.detectRemoteAgents', {
|
||||
connectionId: 'ssh-1'
|
||||
})
|
||||
expect(
|
||||
client.sendRequest.mock.calls.some(([method]) => method === 'preflight.detectAgents')
|
||||
).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -13,7 +13,9 @@ import {
|
||||
} from '../tasks/workspace-ssh-gate'
|
||||
|
||||
type DetectedAgentIdsState = {
|
||||
client: RpcClient
|
||||
connectionId: string | null
|
||||
repoId: string | null
|
||||
ids: Set<string>
|
||||
}
|
||||
|
||||
@@ -29,12 +31,13 @@ export function useNewWorkspaceExecutionTarget(args: {
|
||||
client: RpcClient | null
|
||||
connectionId: string | null
|
||||
visible: boolean
|
||||
repoId?: string | null
|
||||
}): {
|
||||
sshGate: WorkspaceSshGate
|
||||
detectedAgentIds: Set<string> | null
|
||||
connect: () => Promise<void>
|
||||
} {
|
||||
const { client, connectionId, visible } = args
|
||||
const { client, connectionId, visible, repoId = null } = args
|
||||
const [sshState, setSshState] = useState<WorkspaceSshRecord | null>(null)
|
||||
const [connectingTargetId, setConnectingTargetId] = useState<string | null>(null)
|
||||
const [detectedAgentIdsState, setDetectedAgentIdsState] = useState<DetectedAgentIdsState | null>(
|
||||
@@ -46,7 +49,9 @@ export function useNewWorkspaceExecutionTarget(args: {
|
||||
connecting: connectingTargetId === connectionId
|
||||
})
|
||||
const detectedAgentIds =
|
||||
detectedAgentIdsState?.connectionId === connectionId &&
|
||||
detectedAgentIdsState?.client === client &&
|
||||
detectedAgentIdsState.connectionId === connectionId &&
|
||||
detectedAgentIdsState.repoId === repoId &&
|
||||
(connectionId === null || sshGate.status === 'connected')
|
||||
? detectedAgentIdsState.ids
|
||||
: null
|
||||
@@ -92,23 +97,29 @@ export function useNewWorkspaceExecutionTarget(args: {
|
||||
? remoteAgentDetectionRead.interpret(
|
||||
await remoteAgentDetectionRead.request(client, { connectionId })
|
||||
)
|
||||
: localAgentDetectionRead.interpret(await localAgentDetectionRead.request(client))
|
||||
: localAgentDetectionRead.interpret(
|
||||
await (repoId
|
||||
? localAgentDetectionRead.request(client, { repoId })
|
||||
: localAgentDetectionRead.request(client))
|
||||
)
|
||||
if (!stale) {
|
||||
setDetectedAgentIdsState({
|
||||
client,
|
||||
connectionId,
|
||||
repoId,
|
||||
ids: detected.accepted ? new Set(detected.value) : new Set()
|
||||
})
|
||||
}
|
||||
} catch {
|
||||
if (!stale) {
|
||||
setDetectedAgentIdsState({ connectionId, ids: new Set() })
|
||||
setDetectedAgentIdsState({ client, connectionId, repoId, ids: new Set() })
|
||||
}
|
||||
}
|
||||
})()
|
||||
return () => {
|
||||
stale = true
|
||||
}
|
||||
}, [client, connectionId, sshGate.status, visible])
|
||||
}, [client, connectionId, repoId, sshGate.status, visible])
|
||||
|
||||
async function connect(): Promise<void> {
|
||||
if (!client || !connectionId) {
|
||||
|
||||
@@ -25,7 +25,7 @@ const { useMobileTasksWorkspaceSshState } = await import('./use-mobile-tasks-wor
|
||||
const { useMobileTasksWorkspaceCreateActions } =
|
||||
await import('./use-mobile-tasks-workspace-create-actions')
|
||||
|
||||
type Sent = { method: string; executionHost?: string }
|
||||
type Sent = { method: string; params?: unknown; executionHost?: string }
|
||||
const sentBy = new WeakMap<FakeSession, Sent[]>()
|
||||
|
||||
/** Records where each call ran, then answers it with `answer`. */
|
||||
@@ -38,10 +38,10 @@ function recording(
|
||||
client.sendRequest.mockImplementation(
|
||||
async (
|
||||
method: string,
|
||||
_params?: unknown,
|
||||
params?: unknown,
|
||||
options?: { envelope?: { executionHost?: string } }
|
||||
) => {
|
||||
sent.push({ method, executionHost: options?.envelope?.executionHost })
|
||||
sent.push({ method, params, executionHost: options?.envelope?.executionHost })
|
||||
return answer(method, options?.envelope?.executionHost)
|
||||
}
|
||||
)
|
||||
@@ -165,6 +165,9 @@ describe('creating a workspace from a task on a server repo', () => {
|
||||
const { client, pushed } = await createFrom(SERVER_REPO)
|
||||
expect(ranOn(client, 'worktree.create')).toEqual(['runtime:vm'])
|
||||
expect(ranOn(client, 'preflight.detectAgents')).toEqual(['runtime:vm'])
|
||||
expect(
|
||||
sentBy.get(client)?.find((sent) => sent.method === 'preflight.detectAgents')?.params
|
||||
).toEqual({ repoId: SERVER_REPO.id })
|
||||
expect(pushed).toHaveLength(1)
|
||||
expect(new URL(pushed[0]!, 'orca://x').searchParams.get('executionHost')).toBe('runtime:vm')
|
||||
})
|
||||
@@ -173,6 +176,9 @@ describe('creating a workspace from a task on a server repo', () => {
|
||||
const { client, pushed } = await createFrom(DESKTOP_REPO)
|
||||
expect(ranOn(client, 'worktree.create')).toEqual(['desktop'])
|
||||
expect(ranOn(client, 'preflight.detectAgents')).toEqual(['desktop'])
|
||||
expect(
|
||||
sentBy.get(client)?.find((sent) => sent.method === 'preflight.detectAgents')?.params
|
||||
).toEqual({ repoId: DESKTOP_REPO.id })
|
||||
expect(new URL(pushed[0]!, 'orca://x').searchParams.get('executionHost')).toBeNull()
|
||||
})
|
||||
})
|
||||
|
||||
@@ -123,7 +123,12 @@ export function useMobileTasksWorkspaceSshState(model: WorkspaceSparseActionsMod
|
||||
connectionId: workspaceCreateTargetRepo.connectionId
|
||||
})
|
||||
}
|
||||
: { operation: localAgentDetectionRead, reply: localAgentDetectionRead.request(repoClient) }
|
||||
: {
|
||||
operation: localAgentDetectionRead,
|
||||
reply: localAgentDetectionRead.request(repoClient, {
|
||||
repoId: workspaceCreateTargetRepo.id
|
||||
})
|
||||
}
|
||||
void detection.reply
|
||||
.then((reply) => {
|
||||
if (stale) {
|
||||
|
||||
@@ -18,15 +18,16 @@ vi.mock('electron', () => electronMocks)
|
||||
import { OrcaRuntimeService } from './orca-runtime'
|
||||
import { FLOATING_TERMINAL_WORKTREE_ID } from '../../shared/constants'
|
||||
import type { Repo } from '../../shared/repo-types'
|
||||
import type { Project } from '../../shared/project-types'
|
||||
import type { ResolvedWorktree } from './runtime-worktree-path-identity'
|
||||
|
||||
const originalPlatform = process.platform
|
||||
|
||||
function makeRuntime(repos: Repo[]) {
|
||||
function makeRuntime(repos: Repo[], projects: Project[] = []) {
|
||||
const store = {
|
||||
getRepo: (id: string) => repos.find((repo) => repo.id === id),
|
||||
getRepos: () => repos,
|
||||
getProjects: () => [],
|
||||
getProjects: () => projects,
|
||||
getSettings: () => ({ localWindowsRuntimeDefault: { kind: 'wsl', distro: 'Ubuntu' } })
|
||||
}
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: detection resolution reads only the repo, project and settings members supplied here.
|
||||
@@ -79,6 +80,165 @@ describe('OrcaRuntimeService.resolveAgentDetectionHost', () => {
|
||||
)
|
||||
})
|
||||
|
||||
it('resolves the selected repo before a workspace exists (#20060)', async () => {
|
||||
const wslRepo: Repo = { ...local, path: '\\\\wsl.localhost\\Debian\\home\\neil\\repo' }
|
||||
const { runtime, selector } = makeRuntime([wslRepo])
|
||||
expect(await runtime.resolveAgentDetectionHost(undefined, local.id)).toEqual({
|
||||
kind: 'local',
|
||||
context: {
|
||||
projectRuntime: {
|
||||
status: 'resolved',
|
||||
runtime: expect.objectContaining({ kind: 'wsl', distro: 'Debian' })
|
||||
}
|
||||
}
|
||||
})
|
||||
expect(selector).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('refuses a repository target without a store while retaining the untargeted default', async () => {
|
||||
const runtime = new OrcaRuntimeService(null)
|
||||
await expect(runtime.resolveAgentDetectionHost(undefined, local.id)).rejects.toThrow(
|
||||
'repo_not_found'
|
||||
)
|
||||
expect(await runtime.resolveAgentDetectionHost(undefined)).toEqual({ kind: 'local' })
|
||||
expect(
|
||||
await runtime.resolveAgentDetectionHost(FLOATING_TERMINAL_WORKTREE_ID, local.id)
|
||||
).toEqual({
|
||||
kind: 'local'
|
||||
})
|
||||
})
|
||||
|
||||
it.each(['removed', 'path', 'owner', 'connection', 'duplicate'] as const)(
|
||||
'refuses a selected repository %s change across the selector await',
|
||||
async (change) => {
|
||||
const selected = { ...local, path: '\\\\wsl$\\Old\\home\\repo' }
|
||||
const repos: Repo[] = [selected]
|
||||
const { runtime } = makeRuntime(repos)
|
||||
const pending = runtime.resolveAgentDetectionHost(undefined, local.id)
|
||||
repos.splice(
|
||||
0,
|
||||
1,
|
||||
...(change === 'removed'
|
||||
? []
|
||||
: change === 'duplicate'
|
||||
? [selected, { ...selected }]
|
||||
: [
|
||||
{
|
||||
...selected,
|
||||
...(change === 'path' ? { path: '\\\\wsl$\\Current\\home\\repo' } : {}),
|
||||
...(change === 'owner' ? { executionHostId: 'ssh:build-host' as const } : {}),
|
||||
...(change === 'connection' ? { connectionId: 'build-host' } : {})
|
||||
}
|
||||
])
|
||||
)
|
||||
await expect(pending).rejects.toThrow('repo_target_changed')
|
||||
}
|
||||
)
|
||||
|
||||
it('keeps repo detection on its configured runtime for a Windows path', async () => {
|
||||
const { runtime } = makeRuntime([local])
|
||||
expect(await runtime.resolveAgentDetectionHost(undefined, local.id)).toMatchObject({
|
||||
kind: 'local',
|
||||
context: { projectRuntime: { runtime: { kind: 'wsl', distro: 'Ubuntu' } } }
|
||||
})
|
||||
})
|
||||
|
||||
it.each(['darwin', 'linux', 'win32'])(
|
||||
'matches existing workspace runtime resolution on %s',
|
||||
async (platform) => {
|
||||
Object.defineProperty(process, 'platform', { value: platform })
|
||||
const selected = { ...local, path: platform === 'win32' ? local.path : '/src/r1' }
|
||||
const { runtime, selector } = makeRuntime([selected])
|
||||
selector.mockResolvedValue(
|
||||
worktree({ repoId: selected.id, path: selected.path, hostId: 'local' })
|
||||
)
|
||||
const detected = await runtime.resolveAgentDetectionHost(undefined, selected.id)
|
||||
expect(detected).toEqual(
|
||||
await runtime.resolveAgentDetectionHost(`${selected.id}::${selected.path}`)
|
||||
)
|
||||
expect(detected).toMatchObject({
|
||||
kind: 'local',
|
||||
context: {
|
||||
projectRuntime: { runtime: { kind: platform === 'win32' ? 'wsl' : 'local-host' } }
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
it('keeps project overrides above a selected folder repo WSL path', async () => {
|
||||
const folderRepo: Repo = {
|
||||
...local,
|
||||
kind: 'folder',
|
||||
path: '\\\\wsl$\\Debian\\home\\neil\\notes'
|
||||
}
|
||||
const project: Project = {
|
||||
id: 'project-1',
|
||||
displayName: 'Notes',
|
||||
badgeColor: 'blue',
|
||||
sourceRepoIds: [local.id],
|
||||
localWindowsRuntimePreference: { kind: 'windows-host' },
|
||||
createdAt: 1,
|
||||
updatedAt: 1
|
||||
}
|
||||
const { runtime } = makeRuntime([folderRepo], [project])
|
||||
expect(await runtime.resolveAgentDetectionHost(undefined, local.id)).toMatchObject({
|
||||
kind: 'local',
|
||||
context: { projectRuntime: { runtime: { kind: 'windows-host', reason: 'project-override' } } }
|
||||
})
|
||||
})
|
||||
|
||||
it.each([{ connectionId: 'build-host' }, { executionHostId: 'ssh:build-host' }] as const)(
|
||||
'preserves selected repo SSH ownership: %j',
|
||||
async (ownership) => {
|
||||
const { runtime } = makeRuntime([
|
||||
{ ...local, executionHostId: undefined, ...ownership, path: '/srv/r1' }
|
||||
])
|
||||
expect(await runtime.resolveAgentDetectionHost(undefined, local.id)).toEqual({
|
||||
kind: 'ssh',
|
||||
connectionId: 'build-host'
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
it('keeps an explicit workspace authoritative when a repo target is also sent', async () => {
|
||||
const { runtime, selector } = makeRuntime([local])
|
||||
selector.mockResolvedValue(worktree({ repoId: local.id, path: local.path, hostId: 'local' }))
|
||||
const workspaceId = `${local.id}::${local.path}`
|
||||
expect(await runtime.resolveAgentDetectionHost(workspaceId, 'missing')).toEqual(
|
||||
await runtime.resolveAgentDetectionHost(workspaceId)
|
||||
)
|
||||
expect(
|
||||
await runtime.resolveAgentDetectionHost(FLOATING_TERMINAL_WORKTREE_ID, 'missing')
|
||||
).toEqual({ kind: 'local' })
|
||||
})
|
||||
|
||||
it('refuses an unknown or ambiguous repo instead of probing the host default', async () => {
|
||||
const { runtime } = makeRuntime([local, { ...local, executionHostId: 'runtime:other' }])
|
||||
await expect(runtime.resolveAgentDetectionHost(undefined, local.id)).rejects.toThrow(
|
||||
'selector_ambiguous'
|
||||
)
|
||||
await expect(runtime.resolveAgentDetectionHost(undefined, 'missing')).rejects.toThrow(
|
||||
'repo_not_found'
|
||||
)
|
||||
})
|
||||
|
||||
it.each(['runtime:%broken', 'ssh:%broken'] as const)(
|
||||
'refuses a selected repository with malformed owner %s',
|
||||
async (executionHostId) => {
|
||||
const { runtime } = makeRuntime([{ ...local, executionHostId }])
|
||||
await expect(runtime.resolveAgentDetectionHost(undefined, local.id)).rejects.toThrow(
|
||||
'worktree_execution_host_unresolved'
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
it('refuses a repo owned by another runtime', async () => {
|
||||
const { runtime } = makeRuntime([{ ...local, executionHostId: 'runtime:other' }])
|
||||
await expect(runtime.resolveAgentDetectionHost(undefined, local.id)).rejects.toThrow(
|
||||
'workspace_on_other_runtime'
|
||||
)
|
||||
})
|
||||
|
||||
it("routes by the host's own worktree record", async () => {
|
||||
const ssh: Repo = { ...local, path: '/srv/r1', executionHostId: 'ssh:build-host' }
|
||||
const { runtime, selector } = makeRuntime([ssh, local])
|
||||
|
||||
@@ -68,8 +68,8 @@ describe('preflight RPC methods', () => {
|
||||
const refreshed = await dispatcher.dispatch(makeRequest('preflight.refreshAgents', null))
|
||||
|
||||
// An older client sends no workspace and keeps the host-default probe.
|
||||
expect(resolveAgentDetectionHost).toHaveBeenNthCalledWith(1, undefined)
|
||||
expect(resolveAgentDetectionHost).toHaveBeenNthCalledWith(2, undefined)
|
||||
expect(resolveAgentDetectionHost).toHaveBeenNthCalledWith(1, undefined, undefined)
|
||||
expect(resolveAgentDetectionHost).toHaveBeenNthCalledWith(2, undefined, undefined)
|
||||
expect(detectInstalledAgentsWithShellPathHydrationMock).toHaveBeenCalledWith(undefined)
|
||||
expect(refreshShellPathAndDetectAgentsMock).toHaveBeenCalledWith(undefined)
|
||||
expect(detected).toMatchObject({ ok: true, result: ['codex'] })
|
||||
@@ -118,6 +118,58 @@ describe('preflight RPC methods', () => {
|
||||
expect(ssh).toMatchObject({ ok: false, error: { message: SSH_HOST_NOT_SERVED_MESSAGE } })
|
||||
})
|
||||
|
||||
it('forwards a repo target before workspace creation and rejects malformed ids', async () => {
|
||||
detectInstalledAgentsWithShellPathHydrationMock.mockResolvedValueOnce(['codex'])
|
||||
const resolveAgentDetectionHost = vi.fn(() => ({ kind: 'local' }) as const)
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the dispatcher reads only these two runtime members.
|
||||
const runtime = {
|
||||
getRuntimeId: () => 'runtime',
|
||||
resolveAgentDetectionHost
|
||||
} as unknown as OrcaRuntimeService
|
||||
const dispatcher = new RpcDispatcher({ runtime, methods: PREFLIGHT_METHODS })
|
||||
const response = await dispatcher.dispatch(
|
||||
makeRequest('preflight.detectAgents', {
|
||||
repoId: 'r1',
|
||||
path: '/client-controlled',
|
||||
wslDistro: 'client-controlled',
|
||||
executionHostId: 'local'
|
||||
})
|
||||
)
|
||||
expect(resolveAgentDetectionHost).toHaveBeenCalledWith(undefined, 'r1')
|
||||
expect(response).toMatchObject({ ok: true, result: ['codex'] })
|
||||
resolveAgentDetectionHost.mockClear()
|
||||
for (const repoId of ['', null, 1, { path: '/client-controlled' }]) {
|
||||
const malformed = await dispatcher.dispatch(makeRequest('preflight.detectAgents', { repoId }))
|
||||
expect(malformed).toMatchObject({ ok: false, error: { code: 'invalid_argument' } })
|
||||
}
|
||||
expect(resolveAgentDetectionHost).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it.each(['preflight.detectAgents', 'preflight.refreshAgents'])(
|
||||
'%s never probes locally after repository ownership refuses',
|
||||
async (method) => {
|
||||
detectInstalledAgentsWithShellPathHydrationMock.mockClear()
|
||||
refreshShellPathAndDetectAgentsMock.mockClear()
|
||||
const resolveAgentDetectionHost = vi
|
||||
.fn()
|
||||
.mockRejectedValue(new Error('workspace_on_other_runtime'))
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the dispatcher reads only these runtime members.
|
||||
const runtime = {
|
||||
getRuntimeId: () => 'runtime',
|
||||
resolveAgentDetectionHost
|
||||
} as unknown as OrcaRuntimeService
|
||||
const dispatcher = new RpcDispatcher({ runtime, methods: PREFLIGHT_METHODS })
|
||||
const response = await dispatcher.dispatch(makeRequest(method, { repoId: 'foreign-repo' }))
|
||||
expect(resolveAgentDetectionHost).toHaveBeenCalledWith(undefined, 'foreign-repo')
|
||||
expect(response).toMatchObject({
|
||||
ok: false,
|
||||
error: { message: 'workspace_on_other_runtime' }
|
||||
})
|
||||
expect(detectInstalledAgentsWithShellPathHydrationMock).not.toHaveBeenCalled()
|
||||
expect(refreshShellPathAndDetectAgentsMock).not.toHaveBeenCalled()
|
||||
}
|
||||
)
|
||||
|
||||
it('refuses agent detection on an SSH target this host does not serve', async () => {
|
||||
detectInstalledAgentsWithShellPathHydrationMock.mockClear()
|
||||
const runtime = { getRuntimeId: () => 'test-runtime' } as unknown as OrcaRuntimeService
|
||||
|
||||
@@ -27,7 +27,7 @@ export const PREFLIGHT_METHODS = [
|
||||
params: PreflightAgentDetection,
|
||||
// Why the host resolves: only it knows the workspace's project runtime, including WSL.
|
||||
handler: async (params, { runtime }) =>
|
||||
detectAgentsOnHost(await runtime.resolveAgentDetectionHost(params.worktreeId))
|
||||
detectAgentsOnHost(await runtime.resolveAgentDetectionHost(params.worktreeId, params.repoId))
|
||||
}),
|
||||
defineMethod({
|
||||
name: 'preflight.detectRemoteAgents',
|
||||
@@ -47,6 +47,6 @@ export const PREFLIGHT_METHODS = [
|
||||
permission: 'workspace',
|
||||
params: PreflightAgentDetection,
|
||||
handler: async (params, { runtime }) =>
|
||||
refreshAgentsOnHost(await runtime.resolveAgentDetectionHost(params.worktreeId))
|
||||
refreshAgentsOnHost(await runtime.resolveAgentDetectionHost(params.worktreeId, params.repoId))
|
||||
})
|
||||
]
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import { FLOATING_TERMINAL_WORKTREE_ID } from '../../shared/constants'
|
||||
import { getRepoRoutingConnectionId } from '../../shared/execution-host'
|
||||
import type { Repo } from '../../shared/repo-types'
|
||||
import { parseWorkspaceKey } from '../../shared/workspace-scope'
|
||||
import {
|
||||
HOST_DEFAULT_AGENT_DETECTION,
|
||||
resolveWorkspaceAgentDetectionHost,
|
||||
type AgentDetectionHost,
|
||||
type WorkspaceAgentDetectionStore
|
||||
} from '../preflight/workspace-agent-detection'
|
||||
import type { ResolvedWorktree } from './runtime-worktree-path-identity'
|
||||
|
||||
type AgentDetectionContext = {
|
||||
store: WorkspaceAgentDetectionStore | null | undefined
|
||||
resolveRepo: (selector: string) => Promise<Repo>
|
||||
resolveWorktree: (selector: string) => Promise<ResolvedWorktree>
|
||||
}
|
||||
|
||||
/** Resolve from the host's records, including the repo before a workspace exists. */
|
||||
export async function resolveRuntimeAgentDetectionHost(
|
||||
context: AgentDetectionContext,
|
||||
worktreeId: string | null | undefined,
|
||||
repoId?: string
|
||||
): Promise<AgentDetectionHost> {
|
||||
const { store } = context
|
||||
if (!store) {
|
||||
if (!worktreeId && repoId) {
|
||||
throw new Error('repo_not_found')
|
||||
}
|
||||
return HOST_DEFAULT_AGENT_DETECTION
|
||||
}
|
||||
if (!worktreeId && repoId) {
|
||||
const repo = await context.resolveRepo(`id:${repoId}`)
|
||||
const current = store.getRepos?.().filter((entry) => entry.id === repo.id) ?? []
|
||||
if (
|
||||
current.length !== 1 ||
|
||||
current[0].path !== repo.path ||
|
||||
current[0].executionHostId !== repo.executionHostId ||
|
||||
getRepoRoutingConnectionId(current[0]) !== getRepoRoutingConnectionId(repo)
|
||||
) {
|
||||
throw new Error('repo_target_changed')
|
||||
}
|
||||
return resolveWorkspaceAgentDetectionHost(store, {
|
||||
kind: 'worktree',
|
||||
repoId: repo.id,
|
||||
path: repo.path
|
||||
})
|
||||
}
|
||||
if (!worktreeId || worktreeId === FLOATING_TERMINAL_WORKTREE_ID) {
|
||||
return HOST_DEFAULT_AGENT_DETECTION
|
||||
}
|
||||
const scope = parseWorkspaceKey(worktreeId)
|
||||
if (scope?.type === 'folder') {
|
||||
return resolveWorkspaceAgentDetectionHost(store, {
|
||||
kind: 'folder',
|
||||
folderWorkspaceId: scope.folderWorkspaceId
|
||||
})
|
||||
}
|
||||
let worktree: ResolvedWorktree
|
||||
try {
|
||||
worktree = await context.resolveWorktree(`id:${worktreeId}`)
|
||||
} catch (error) {
|
||||
// Only unknown workspaces retain the legacy default; an ambiguous owner must refuse.
|
||||
if (error instanceof Error && error.message === 'selector_not_found') {
|
||||
return HOST_DEFAULT_AGENT_DETECTION
|
||||
}
|
||||
throw error
|
||||
}
|
||||
return resolveWorkspaceAgentDetectionHost(store, {
|
||||
kind: 'worktree',
|
||||
repoId: worktree.repoId,
|
||||
path: worktree.path,
|
||||
hostId: worktree.hostId
|
||||
})
|
||||
}
|
||||
@@ -14,14 +14,8 @@ import { RECENT_PTY_OUTPUT_LIMIT, RecentPtyOutputBuffer } from '../../recent-pty
|
||||
import { appendRecentPtyPathCandidates } from '../../terminal-output-path-candidates'
|
||||
import type { ProjectExecutionRuntimeResolution } from '../../../../shared/project-execution-runtime'
|
||||
import { resolveLocalProjectRuntimeForWorktreeId } from '../../../local-project-runtime-resolution'
|
||||
import {
|
||||
HOST_DEFAULT_AGENT_DETECTION,
|
||||
resolveWorkspaceAgentDetectionHost,
|
||||
type AgentDetectionHost
|
||||
} from '../../../preflight/workspace-agent-detection'
|
||||
import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../../shared/constants'
|
||||
import { parseWorkspaceKey } from '../../../../shared/workspace-scope'
|
||||
import type { ResolvedWorktree } from '../../runtime-worktree-path-identity'
|
||||
import type { AgentDetectionHost } from '../../../preflight/workspace-agent-detection'
|
||||
import { resolveRuntimeAgentDetectionHost } from '../../runtime-agent-detection-host'
|
||||
import type { RuntimePtyWorktreeRecord } from '../../runtime-terminal-state-records'
|
||||
import {
|
||||
localOrchestrationCliCommand,
|
||||
@@ -263,39 +257,19 @@ export abstract class OrcaRuntimeWithGetOrchestrationDispatchAuthority extends O
|
||||
: undefined
|
||||
}
|
||||
|
||||
// Why the host's own worktree record: it carries the owning host and the real path, where the
|
||||
// client's id alone could pick the wrong row for a repo id shared across hosts.
|
||||
async resolveAgentDetectionHost(
|
||||
worktreeId: string | null | undefined
|
||||
worktreeId: string | null | undefined,
|
||||
repoId?: string
|
||||
): Promise<AgentDetectionHost> {
|
||||
if (!this.store || !worktreeId || worktreeId === FLOATING_TERMINAL_WORKTREE_ID) {
|
||||
return HOST_DEFAULT_AGENT_DETECTION
|
||||
}
|
||||
const store = this.requireStore()
|
||||
const scope = parseWorkspaceKey(worktreeId)
|
||||
if (scope?.type === 'folder') {
|
||||
return resolveWorkspaceAgentDetectionHost(store, {
|
||||
kind: 'folder',
|
||||
folderWorkspaceId: scope.folderWorkspaceId
|
||||
})
|
||||
}
|
||||
let worktree: ResolvedWorktree
|
||||
try {
|
||||
worktree = await this.resolveWorktreeSelector(`id:${worktreeId}`)
|
||||
} catch (error) {
|
||||
// Only an unknown workspace keeps the answer older clients always got; an ambiguous one
|
||||
// has no single host, and probing this one would list another machine's agents.
|
||||
if (error instanceof Error && error.message === 'selector_not_found') {
|
||||
return HOST_DEFAULT_AGENT_DETECTION
|
||||
}
|
||||
throw error
|
||||
}
|
||||
return resolveWorkspaceAgentDetectionHost(store, {
|
||||
kind: 'worktree',
|
||||
repoId: worktree.repoId,
|
||||
path: worktree.path,
|
||||
hostId: worktree.hostId
|
||||
})
|
||||
return resolveRuntimeAgentDetectionHost(
|
||||
{
|
||||
store: this.store,
|
||||
resolveRepo: (selector) => this.resolveRepoSelector(selector),
|
||||
resolveWorktree: (selector) => this.resolveWorktreeSelector(selector)
|
||||
},
|
||||
worktreeId,
|
||||
repoId
|
||||
)
|
||||
}
|
||||
|
||||
getOrchestrationFleetAgentStatusSnapshot() {
|
||||
|
||||
@@ -14,5 +14,6 @@ export const PreflightDetectRemoteWindowsTerminalCapabilities = z.object({
|
||||
|
||||
/** Why optional: a client that predates it sends nothing and keeps the host-default probe. */
|
||||
export const PreflightAgentDetection = z.object({
|
||||
worktreeId: z.string().min(1).optional()
|
||||
worktreeId: z.string().min(1).optional(),
|
||||
repoId: z.string().min(1).optional()
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user