mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-01 02:04:24 +00:00
Project a Petri run's sandbox instance from its scope records
Petri a5906f6 records where each scope's sandbox ran (`scope.acquired`, `scope.failed`) and how its lease was released (`scope.released`). The projection folds the root invocation's records into `Run.sandbox`: `initializing` from `run.started`, `ready` with the `RunSandboxInstance` (the provider, Petri's `host` as Fabro's `local`, the provider's id, the image and snapshot, the working directory) from `scope.acquired`, `failed` from `scope.failed`; the retention outcome is kept in the fold state, since the view has no field for it. Ask Fabro reconnect and `sandbox cp`, `preview` and `ssh` reach the run's sandbox again. A local reconnect designates the recorded working directory again when the host provider does not know the id: the provider mints a registry-only id for a workspace path too long for a path-derived one, and that registry belongs to the run's worker. The stream listing redacts its items the way the attached stream does, so a client that pages after a stream sees the same items. Pins move to Petri a5906f6 (run format 6, engine log v11, event contract 4). The attach stream snapshot is re-recorded with the new record and a filter for the host provider's minted ids; `sandbox cp` reads an upload back through the run's workspace, which is no longer the target folder. Server scenario tests prove the projected instance on the host and Docker providers. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
parent
2f4888c199
commit
f0b23fe426
13 changed files with 851 additions and 433 deletions
30
Cargo.lock
generated
30
Cargo.lock
generated
|
|
@ -5324,7 +5324,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
|
|||
[[package]]
|
||||
name = "petri-attractor-steps"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?rev=4d4bdd694aa573514968f8e8321123146d2d2458#4d4bdd694aa573514968f8e8321123146d2d2458"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?rev=a5906f6554b94f608ede3902daab106d5f8062c3#a5906f6554b94f608ede3902daab106d5f8062c3"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"globset",
|
||||
|
|
@ -5355,7 +5355,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "petri-driver"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?rev=4d4bdd694aa573514968f8e8321123146d2d2458#4d4bdd694aa573514968f8e8321123146d2d2458"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?rev=a5906f6554b94f608ede3902daab106d5f8062c3#a5906f6554b94f608ede3902daab106d5f8062c3"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"getrandom 0.3.4",
|
||||
|
|
@ -5375,7 +5375,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "petri-engine"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?rev=4d4bdd694aa573514968f8e8321123146d2d2458#4d4bdd694aa573514968f8e8321123146d2d2458"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?rev=a5906f6554b94f608ede3902daab106d5f8062c3#a5906f6554b94f608ede3902daab106d5f8062c3"
|
||||
dependencies = [
|
||||
"petri-ir",
|
||||
"serde",
|
||||
|
|
@ -5387,7 +5387,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "petri-execution"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?rev=4d4bdd694aa573514968f8e8321123146d2d2458#4d4bdd694aa573514968f8e8321123146d2d2458"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?rev=a5906f6554b94f608ede3902daab106d5f8062c3#a5906f6554b94f608ede3902daab106d5f8062c3"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"petri-driver",
|
||||
|
|
@ -5411,7 +5411,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "petri-executor"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?rev=4d4bdd694aa573514968f8e8321123146d2d2458#4d4bdd694aa573514968f8e8321123146d2d2458"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?rev=a5906f6554b94f608ede3902daab106d5f8062c3#a5906f6554b94f608ede3902daab106d5f8062c3"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"libc",
|
||||
|
|
@ -5426,7 +5426,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "petri-executor-sandbox"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?rev=4d4bdd694aa573514968f8e8321123146d2d2458#4d4bdd694aa573514968f8e8321123146d2d2458"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?rev=a5906f6554b94f608ede3902daab106d5f8062c3#a5906f6554b94f608ede3902daab106d5f8062c3"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"petri-executor",
|
||||
|
|
@ -5448,7 +5448,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "petri-frontend"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?rev=4d4bdd694aa573514968f8e8321123146d2d2458#4d4bdd694aa573514968f8e8321123146d2d2458"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?rev=a5906f6554b94f608ede3902daab106d5f8062c3#a5906f6554b94f608ede3902daab106d5f8062c3"
|
||||
dependencies = [
|
||||
"marked-yaml",
|
||||
"petri-ir",
|
||||
|
|
@ -5462,7 +5462,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "petri-frontend-attractor"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?rev=4d4bdd694aa573514968f8e8321123146d2d2458#4d4bdd694aa573514968f8e8321123146d2d2458"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?rev=a5906f6554b94f608ede3902daab106d5f8062c3#a5906f6554b94f608ede3902daab106d5f8062c3"
|
||||
dependencies = [
|
||||
"minijinja",
|
||||
"petri-frontend",
|
||||
|
|
@ -5479,7 +5479,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "petri-frontend-fabro"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?rev=4d4bdd694aa573514968f8e8321123146d2d2458#4d4bdd694aa573514968f8e8321123146d2d2458"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?rev=a5906f6554b94f608ede3902daab106d5f8062c3#a5906f6554b94f608ede3902daab106d5f8062c3"
|
||||
dependencies = [
|
||||
"petri-frontend",
|
||||
"petri-frontend-attractor",
|
||||
|
|
@ -5495,7 +5495,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "petri-frontend-native"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?rev=4d4bdd694aa573514968f8e8321123146d2d2458#4d4bdd694aa573514968f8e8321123146d2d2458"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?rev=a5906f6554b94f608ede3902daab106d5f8062c3#a5906f6554b94f608ede3902daab106d5f8062c3"
|
||||
dependencies = [
|
||||
"petri-frontend",
|
||||
"petri-ir",
|
||||
|
|
@ -5506,7 +5506,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "petri-ir"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?rev=4d4bdd694aa573514968f8e8321123146d2d2458#4d4bdd694aa573514968f8e8321123146d2d2458"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?rev=a5906f6554b94f608ede3902daab106d5f8062c3#a5906f6554b94f608ede3902daab106d5f8062c3"
|
||||
dependencies = [
|
||||
"regex",
|
||||
"serde",
|
||||
|
|
@ -5519,7 +5519,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "petri-runtime"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?rev=4d4bdd694aa573514968f8e8321123146d2d2458#4d4bdd694aa573514968f8e8321123146d2d2458"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?rev=a5906f6554b94f608ede3902daab106d5f8062c3#a5906f6554b94f608ede3902daab106d5f8062c3"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"petri-driver",
|
||||
|
|
@ -5540,7 +5540,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "petri-steps"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?rev=4d4bdd694aa573514968f8e8321123146d2d2458#4d4bdd694aa573514968f8e8321123146d2d2458"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?rev=a5906f6554b94f608ede3902daab106d5f8062c3#a5906f6554b94f608ede3902daab106d5f8062c3"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"petri-executor",
|
||||
|
|
@ -5556,7 +5556,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "petri-store"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?rev=4d4bdd694aa573514968f8e8321123146d2d2458#4d4bdd694aa573514968f8e8321123146d2d2458"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?rev=a5906f6554b94f608ede3902daab106d5f8062c3#a5906f6554b94f608ede3902daab106d5f8062c3"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"getrandom 0.3.4",
|
||||
|
|
@ -5571,7 +5571,7 @@ dependencies = [
|
|||
[[package]]
|
||||
name = "petri-testkit"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?rev=4d4bdd694aa573514968f8e8321123146d2d2458#4d4bdd694aa573514968f8e8321123146d2d2458"
|
||||
source = "git+https://github.com/lithoscomputer/petri.git?rev=a5906f6554b94f608ede3902daab106d5f8062c3#a5906f6554b94f608ede3902daab106d5f8062c3"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"petri-driver",
|
||||
|
|
|
|||
14
Cargo.toml
14
Cargo.toml
|
|
@ -132,13 +132,13 @@ pebble-cli-core = { git = "https://github.com/lithoscomputer/pebble", rev = "a39
|
|||
# lithos-llm and sandbox-driver revisions as this file, so the workspace links
|
||||
# one copy of each. Only `fabro-petri` may depend on these packages; the keys
|
||||
# carry the `petri_` prefix so the crate names say where they come from.
|
||||
petri_runtime = { git = "https://github.com/lithoscomputer/petri.git", rev = "4d4bdd694aa573514968f8e8321123146d2d2458", package = "petri-runtime" }
|
||||
petri_execution = { git = "https://github.com/lithoscomputer/petri.git", rev = "4d4bdd694aa573514968f8e8321123146d2d2458", package = "petri-execution" }
|
||||
petri_store = { git = "https://github.com/lithoscomputer/petri.git", rev = "4d4bdd694aa573514968f8e8321123146d2d2458", package = "petri-store" }
|
||||
petri_attractor_steps = { git = "https://github.com/lithoscomputer/petri.git", rev = "4d4bdd694aa573514968f8e8321123146d2d2458", package = "petri-attractor-steps" }
|
||||
petri_frontend_attractor = { git = "https://github.com/lithoscomputer/petri.git", rev = "4d4bdd694aa573514968f8e8321123146d2d2458", package = "petri-frontend-attractor" }
|
||||
petri_frontend_fabro = { git = "https://github.com/lithoscomputer/petri.git", rev = "4d4bdd694aa573514968f8e8321123146d2d2458", package = "petri-frontend-fabro" }
|
||||
petri_testkit = { git = "https://github.com/lithoscomputer/petri.git", rev = "4d4bdd694aa573514968f8e8321123146d2d2458", package = "petri-testkit" }
|
||||
petri_runtime = { git = "https://github.com/lithoscomputer/petri.git", rev = "a5906f6554b94f608ede3902daab106d5f8062c3", package = "petri-runtime" }
|
||||
petri_execution = { git = "https://github.com/lithoscomputer/petri.git", rev = "a5906f6554b94f608ede3902daab106d5f8062c3", package = "petri-execution" }
|
||||
petri_store = { git = "https://github.com/lithoscomputer/petri.git", rev = "a5906f6554b94f608ede3902daab106d5f8062c3", package = "petri-store" }
|
||||
petri_attractor_steps = { git = "https://github.com/lithoscomputer/petri.git", rev = "a5906f6554b94f608ede3902daab106d5f8062c3", package = "petri-attractor-steps" }
|
||||
petri_frontend_attractor = { git = "https://github.com/lithoscomputer/petri.git", rev = "a5906f6554b94f608ede3902daab106d5f8062c3", package = "petri-frontend-attractor" }
|
||||
petri_frontend_fabro = { git = "https://github.com/lithoscomputer/petri.git", rev = "a5906f6554b94f608ede3902daab106d5f8062c3", package = "petri-frontend-fabro" }
|
||||
petri_testkit = { git = "https://github.com/lithoscomputer/petri.git", rev = "a5906f6554b94f608ede3902daab106d5f8062c3", package = "petri-testkit" }
|
||||
sentry = { version = "0.35", default-features = false, features = ["backtrace", "contexts", "ureq", "rustls"] }
|
||||
fork = "0.2"
|
||||
exec = "0.3"
|
||||
|
|
|
|||
|
|
@ -902,7 +902,7 @@ fn attach_json_errors_without_prompting_for_human_input() {
|
|||
"recorded_at": "[EPOCH_MS]",
|
||||
"body": {
|
||||
"event": "run.started",
|
||||
"format_version": 5,
|
||||
"format_version": 6,
|
||||
"key": "[ULID]",
|
||||
"root": 0,
|
||||
"middleware_chain": [
|
||||
|
|
@ -1323,46 +1323,20 @@ fn attach_json_errors_without_prompting_for_human_input() {
|
|||
"invocation": 0,
|
||||
"execution": 0
|
||||
},
|
||||
"subject": {
|
||||
"node": {
|
||||
"id": 0,
|
||||
"name": "start",
|
||||
"kind": "attractor/stage",
|
||||
"meta": {
|
||||
"label": "Start",
|
||||
"shape": "Mdiamond",
|
||||
"kind": "start",
|
||||
"classes": [],
|
||||
"span": {
|
||||
"line": 3,
|
||||
"column": 3
|
||||
},
|
||||
"admission_hooks": "step",
|
||||
"edges": {
|
||||
"0": {
|
||||
"to": "approve",
|
||||
"label": null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"firing": 1,
|
||||
"visit": 1,
|
||||
"attempt": 1,
|
||||
"generation": 0,
|
||||
"branch": {
|
||||
"role": "none"
|
||||
}
|
||||
},
|
||||
"recorded_at": "[EPOCH_MS]",
|
||||
"record": {
|
||||
"seq": 4,
|
||||
"origin": "external",
|
||||
"recorded_at": "[EPOCH_MS]",
|
||||
"body": {
|
||||
"event": "step.started",
|
||||
"firing": 1,
|
||||
"attempt": 1
|
||||
"event": "scope.acquired",
|
||||
"scope": 0,
|
||||
"lease": 0,
|
||||
"workspace": "invocation-0-scope-0",
|
||||
"provider": "host",
|
||||
"instance": "host-g[ID]",
|
||||
"working_directory": "[RUN_DIR]/petri/scopes/invocation-0-scope-0/work",
|
||||
"duration_ms": "[DURATION_MS]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1371,62 +1345,6 @@ fn attach_json_errors_without_prompting_for_human_input() {
|
|||
"run_id": "[ULID]",
|
||||
"stream_seq": 18,
|
||||
"kind": "petri",
|
||||
"id": "execution 0/4/1",
|
||||
"recorded_at": "[EPOCH_MS]",
|
||||
"item": {
|
||||
"id": {
|
||||
"log": "execution",
|
||||
"execution": 0,
|
||||
"seq": 4,
|
||||
"index": 1
|
||||
},
|
||||
"origin": "derived",
|
||||
"context": {
|
||||
"invocation": 0,
|
||||
"execution": 0
|
||||
},
|
||||
"subject": {
|
||||
"node": {
|
||||
"id": 0,
|
||||
"name": "start",
|
||||
"kind": "attractor/stage",
|
||||
"meta": {
|
||||
"label": "Start",
|
||||
"shape": "Mdiamond",
|
||||
"kind": "start",
|
||||
"classes": [],
|
||||
"span": {
|
||||
"line": 3,
|
||||
"column": 3
|
||||
},
|
||||
"admission_hooks": "step",
|
||||
"edges": {
|
||||
"0": {
|
||||
"to": "approve",
|
||||
"label": null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"firing": 1,
|
||||
"visit": 1,
|
||||
"attempt": 1,
|
||||
"generation": 0,
|
||||
"branch": {
|
||||
"role": "none"
|
||||
}
|
||||
},
|
||||
"recorded_at": "[EPOCH_MS]",
|
||||
"derived": {
|
||||
"event": "wait.state.changed",
|
||||
"state": "running"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"run_id": "[ULID]",
|
||||
"stream_seq": 19,
|
||||
"kind": "petri",
|
||||
"id": "execution 0/5/0",
|
||||
"recorded_at": "[EPOCH_MS]",
|
||||
"item": {
|
||||
|
|
@ -1478,15 +1396,66 @@ fn attach_json_errors_without_prompting_for_human_input() {
|
|||
"origin": "external",
|
||||
"recorded_at": "[EPOCH_MS]",
|
||||
"body": {
|
||||
"event": "step.progress.recorded",
|
||||
"event": "step.started",
|
||||
"firing": 1,
|
||||
"ev": {
|
||||
"log": {
|
||||
"stream": "stderr",
|
||||
"line": "checkout: [TEMP_DIR] is not a Git repository; the workspace starts empty"
|
||||
"attempt": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"run_id": "[ULID]",
|
||||
"stream_seq": 19,
|
||||
"kind": "petri",
|
||||
"id": "execution 0/5/1",
|
||||
"recorded_at": "[EPOCH_MS]",
|
||||
"item": {
|
||||
"id": {
|
||||
"log": "execution",
|
||||
"execution": 0,
|
||||
"seq": 5,
|
||||
"index": 1
|
||||
},
|
||||
"origin": "derived",
|
||||
"context": {
|
||||
"invocation": 0,
|
||||
"execution": 0
|
||||
},
|
||||
"subject": {
|
||||
"node": {
|
||||
"id": 0,
|
||||
"name": "start",
|
||||
"kind": "attractor/stage",
|
||||
"meta": {
|
||||
"label": "Start",
|
||||
"shape": "Mdiamond",
|
||||
"kind": "start",
|
||||
"classes": [],
|
||||
"span": {
|
||||
"line": 3,
|
||||
"column": 3
|
||||
},
|
||||
"admission_hooks": "step",
|
||||
"edges": {
|
||||
"0": {
|
||||
"to": "approve",
|
||||
"label": null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"firing": 1,
|
||||
"visit": 1,
|
||||
"attempt": 1,
|
||||
"generation": 0,
|
||||
"branch": {
|
||||
"role": "none"
|
||||
}
|
||||
},
|
||||
"recorded_at": "[EPOCH_MS]",
|
||||
"derived": {
|
||||
"event": "wait.state.changed",
|
||||
"state": "running"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -1548,34 +1517,9 @@ fn attach_json_errors_without_prompting_for_human_input() {
|
|||
"event": "step.progress.recorded",
|
||||
"firing": 1,
|
||||
"ev": {
|
||||
"custom": {
|
||||
"$note": {
|
||||
"kind": "fabro.checkpoint",
|
||||
"payload": {
|
||||
"execution": 0,
|
||||
"firing": 1,
|
||||
"attempt": 1,
|
||||
"workspace": "invocation-0-scope-0",
|
||||
"git_commit_sha": "[DIGEST]",
|
||||
"reused": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"derived": {
|
||||
"parsed": {
|
||||
"kind": "note",
|
||||
"note": {
|
||||
"kind": "fabro.checkpoint",
|
||||
"payload": {
|
||||
"execution": 0,
|
||||
"firing": 1,
|
||||
"attempt": 1,
|
||||
"workspace": "invocation-0-scope-0",
|
||||
"git_commit_sha": "[DIGEST]",
|
||||
"reused": false
|
||||
"log": {
|
||||
"stream": "stderr",
|
||||
"line": "checkout: [TEMP_DIR] is not a Git repository; the workspace starts empty"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1636,6 +1580,98 @@ fn attach_json_errors_without_prompting_for_human_input() {
|
|||
"seq": 7,
|
||||
"origin": "external",
|
||||
"recorded_at": "[EPOCH_MS]",
|
||||
"body": {
|
||||
"event": "step.progress.recorded",
|
||||
"firing": 1,
|
||||
"ev": {
|
||||
"custom": {
|
||||
"$note": {
|
||||
"kind": "fabro.checkpoint",
|
||||
"payload": {
|
||||
"execution": 0,
|
||||
"firing": 1,
|
||||
"attempt": 1,
|
||||
"workspace": "invocation-0-scope-0",
|
||||
"git_commit_sha": "[DIGEST]",
|
||||
"reused": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"derived": {
|
||||
"parsed": {
|
||||
"kind": "note",
|
||||
"note": {
|
||||
"kind": "fabro.checkpoint",
|
||||
"payload": {
|
||||
"execution": 0,
|
||||
"firing": 1,
|
||||
"attempt": 1,
|
||||
"workspace": "invocation-0-scope-0",
|
||||
"git_commit_sha": "[DIGEST]",
|
||||
"reused": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"run_id": "[ULID]",
|
||||
"stream_seq": 22,
|
||||
"kind": "petri",
|
||||
"id": "execution 0/8/0",
|
||||
"recorded_at": "[EPOCH_MS]",
|
||||
"item": {
|
||||
"id": {
|
||||
"log": "execution",
|
||||
"execution": 0,
|
||||
"seq": 8,
|
||||
"index": 0
|
||||
},
|
||||
"origin": "external",
|
||||
"context": {
|
||||
"invocation": 0,
|
||||
"execution": 0
|
||||
},
|
||||
"subject": {
|
||||
"node": {
|
||||
"id": 0,
|
||||
"name": "start",
|
||||
"kind": "attractor/stage",
|
||||
"meta": {
|
||||
"label": "Start",
|
||||
"shape": "Mdiamond",
|
||||
"kind": "start",
|
||||
"classes": [],
|
||||
"span": {
|
||||
"line": 3,
|
||||
"column": 3
|
||||
},
|
||||
"admission_hooks": "step",
|
||||
"edges": {
|
||||
"0": {
|
||||
"to": "approve",
|
||||
"label": null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"firing": 1,
|
||||
"visit": 1,
|
||||
"attempt": 1,
|
||||
"generation": 0,
|
||||
"branch": {
|
||||
"role": "none"
|
||||
}
|
||||
},
|
||||
"recorded_at": "[EPOCH_MS]",
|
||||
"record": {
|
||||
"seq": 8,
|
||||
"origin": "external",
|
||||
"recorded_at": "[EPOCH_MS]",
|
||||
"body": {
|
||||
"event": "step.finished",
|
||||
"firing": 1,
|
||||
|
|
@ -1664,15 +1700,15 @@ fn attach_json_errors_without_prompting_for_human_input() {
|
|||
},
|
||||
{
|
||||
"run_id": "[ULID]",
|
||||
"stream_seq": 22,
|
||||
"stream_seq": 23,
|
||||
"kind": "petri",
|
||||
"id": "execution 0/7/1",
|
||||
"id": "execution 0/8/1",
|
||||
"recorded_at": "[EPOCH_MS]",
|
||||
"item": {
|
||||
"id": {
|
||||
"log": "execution",
|
||||
"execution": 0,
|
||||
"seq": 7,
|
||||
"seq": 8,
|
||||
"index": 1
|
||||
},
|
||||
"origin": "derived",
|
||||
|
|
@ -1735,7 +1771,7 @@ fn attach_json_errors_without_prompting_for_human_input() {
|
|||
},
|
||||
{
|
||||
"run_id": "[ULID]",
|
||||
"stream_seq": 23,
|
||||
"stream_seq": 24,
|
||||
"kind": "platform",
|
||||
"id": "[EVENT_ID]",
|
||||
"recorded_at": "[EPOCH_MS]",
|
||||
|
|
@ -1768,15 +1804,15 @@ fn attach_json_errors_without_prompting_for_human_input() {
|
|||
},
|
||||
{
|
||||
"run_id": "[ULID]",
|
||||
"stream_seq": 24,
|
||||
"stream_seq": 25,
|
||||
"kind": "petri",
|
||||
"id": "execution 0/8/0",
|
||||
"id": "execution 0/9/0",
|
||||
"recorded_at": "[EPOCH_MS]",
|
||||
"item": {
|
||||
"id": {
|
||||
"log": "execution",
|
||||
"execution": 0,
|
||||
"seq": 8,
|
||||
"seq": 9,
|
||||
"index": 0
|
||||
},
|
||||
"origin": "external",
|
||||
|
|
@ -1817,7 +1853,7 @@ fn attach_json_errors_without_prompting_for_human_input() {
|
|||
},
|
||||
"recorded_at": "[EPOCH_MS]",
|
||||
"record": {
|
||||
"seq": 8,
|
||||
"seq": 9,
|
||||
"origin": "external",
|
||||
"recorded_at": "[EPOCH_MS]",
|
||||
"body": {
|
||||
|
|
@ -1876,15 +1912,15 @@ fn attach_json_errors_without_prompting_for_human_input() {
|
|||
},
|
||||
{
|
||||
"run_id": "[ULID]",
|
||||
"stream_seq": 25,
|
||||
"stream_seq": 26,
|
||||
"kind": "petri",
|
||||
"id": "execution 0/8/1",
|
||||
"id": "execution 0/9/1",
|
||||
"recorded_at": "[EPOCH_MS]",
|
||||
"item": {
|
||||
"id": {
|
||||
"log": "execution",
|
||||
"execution": 0,
|
||||
"seq": 8,
|
||||
"seq": 9,
|
||||
"index": 1
|
||||
},
|
||||
"origin": "derived",
|
||||
|
|
@ -1945,15 +1981,15 @@ fn attach_json_errors_without_prompting_for_human_input() {
|
|||
},
|
||||
{
|
||||
"run_id": "[ULID]",
|
||||
"stream_seq": 26,
|
||||
"stream_seq": 27,
|
||||
"kind": "petri",
|
||||
"id": "execution 0/8/2",
|
||||
"id": "execution 0/9/2",
|
||||
"recorded_at": "[EPOCH_MS]",
|
||||
"item": {
|
||||
"id": {
|
||||
"log": "execution",
|
||||
"execution": 0,
|
||||
"seq": 8,
|
||||
"seq": 9,
|
||||
"index": 2
|
||||
},
|
||||
"origin": "derived",
|
||||
|
|
@ -2002,99 +2038,6 @@ fn attach_json_errors_without_prompting_for_human_input() {
|
|||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"run_id": "[ULID]",
|
||||
"stream_seq": 27,
|
||||
"kind": "petri",
|
||||
"id": "execution 0/9/0",
|
||||
"recorded_at": "[EPOCH_MS]",
|
||||
"item": {
|
||||
"id": {
|
||||
"log": "execution",
|
||||
"execution": 0,
|
||||
"seq": 9,
|
||||
"index": 0
|
||||
},
|
||||
"origin": "core",
|
||||
"context": {
|
||||
"invocation": 0,
|
||||
"execution": 0
|
||||
},
|
||||
"subject": {
|
||||
"node": {
|
||||
"id": 0,
|
||||
"name": "start",
|
||||
"kind": "attractor/stage",
|
||||
"meta": {
|
||||
"label": "Start",
|
||||
"shape": "Mdiamond",
|
||||
"kind": "start",
|
||||
"classes": [],
|
||||
"span": {
|
||||
"line": 3,
|
||||
"column": 3
|
||||
},
|
||||
"admission_hooks": "step",
|
||||
"edges": {
|
||||
"0": {
|
||||
"to": "approve",
|
||||
"label": null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"firing": 1,
|
||||
"visit": 1,
|
||||
"attempt": 1,
|
||||
"generation": 0,
|
||||
"branch": {
|
||||
"role": "none"
|
||||
}
|
||||
},
|
||||
"recorded_at": "[EPOCH_MS]",
|
||||
"record": {
|
||||
"seq": 9,
|
||||
"origin": "core",
|
||||
"recorded_at": "[EPOCH_MS]",
|
||||
"body": {
|
||||
"event": "route.applied",
|
||||
"kind": "edge",
|
||||
"firing": 1,
|
||||
"group": 0,
|
||||
"edge": 0
|
||||
}
|
||||
},
|
||||
"derived": {
|
||||
"target": {
|
||||
"id": 2,
|
||||
"name": "approve",
|
||||
"kind": "attractor/human",
|
||||
"meta": {
|
||||
"label": "Approve?",
|
||||
"shape": "hexagon",
|
||||
"kind": "human",
|
||||
"classes": [],
|
||||
"span": {
|
||||
"line": 5,
|
||||
"column": 3
|
||||
},
|
||||
"edges": {
|
||||
"1": {
|
||||
"to": "ship",
|
||||
"label": "[A] Approve"
|
||||
},
|
||||
"2": {
|
||||
"to": "revise",
|
||||
"label": "[R] Revise"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"transition": "Continue",
|
||||
"back": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"run_id": "[ULID]",
|
||||
"stream_seq": 28,
|
||||
|
|
@ -2150,38 +2093,15 @@ fn attach_json_errors_without_prompting_for_human_input() {
|
|||
"origin": "core",
|
||||
"recorded_at": "[EPOCH_MS]",
|
||||
"body": {
|
||||
"event": "token.emitted",
|
||||
"edge": 0,
|
||||
"generation": 0,
|
||||
"payload": {
|
||||
"outcome": "succeeded",
|
||||
"failure_class": ""
|
||||
},
|
||||
"from": 1
|
||||
"event": "route.applied",
|
||||
"kind": "edge",
|
||||
"firing": 1,
|
||||
"group": 0,
|
||||
"edge": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"run_id": "[ULID]",
|
||||
"stream_seq": 29,
|
||||
"kind": "petri",
|
||||
"id": "execution 0/11/0",
|
||||
"recorded_at": "[EPOCH_MS]",
|
||||
"item": {
|
||||
"id": {
|
||||
"log": "execution",
|
||||
"execution": 0,
|
||||
"seq": 11,
|
||||
"index": 0
|
||||
},
|
||||
"origin": "external",
|
||||
"context": {
|
||||
"invocation": 0,
|
||||
"execution": 0
|
||||
},
|
||||
"subject": {
|
||||
"node": {
|
||||
"derived": {
|
||||
"target": {
|
||||
"id": 2,
|
||||
"name": "approve",
|
||||
"kind": "attractor/human",
|
||||
|
|
@ -2206,7 +2126,53 @@ fn attach_json_errors_without_prompting_for_human_input() {
|
|||
}
|
||||
}
|
||||
},
|
||||
"firing": 2,
|
||||
"transition": "Continue",
|
||||
"back": false
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"run_id": "[ULID]",
|
||||
"stream_seq": 29,
|
||||
"kind": "petri",
|
||||
"id": "execution 0/11/0",
|
||||
"recorded_at": "[EPOCH_MS]",
|
||||
"item": {
|
||||
"id": {
|
||||
"log": "execution",
|
||||
"execution": 0,
|
||||
"seq": 11,
|
||||
"index": 0
|
||||
},
|
||||
"origin": "core",
|
||||
"context": {
|
||||
"invocation": 0,
|
||||
"execution": 0
|
||||
},
|
||||
"subject": {
|
||||
"node": {
|
||||
"id": 0,
|
||||
"name": "start",
|
||||
"kind": "attractor/stage",
|
||||
"meta": {
|
||||
"label": "Start",
|
||||
"shape": "Mdiamond",
|
||||
"kind": "start",
|
||||
"classes": [],
|
||||
"span": {
|
||||
"line": 3,
|
||||
"column": 3
|
||||
},
|
||||
"admission_hooks": "step",
|
||||
"edges": {
|
||||
"0": {
|
||||
"to": "approve",
|
||||
"label": null
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"firing": 1,
|
||||
"visit": 1,
|
||||
"attempt": 1,
|
||||
"generation": 0,
|
||||
|
|
@ -2217,18 +2183,17 @@ fn attach_json_errors_without_prompting_for_human_input() {
|
|||
"recorded_at": "[EPOCH_MS]",
|
||||
"record": {
|
||||
"seq": 11,
|
||||
"origin": "external",
|
||||
"origin": "core",
|
||||
"recorded_at": "[EPOCH_MS]",
|
||||
"body": {
|
||||
"event": "admission.decided",
|
||||
"decision_id": {
|
||||
"attempt_start": {
|
||||
"firing": 2,
|
||||
"attempt": 1
|
||||
}
|
||||
"event": "token.emitted",
|
||||
"edge": 0,
|
||||
"generation": 0,
|
||||
"payload": {
|
||||
"outcome": "succeeded",
|
||||
"failure_class": ""
|
||||
},
|
||||
"decision": "admit",
|
||||
"trace": []
|
||||
"from": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2291,9 +2256,15 @@ fn attach_json_errors_without_prompting_for_human_input() {
|
|||
"origin": "external",
|
||||
"recorded_at": "[EPOCH_MS]",
|
||||
"body": {
|
||||
"event": "step.started",
|
||||
"firing": 2,
|
||||
"attempt": 1
|
||||
"event": "admission.decided",
|
||||
"decision_id": {
|
||||
"attempt_start": {
|
||||
"firing": 2,
|
||||
"attempt": 1
|
||||
}
|
||||
},
|
||||
"decision": "admit",
|
||||
"trace": []
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -2302,65 +2273,6 @@ fn attach_json_errors_without_prompting_for_human_input() {
|
|||
"run_id": "[ULID]",
|
||||
"stream_seq": 31,
|
||||
"kind": "petri",
|
||||
"id": "execution 0/12/1",
|
||||
"recorded_at": "[EPOCH_MS]",
|
||||
"item": {
|
||||
"id": {
|
||||
"log": "execution",
|
||||
"execution": 0,
|
||||
"seq": 12,
|
||||
"index": 1
|
||||
},
|
||||
"origin": "derived",
|
||||
"context": {
|
||||
"invocation": 0,
|
||||
"execution": 0
|
||||
},
|
||||
"subject": {
|
||||
"node": {
|
||||
"id": 2,
|
||||
"name": "approve",
|
||||
"kind": "attractor/human",
|
||||
"meta": {
|
||||
"label": "Approve?",
|
||||
"shape": "hexagon",
|
||||
"kind": "human",
|
||||
"classes": [],
|
||||
"span": {
|
||||
"line": 5,
|
||||
"column": 3
|
||||
},
|
||||
"edges": {
|
||||
"1": {
|
||||
"to": "ship",
|
||||
"label": "[A] Approve"
|
||||
},
|
||||
"2": {
|
||||
"to": "revise",
|
||||
"label": "[R] Revise"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"firing": 2,
|
||||
"visit": 1,
|
||||
"attempt": 1,
|
||||
"generation": 0,
|
||||
"branch": {
|
||||
"role": "none"
|
||||
}
|
||||
},
|
||||
"recorded_at": "[EPOCH_MS]",
|
||||
"derived": {
|
||||
"event": "wait.state.changed",
|
||||
"state": "running"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"run_id": "[ULID]",
|
||||
"stream_seq": 32,
|
||||
"kind": "petri",
|
||||
"id": "execution 0/13/0",
|
||||
"recorded_at": "[EPOCH_MS]",
|
||||
"item": {
|
||||
|
|
@ -2415,58 +2327,16 @@ fn attach_json_errors_without_prompting_for_human_input() {
|
|||
"origin": "external",
|
||||
"recorded_at": "[EPOCH_MS]",
|
||||
"body": {
|
||||
"event": "step.progress.recorded",
|
||||
"event": "step.started",
|
||||
"firing": 2,
|
||||
"ev": {
|
||||
"custom": {
|
||||
"$question": {
|
||||
"id": "approve#2",
|
||||
"text": "Approve?",
|
||||
"options": [
|
||||
{
|
||||
"key": "A",
|
||||
"label": "[A] Approve"
|
||||
},
|
||||
{
|
||||
"key": "R",
|
||||
"label": "[R] Revise"
|
||||
}
|
||||
],
|
||||
"default": "A",
|
||||
"freeform": false,
|
||||
"sensitive": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"derived": {
|
||||
"parsed": {
|
||||
"kind": "question",
|
||||
"question": {
|
||||
"id": "approve#2",
|
||||
"text": "Approve?",
|
||||
"options": [
|
||||
{
|
||||
"key": "A",
|
||||
"label": "[A] Approve"
|
||||
},
|
||||
{
|
||||
"key": "R",
|
||||
"label": "[R] Revise"
|
||||
}
|
||||
],
|
||||
"default": "A",
|
||||
"freeform": false,
|
||||
"sensitive": false
|
||||
}
|
||||
"attempt": 1
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"run_id": "[ULID]",
|
||||
"stream_seq": 33,
|
||||
"stream_seq": 32,
|
||||
"kind": "petri",
|
||||
"id": "execution 0/13/1",
|
||||
"recorded_at": "[EPOCH_MS]",
|
||||
|
|
@ -2519,13 +2389,13 @@ fn attach_json_errors_without_prompting_for_human_input() {
|
|||
"recorded_at": "[EPOCH_MS]",
|
||||
"derived": {
|
||||
"event": "wait.state.changed",
|
||||
"state": "awaiting_answer"
|
||||
"state": "running"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"run_id": "[ULID]",
|
||||
"stream_seq": 34,
|
||||
"stream_seq": 33,
|
||||
"kind": "petri",
|
||||
"id": "execution 0/14/0",
|
||||
"recorded_at": "[EPOCH_MS]",
|
||||
|
|
@ -2580,6 +2450,172 @@ fn attach_json_errors_without_prompting_for_human_input() {
|
|||
"seq": 14,
|
||||
"origin": "external",
|
||||
"recorded_at": "[EPOCH_MS]",
|
||||
"body": {
|
||||
"event": "step.progress.recorded",
|
||||
"firing": 2,
|
||||
"ev": {
|
||||
"custom": {
|
||||
"$question": {
|
||||
"id": "approve#2",
|
||||
"text": "Approve?",
|
||||
"options": [
|
||||
{
|
||||
"key": "A",
|
||||
"label": "[A] Approve"
|
||||
},
|
||||
{
|
||||
"key": "R",
|
||||
"label": "[R] Revise"
|
||||
}
|
||||
],
|
||||
"default": "A",
|
||||
"freeform": false,
|
||||
"sensitive": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"derived": {
|
||||
"parsed": {
|
||||
"kind": "question",
|
||||
"question": {
|
||||
"id": "approve#2",
|
||||
"text": "Approve?",
|
||||
"options": [
|
||||
{
|
||||
"key": "A",
|
||||
"label": "[A] Approve"
|
||||
},
|
||||
{
|
||||
"key": "R",
|
||||
"label": "[R] Revise"
|
||||
}
|
||||
],
|
||||
"default": "A",
|
||||
"freeform": false,
|
||||
"sensitive": false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"run_id": "[ULID]",
|
||||
"stream_seq": 34,
|
||||
"kind": "petri",
|
||||
"id": "execution 0/14/1",
|
||||
"recorded_at": "[EPOCH_MS]",
|
||||
"item": {
|
||||
"id": {
|
||||
"log": "execution",
|
||||
"execution": 0,
|
||||
"seq": 14,
|
||||
"index": 1
|
||||
},
|
||||
"origin": "derived",
|
||||
"context": {
|
||||
"invocation": 0,
|
||||
"execution": 0
|
||||
},
|
||||
"subject": {
|
||||
"node": {
|
||||
"id": 2,
|
||||
"name": "approve",
|
||||
"kind": "attractor/human",
|
||||
"meta": {
|
||||
"label": "Approve?",
|
||||
"shape": "hexagon",
|
||||
"kind": "human",
|
||||
"classes": [],
|
||||
"span": {
|
||||
"line": 5,
|
||||
"column": 3
|
||||
},
|
||||
"edges": {
|
||||
"1": {
|
||||
"to": "ship",
|
||||
"label": "[A] Approve"
|
||||
},
|
||||
"2": {
|
||||
"to": "revise",
|
||||
"label": "[R] Revise"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"firing": 2,
|
||||
"visit": 1,
|
||||
"attempt": 1,
|
||||
"generation": 0,
|
||||
"branch": {
|
||||
"role": "none"
|
||||
}
|
||||
},
|
||||
"recorded_at": "[EPOCH_MS]",
|
||||
"derived": {
|
||||
"event": "wait.state.changed",
|
||||
"state": "awaiting_answer"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"run_id": "[ULID]",
|
||||
"stream_seq": 35,
|
||||
"kind": "petri",
|
||||
"id": "execution 0/15/0",
|
||||
"recorded_at": "[EPOCH_MS]",
|
||||
"item": {
|
||||
"id": {
|
||||
"log": "execution",
|
||||
"execution": 0,
|
||||
"seq": 15,
|
||||
"index": 0
|
||||
},
|
||||
"origin": "external",
|
||||
"context": {
|
||||
"invocation": 0,
|
||||
"execution": 0
|
||||
},
|
||||
"subject": {
|
||||
"node": {
|
||||
"id": 2,
|
||||
"name": "approve",
|
||||
"kind": "attractor/human",
|
||||
"meta": {
|
||||
"label": "Approve?",
|
||||
"shape": "hexagon",
|
||||
"kind": "human",
|
||||
"classes": [],
|
||||
"span": {
|
||||
"line": 5,
|
||||
"column": 3
|
||||
},
|
||||
"edges": {
|
||||
"1": {
|
||||
"to": "ship",
|
||||
"label": "[A] Approve"
|
||||
},
|
||||
"2": {
|
||||
"to": "revise",
|
||||
"label": "[R] Revise"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"firing": 2,
|
||||
"visit": 1,
|
||||
"attempt": 1,
|
||||
"generation": 0,
|
||||
"branch": {
|
||||
"role": "none"
|
||||
}
|
||||
},
|
||||
"recorded_at": "[EPOCH_MS]",
|
||||
"record": {
|
||||
"seq": 15,
|
||||
"origin": "external",
|
||||
"recorded_at": "[EPOCH_MS]",
|
||||
"body": {
|
||||
"event": "step.progress.recorded",
|
||||
"firing": 2,
|
||||
|
|
|
|||
|
|
@ -3,9 +3,14 @@
|
|||
reason = "integration tests stage fixtures with sync std::fs; test infrastructure, not Tokio-hot path"
|
||||
)]
|
||||
|
||||
use fabro_test::{fabro_snapshot, test_context};
|
||||
use std::path::Path;
|
||||
|
||||
use super::support::{read_text, setup_local_sandbox_run, setup_seeded_created_dry_run, text_tree};
|
||||
use fabro_test::{fabro_snapshot, test_context};
|
||||
use fabro_types::RunSandbox;
|
||||
|
||||
use super::support::{
|
||||
read_text, run_state, setup_local_sandbox_run, setup_seeded_created_dry_run, text_tree,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn help() {
|
||||
|
|
@ -113,8 +118,15 @@ fn sandbox_cp_uploads_file_to_run() {
|
|||
----- stdout -----
|
||||
----- stderr -----
|
||||
");
|
||||
// The run executes in its own workspace, not in the target folder: the
|
||||
// upload lands where the run's sandbox works.
|
||||
let working_directory = run_state(&setup.run.run_dir)
|
||||
.sandbox
|
||||
.and_then(RunSandbox::into_instance)
|
||||
.map(|instance| instance.runtime.working_directory)
|
||||
.expect("the run's sandbox instance");
|
||||
assert_eq!(
|
||||
read_text(&setup.workspace_dir.join("sandbox_dir/uploaded.txt")),
|
||||
read_text(&Path::new(&working_directory).join("sandbox_dir/uploaded.txt")),
|
||||
"uploaded-root"
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,9 +58,11 @@ pub(crate) struct ProjectFixture {
|
|||
pub(crate) fabro_root: PathBuf,
|
||||
}
|
||||
|
||||
/// A run whose workflow populated its sandbox. The run executes in its own
|
||||
/// workspace, not in the target folder, so the sandbox's files are read
|
||||
/// back through the run.
|
||||
pub(crate) struct WorkspaceRunSetup {
|
||||
pub(crate) run: RunSetup,
|
||||
pub(crate) workspace_dir: PathBuf,
|
||||
pub(crate) run: RunSetup,
|
||||
}
|
||||
|
||||
pub(crate) struct WorkflowGate {
|
||||
|
|
@ -542,7 +544,7 @@ provider = "local"
|
|||
let run = run_local_workflow(context, &workspace_dir, "workflow.toml");
|
||||
assert!(run_state(&run.run_dir).sandbox.is_some());
|
||||
|
||||
WorkspaceRunSetup { run, workspace_dir }
|
||||
WorkspaceRunSetup { run }
|
||||
}
|
||||
|
||||
fn run_local_workflow(context: &TestContext, workspace_dir: &Path, workflow: &str) -> RunSetup {
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ use std::time::Duration;
|
|||
|
||||
use fabro_api::types::PaginatedRunStreamList;
|
||||
use fabro_petri::petri::EVENT_CONTRACT_VERSION;
|
||||
use fabro_redact::redact_json_value;
|
||||
use fabro_types::RunStreamItem;
|
||||
use tokio::sync::broadcast::error::RecvError;
|
||||
use tokio::time::{self, Instant};
|
||||
|
|
@ -154,6 +155,12 @@ async fn list_run_stream(state: &AppState, id: RunId, after: u64, limit: usize)
|
|||
Ok(mut items) => {
|
||||
let has_more = items.len() > limit;
|
||||
items.truncate(limit);
|
||||
// The same items the attached stream serves, redacted the same
|
||||
// way, so a client that pages the listing after a stream sees
|
||||
// what the stream showed.
|
||||
for item in &mut items {
|
||||
item.item = redact_json_value(std::mem::take(&mut item.item));
|
||||
}
|
||||
Json(PaginatedRunStreamList {
|
||||
data: items,
|
||||
meta: PaginationMeta {
|
||||
|
|
|
|||
|
|
@ -20,7 +20,8 @@
|
|||
|
||||
use std::collections::BTreeMap;
|
||||
use std::env;
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::process::{Command, Stdio};
|
||||
use std::sync::Arc;
|
||||
|
||||
use axum::body::Body;
|
||||
|
|
@ -46,6 +47,8 @@ use crate::helpers::{
|
|||
|
||||
const HOST_PLUGIN: &str = "sandbox-driver-host";
|
||||
const HOST_PLUGIN_OVERRIDE: &str = "PETRI_SANDBOX_HOST_PLUGIN";
|
||||
const DOCKER_PLUGIN: &str = "sandbox-driver-docker";
|
||||
const DOCKER_PLUGIN_OVERRIDE: &str = "PETRI_SANDBOX_DOCKER_PLUGIN";
|
||||
const REQUIRE_ENV: &str = "FABRO_REQUIRE_SANDBOX_PLUGINS";
|
||||
|
||||
const OPENAI_MODEL: &str = "gpt-5.4";
|
||||
|
|
@ -127,6 +130,38 @@ pub(super) fn host_plugin() -> Option<PathBuf> {
|
|||
found
|
||||
}
|
||||
|
||||
/// The Docker plugin as Petri's lookup finds it, with a daemon that
|
||||
/// answers. `None`, after saying so, when the test should skip; a panic
|
||||
/// when the environment forbids a skip and the plugin is missing.
|
||||
fn docker_plugin() -> Option<PathBuf> {
|
||||
let found = env::var_os(DOCKER_PLUGIN_OVERRIDE)
|
||||
.map(PathBuf::from)
|
||||
.or_else(|| {
|
||||
env::split_paths(&env::var_os("PATH")?)
|
||||
.map(|dir| dir.join(DOCKER_PLUGIN))
|
||||
.find(|candidate| candidate.is_file())
|
||||
});
|
||||
let Some(found) = found else {
|
||||
assert!(
|
||||
env::var_os(REQUIRE_ENV).is_none(),
|
||||
"{REQUIRE_ENV} is set, but {DOCKER_PLUGIN} is not on PATH and {DOCKER_PLUGIN_OVERRIDE} is unset"
|
||||
);
|
||||
eprintln!("skipping: {DOCKER_PLUGIN} is not on PATH and {DOCKER_PLUGIN_OVERRIDE} is unset");
|
||||
return None;
|
||||
};
|
||||
let daemon = Command::new("docker")
|
||||
.args(["version", "--format", "{{.Server.Version}}"])
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.status()
|
||||
.is_ok_and(|status| status.success());
|
||||
if !daemon {
|
||||
eprintln!("skipping: no Docker daemon answers");
|
||||
return None;
|
||||
}
|
||||
Some(found)
|
||||
}
|
||||
|
||||
/// Register a version whose entrypoint is `workflow.fabro`, with the given
|
||||
/// files beside it.
|
||||
pub(super) async fn register_version(app: &axum::Router, files: &[(&str, &str)]) -> String {
|
||||
|
|
@ -652,3 +687,177 @@ async fn a_human_gate_is_answered_through_the_questions_api() {
|
|||
);
|
||||
super::petri_stream::capture_settled(&state, &app, &run_id, "gate").await;
|
||||
}
|
||||
|
||||
/// The sandbox a run executed in, as its projection carries it from Petri's
|
||||
/// `scope.acquired`: the run's own scope on the host provider, ready, with
|
||||
/// the directory id a reconnect attaches by and the working directory the
|
||||
/// steps ran in. The summary carries the same instance, so Ask Fabro and
|
||||
/// `sandbox cp` reach the sandbox after the run.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn a_runs_projection_carries_its_host_sandbox_instance() {
|
||||
if host_plugin().is_none() {
|
||||
return;
|
||||
}
|
||||
let workspace = tempfile::tempdir().expect("workspace tempdir");
|
||||
let settings = settings_from_toml("_version = 1\n\n[run.environment]\nid = \"local\"\n");
|
||||
let state = test_app_state_with_options(settings, 5);
|
||||
let app = test_app_with_scheduler(Arc::clone(&state));
|
||||
|
||||
let version_id = register_version(&app, &[
|
||||
("workflow.fabro", COMMAND_DOT),
|
||||
("workflow.toml", PLAIN_SETTINGS),
|
||||
])
|
||||
.await;
|
||||
let run_id =
|
||||
create_and_start_run_from_intent(&app, intent(&version_id, workspace.path())).await;
|
||||
let status = wait_for_run_status(&app, &run_id, &["succeeded", "failed"]).await;
|
||||
assert_eq!(
|
||||
status,
|
||||
"succeeded",
|
||||
"run: {}",
|
||||
run_json(&app, &run_id).await
|
||||
);
|
||||
|
||||
let projection = settled_state(&state, &app, &run_id).await;
|
||||
let sandbox = &projection["sandbox"];
|
||||
assert_eq!(sandbox["kind"], "ready", "{sandbox}");
|
||||
assert_eq!(sandbox["plan"]["provider"], "local", "{sandbox}");
|
||||
let instance = &sandbox["instance"];
|
||||
assert_eq!(instance["provider"], "local", "{instance}");
|
||||
assert!(
|
||||
instance.get("image").is_none(),
|
||||
"a host directory runs no image: {instance}"
|
||||
);
|
||||
let id = instance["runtime"]["id"]
|
||||
.as_str()
|
||||
.expect("the provider's id for the sandbox");
|
||||
assert!(
|
||||
id.starts_with("host-"),
|
||||
"the host provider's id for the workspace directory: {id}"
|
||||
);
|
||||
let working_directory = instance["runtime"]["working_directory"]
|
||||
.as_str()
|
||||
.expect("the working directory");
|
||||
assert!(
|
||||
Path::new(working_directory).is_dir(),
|
||||
"the workspace is retained after the run: {working_directory}"
|
||||
);
|
||||
assert!(sandbox.get("failure").is_none(), "{sandbox}");
|
||||
|
||||
let run = run_json(&app, &run_id).await;
|
||||
assert_eq!(run["sandbox"]["kind"], "ready", "{run}");
|
||||
assert_eq!(run["sandbox"]["instance"]["runtime"]["id"], id, "{run}");
|
||||
}
|
||||
|
||||
/// The same on the Docker provider: the instance is the run's container,
|
||||
/// with the image it runs and the container's workspace, so a reconnect
|
||||
/// attaches to it on the daemon.
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn a_runs_projection_carries_its_docker_sandbox_instance() {
|
||||
if docker_plugin().is_none() {
|
||||
return;
|
||||
}
|
||||
let settings = settings_from_toml("_version = 1\n\n[run.environment]\nid = \"docker\"\n");
|
||||
let state = test_app_state_with_options(settings, 5);
|
||||
let app = test_app_with_scheduler(Arc::clone(&state));
|
||||
// A Docker environment on the daemon's default runner image.
|
||||
let environment = serde_json::json!({
|
||||
"id": "docker",
|
||||
"provider": "docker",
|
||||
"image": { "docker": null, "dockerfile": null },
|
||||
"resources": { "cpu": null, "memory": null, "disk": null },
|
||||
"network": { "mode": "allow_all", "allow": [] },
|
||||
"lifecycle": { "preserve": false, "stop_on_terminal": true, "auto_stop": null },
|
||||
"labels": {},
|
||||
"env": {}
|
||||
});
|
||||
let request = Request::builder()
|
||||
.method("POST")
|
||||
.uri(api("/environments"))
|
||||
.header("content-type", "application/json")
|
||||
.body(Body::from(environment.to_string()))
|
||||
.expect("environment request should build");
|
||||
let response = app
|
||||
.clone()
|
||||
.oneshot(request)
|
||||
.await
|
||||
.expect("environment request routes");
|
||||
response_json(
|
||||
response,
|
||||
StatusCode::CREATED,
|
||||
"POST /api/v1/environments".to_string(),
|
||||
)
|
||||
.await;
|
||||
|
||||
let version_id = register_version(&app, &[
|
||||
("workflow.fabro", COMMAND_DOT),
|
||||
("workflow.toml", PLAIN_SETTINGS),
|
||||
])
|
||||
.await;
|
||||
// A Docker environment takes no folder target: the workspace is the
|
||||
// container's own.
|
||||
let intent = serde_json::json!({
|
||||
"workflow_version_id": version_id,
|
||||
"target": {"kind": "none"},
|
||||
"environment_id": "docker",
|
||||
"args": {},
|
||||
});
|
||||
let run_id = create_and_start_run_from_intent(&app, intent).await;
|
||||
let status = wait_for_run_status(&app, &run_id, &["succeeded", "failed"]).await;
|
||||
assert_eq!(
|
||||
status,
|
||||
"succeeded",
|
||||
"run: {}",
|
||||
run_json(&app, &run_id).await
|
||||
);
|
||||
|
||||
let projection = settled_state(&state, &app, &run_id).await;
|
||||
let sandbox = &projection["sandbox"];
|
||||
assert_eq!(sandbox["kind"], "ready", "{sandbox}");
|
||||
let instance = &sandbox["instance"];
|
||||
assert_eq!(instance["provider"], "docker", "{instance}");
|
||||
assert!(
|
||||
instance["image"]
|
||||
.as_str()
|
||||
.is_some_and(|image| !image.is_empty()),
|
||||
"the image the container runs: {instance}"
|
||||
);
|
||||
let id = instance["runtime"]["id"]
|
||||
.as_str()
|
||||
.expect("the container id");
|
||||
assert!(!id.is_empty(), "{instance}");
|
||||
assert_eq!(
|
||||
instance["runtime"]["working_directory"], "/workspace",
|
||||
"{instance}"
|
||||
);
|
||||
|
||||
// The container is on the daemon, under Petri's run label.
|
||||
let output = Command::new("docker")
|
||||
.args([
|
||||
"ps",
|
||||
"-aq",
|
||||
"--filter",
|
||||
&format!("label=petri.run={run_id}"),
|
||||
])
|
||||
.output()
|
||||
.expect("docker ps runs");
|
||||
let containers: Vec<String> = String::from_utf8_lossy(&output.stdout)
|
||||
.lines()
|
||||
.map(str::trim)
|
||||
.filter(|line| !line.is_empty())
|
||||
.map(str::to_owned)
|
||||
.collect();
|
||||
assert!(
|
||||
containers
|
||||
.iter()
|
||||
.any(|container| id.starts_with(container.as_str())),
|
||||
"the recorded instance is the run's container: {id} in {containers:?}"
|
||||
);
|
||||
for container in &containers {
|
||||
let _ = Command::new("docker")
|
||||
.args(["rm", "-f", container])
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::null())
|
||||
.status();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,7 +111,7 @@ pub(super) async fn list_stream(
|
|||
.expect("has_more is a bool");
|
||||
assert_eq!(
|
||||
page["event_contract_version"].as_u64(),
|
||||
Some(3),
|
||||
Some(u64::from(fabro_petri::petri::EVENT_CONTRACT_VERSION)),
|
||||
"the server reports Petri's contract version: {page}"
|
||||
);
|
||||
let Some(last) = data.last() else {
|
||||
|
|
|
|||
|
|
@ -104,8 +104,11 @@ yet, keep their default value in the projection: `StageProjection.diff` and
|
|||
`Checkpoint`'s engine-derived maps (`completed_nodes`, `node_retries`,
|
||||
`context_values`, `node_outcomes`, `next_node_id`), `agent_tools`,
|
||||
`permission_level`, `script_invocation` and `script_timing`, a stage's
|
||||
`notes`, `StageCompletion` details for a `parsed.note`, the sandbox instance
|
||||
(the matrix's two gaps), `Run.ask_fabro`, an interview option's
|
||||
`notes`, `StageCompletion` details for a `parsed.note`, the sandbox
|
||||
instance's clone fields and workspace roots (Petri's checkout is a copy of
|
||||
the bound repository, not a clone; the roots are the provider's, read live)
|
||||
and the retention outcome (kept as `FoldState.sandbox_retained`; the view
|
||||
has no field for it), `Run.ask_fabro`, an interview option's
|
||||
`description` and `preview`, the pull request `creation` state, and the
|
||||
run's notices, notifications and pairings (recorded, not shown).
|
||||
|
||||
|
|
|
|||
|
|
@ -198,18 +198,25 @@ the CLI setup lines.
|
|||
|
||||
Under the plan Petri acquires every scope through the sandbox-driver plugin,
|
||||
labels it with the run key, and decides retention (`Always` is the Fabro
|
||||
default). Petri records the binding and nothing else durable about the
|
||||
instance: the acquisition progress lines are terminal-only, and the
|
||||
`ScopeReady` hook payload (`scope`, `workspace`) reaches a durable note only
|
||||
when a `sandbox_ready` hook ran.
|
||||
default). Petri records the binding, the instance and the retention outcome:
|
||||
`scope.acquired` (an engine record, once per acquisition, before any attempt
|
||||
in the scope) carries the provider, the provider's id for the sandbox, its
|
||||
image and snapshot when the provider knows them, the working directory, the
|
||||
workspace and lease, and the acquisition time; `scope.failed` the error, its
|
||||
causes and the reserved provider; `scope.released` (a coordinator record,
|
||||
once per lease the invocation owned, before `run.finished`) the outcome
|
||||
retention read, whether the sandbox still exists, and any release problem.
|
||||
The run's sandbox is the root invocation's scope; a child invocation's scope
|
||||
(a parallel branch) is not the run's. Petri names the host provider `host`,
|
||||
which is Fabro's `local`; every other kind is spelled the same.
|
||||
|
||||
| Fabro fact | Fields | Source | Keyed on |
|
||||
| --- | --- | --- | --- |
|
||||
| plan | `RunSandbox.plan {provider, image, snapshot}` | `graph.registered`'s `fabro.environment` and `fabro.launch {sandbox_backend}` params; platform record `run.created` | run |
|
||||
| binding: isolated or inherited | none today | `invocation.declared {sandbox}` | invocation |
|
||||
| which: planned, initializing, ready, failed | `RunSandbox.kind`, `sandbox.initializing`, `sandbox.ready {duration_ms, name, url}`, `sandbox.failed {error, causes, duration_ms}` | gap: proposed Petri record `scope.acquired`, `scope.failed` | scope |
|
||||
| where: instance id, working directory, clone, workspace roots | `RunSandboxInstance.runtime {id, working_directory, repo_cloned, clone_origin_url, clone_branch, workspace_root, repos_root, primary_repo_path, primary_repo_link}`, `sandbox.initialized` | gap: the same `scope.acquired`; the clone from `custom attractor.checkout` | scope |
|
||||
| retention | none today; the run-end `sandbox_cleanup` hook | gap: proposed `scope.released {scope, outcome, retained}`; `run.note.recorded {kind: hook, point: scope_released}` when a hook ran | scope |
|
||||
| which: planned, initializing, ready, failed | `RunSandbox.kind`, `sandbox.initializing`, `sandbox.ready {duration_ms, name, url}`, `sandbox.failed {error, causes, duration_ms}` | `planned` from the platform record `run.created`; `initializing` from `run.started`; `ready` from the root invocation's `scope.acquired` (`provider`, `image`, `snapshot`); `failed` from its `scope.failed` (`provider`, `error`, `causes`, `duration_ms`). The ready duration (`scope.acquired` `duration_ms`) has no field on `RunSandbox` and is not projected | scope |
|
||||
| where: instance id, working directory, clone, workspace roots | `RunSandboxInstance.runtime {id, working_directory, repo_cloned, clone_origin_url, clone_branch, workspace_root, repos_root, primary_repo_path, primary_repo_link}`, `sandbox.initialized` | `scope.acquired` (`instance` is the id a reconnect attaches by, `working_directory`). The clone fields stay unset: `custom attractor.checkout` records Petri's copy of the bound repository into the workspace (`repository`, `commit`, `depth`, `files`), which is not a clone Fabro made, and the workspace roots are the provider's layout, read live | scope |
|
||||
| retention | none today; the run-end `sandbox_cleanup` hook | `scope.released {outcome, retained, problems}` of the root invocation's lease, kept as `FoldState.sandbox_retained`; the view has no field for it and `Run.sandbox` keeps naming the instance that ran. `run.note.recorded {kind: hook, point: scope_released}` when a hook ran | scope |
|
||||
| live status, resources, files, services, VNC, preview, SSH | `SandboxStatus`, `SandboxFileEntry`, `SandboxService`, `VncPreviewResponse`, `PreviewUrlResponse`, `SshAccessResponse`, `ssh.ready` | live: the sandbox-driver provider queried by the run label | run |
|
||||
| setup commands | `setup.started`, `setup.command.completed`, `setup.completed`, `setup.failed`, `cli.ensure.*` | the `run_prepare_N` stages (Stages section); `cli.ensure.*` has no Petri equivalent and is dropped (the image carries the CLI) | stage |
|
||||
|
||||
|
|
@ -417,8 +424,6 @@ record where Fabro does.
|
|||
|
||||
| Fact | Views | Smallest source |
|
||||
| --- | --- | --- |
|
||||
| sandbox instance: provider, instance id, image, snapshot, working directory, workspace roots, duration, failure | `Run.sandbox`, the Sandbox tab, `sandbox.*` CLI lines, `runs inspect`, `ask_fabro` | a Petri engine record `scope.acquired {scope, provider, instance, image, snapshot, workspace, duration_ms}` and `scope.failed {scope, provider, error, causes, duration_ms}`, appended by the driver where it fires `ScopeReady`; today the facts are terminal-only progress lines. Fallback: a platform record `sandbox.ready` written from Fabro's forwarded `ScopeReady` hook, which carries only `scope` and `workspace` |
|
||||
| retention outcome | `sandbox_cleanup`, the sandbox tab after the run | a Petri record `scope.released {scope, outcome, retained}` where the driver fires `ScopeReleased`; today only a `run.note.recorded` exists, and only when a hook ran |
|
||||
| tools available to an agent | `agent_tools`, the insights sidebar's tool list | a `custom attractor.tools {node, firing, attempt, session, tools[] {name, description, source, category}}` from the native backend once per session, where it calls the `HostTools` builders; Pebble's `SessionStarted` carries only the provider and model |
|
||||
| question option `description` and `preview`, `context_display` | the interview dock, the human Q&A renderer | optional fields on Petri's `QuestionOption` (`description`, `preview`) and `Question` (`context`), set by the human gate from the edge attributes Fabro's lowering already reads |
|
||||
| who answered | `interview.completed` `actor`, Slack attribution | platform record `interview.answered {question, principal, channel}` written by Fabro's interviewer beside its `InterviewReply` |
|
||||
|
|
|
|||
|
|
@ -37,17 +37,18 @@ use fabro_types::{
|
|||
FailureCategory, FailureDetail, FailureReason, InterviewOption, InterviewQuestionRecord,
|
||||
ModelRef, ModelUsage, ParallelBranchId, ParallelBranchResult, PendingInterviewRecord,
|
||||
PullRequestCreation, PullRequestCreationStatus, PullRequestLink, RunApproval, RunApprovalState,
|
||||
RunControlAction, RunDiff, RunFailure, RunId, RunProjection, RunSandbox, RunSandboxPlan,
|
||||
RunStatus, RunTiming, SandboxProviderKind, StageCompletion, StageHandler, StageId,
|
||||
StageInferenceProjection, StageModelUsage, StageOutcome, StageProjection, StageState,
|
||||
StageTiming, StartRecord, SuccessReason, first_event_seq, timing, usage_rollup,
|
||||
RunControlAction, RunDiff, RunFailure, RunId, RunProjection, RunSandbox, RunSandboxFailure,
|
||||
RunSandboxInstance, RunSandboxPlan, RunSandboxRuntime, RunStatus, RunTiming,
|
||||
SandboxProviderKind, StageCompletion, StageHandler, StageId, StageInferenceProjection,
|
||||
StageModelUsage, StageOutcome, StageProjection, StageState, StageTiming, StartRecord,
|
||||
SuccessReason, first_event_seq, timing, usage_rollup,
|
||||
};
|
||||
use lithos_llm::catalog::{ModelId, ProviderId};
|
||||
use lithos_llm::types::Usage;
|
||||
use petri_execution::events::{Derived, NodeRef, Parsed, RunEvent, Subject, ViewEvent, WaitState};
|
||||
use petri_execution::{CoordinatorEvent, ExecutionId};
|
||||
use petri_execution::{CoordinatorEvent, ExecutionId, InvocationId};
|
||||
use petri_runtime::engine::{Admission, Event};
|
||||
use petri_runtime::ir::{Metrics, Status, StepEvent};
|
||||
use petri_runtime::ir::{Metrics, SandboxInstance, Status, StepEvent};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::Value;
|
||||
use tracing::debug;
|
||||
|
|
@ -136,6 +137,11 @@ pub struct FoldState {
|
|||
/// behind.
|
||||
#[serde(default)]
|
||||
pub finished_firings: BTreeSet<String>,
|
||||
/// Whether the run's sandbox still exists after its release
|
||||
/// (`scope.released` `retained`): kept stopped, or deleted. Absent until
|
||||
/// the root invocation's lease was released.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub sandbox_retained: Option<bool>,
|
||||
}
|
||||
|
||||
impl FoldState {
|
||||
|
|
@ -338,6 +344,11 @@ impl RunView {
|
|||
run_branch: self.state.run_branch.clone(),
|
||||
base_sha: self.state.base_sha.clone(),
|
||||
});
|
||||
// The scope's sandbox is acquired next; `scope.acquired`
|
||||
// or `scope.failed` settles it.
|
||||
if let Some(sandbox) = projection.sandbox.take() {
|
||||
projection.sandbox = Some(RunSandbox::initializing(sandbox.plan().clone()));
|
||||
}
|
||||
}
|
||||
}
|
||||
CoordinatorEvent::InvocationDeclared { invocation, .. } => {
|
||||
|
|
@ -404,6 +415,19 @@ impl RunView {
|
|||
self.state.finished = Some(status.to_string());
|
||||
self.conclude(status.to_string().as_str(), at);
|
||||
}
|
||||
// ── Sandbox: the retention outcome (VIEWS.md "Sandbox") ─────────
|
||||
// The view has no retention field; the fact is kept in the fold
|
||||
// state for the read side. The instance stays on `Run.sandbox`:
|
||||
// it names what ran, whether or not it still exists.
|
||||
CoordinatorEvent::ScopeReleased {
|
||||
invocation,
|
||||
retained,
|
||||
..
|
||||
} => {
|
||||
if Some(invocation.raw()) == self.state.root {
|
||||
self.state.sandbox_retained = Some(*retained);
|
||||
}
|
||||
}
|
||||
CoordinatorEvent::GraphRegistered { .. }
|
||||
| CoordinatorEvent::ExecutionFinished { .. }
|
||||
| CoordinatorEvent::InvocationCancelRequested { .. }
|
||||
|
|
@ -603,6 +627,41 @@ impl RunView {
|
|||
self.close_questions(answer.question.as_deref(), firing_key.as_deref(), at);
|
||||
}
|
||||
}
|
||||
// ── Sandbox: the instance (VIEWS.md "Sandbox") ──────────────────
|
||||
// The run's sandbox is the root invocation's scope. A child
|
||||
// invocation's scope (a parallel branch) shares or owns another
|
||||
// one and is not the run's; a re-acquisition (a resume, a
|
||||
// replaced sandbox) names the current instance.
|
||||
Event::ScopeAcquired { sandbox, .. } => {
|
||||
if let Some(projection) = self.root_scope_projection(event) {
|
||||
let plan = sandbox_plan_of(projection);
|
||||
projection.sandbox = Some(RunSandbox::ready(
|
||||
plan.clone(),
|
||||
sandbox_instance(&plan, sandbox),
|
||||
));
|
||||
}
|
||||
}
|
||||
Event::ScopeFailed {
|
||||
provider,
|
||||
error,
|
||||
causes,
|
||||
duration_ms,
|
||||
..
|
||||
} => {
|
||||
if let Some(projection) = self.root_scope_projection(event) {
|
||||
let plan = sandbox_plan_of(projection);
|
||||
let provider = provider
|
||||
.as_deref()
|
||||
.and_then(provider_kind)
|
||||
.unwrap_or_else(|| plan.provider.clone());
|
||||
projection.sandbox = Some(RunSandbox::failed(plan, RunSandboxFailure {
|
||||
provider: provider.to_string(),
|
||||
error: error.clone(),
|
||||
causes: causes.clone(),
|
||||
duration_ms: *duration_ms,
|
||||
}));
|
||||
}
|
||||
}
|
||||
Event::ExecutionStarted { .. }
|
||||
| Event::TokenEmitted { .. }
|
||||
| Event::RoutingResolved { .. }
|
||||
|
|
@ -614,6 +673,16 @@ impl RunView {
|
|||
}
|
||||
}
|
||||
|
||||
/// The projection, when `event` is a scope record of the root
|
||||
/// invocation: the run's own sandbox, not a child invocation's.
|
||||
fn root_scope_projection(&mut self, event: &RunEvent) -> Option<&mut RunProjection> {
|
||||
let root = self.state.root?;
|
||||
if event.context.invocation.map(InvocationId::raw) != Some(root) {
|
||||
return None;
|
||||
}
|
||||
self.projection.as_mut()
|
||||
}
|
||||
|
||||
fn fold_progress(
|
||||
&mut self,
|
||||
execution: ExecutionId,
|
||||
|
|
@ -1349,6 +1418,55 @@ fn sandbox_plan(settings: &RunEnvironmentSettings) -> RunSandboxPlan {
|
|||
}
|
||||
}
|
||||
|
||||
/// The plan the projection's sandbox carries, or the one its environment
|
||||
/// settings give when no sandbox was projected yet.
|
||||
fn sandbox_plan_of(projection: &RunProjection) -> RunSandboxPlan {
|
||||
projection.sandbox.as_ref().map_or_else(
|
||||
|| sandbox_plan(&projection.spec.settings.run.environment),
|
||||
|sandbox| sandbox.plan().clone(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Fabro's name for the provider Petri's `scope.acquired` names: Petri's
|
||||
/// `host` is Fabro's `local`; every other kind is spelled the same. `None`
|
||||
/// for a name that is no provider kind.
|
||||
fn provider_kind(provider: &str) -> Option<SandboxProviderKind> {
|
||||
if provider == "host" {
|
||||
return Some(SandboxProviderKind::LOCAL);
|
||||
}
|
||||
SandboxProviderKind::try_new(provider).ok()
|
||||
}
|
||||
|
||||
/// The run's sandbox instance from Petri's record of the scope's
|
||||
/// acquisition: the provider, the provider's id for the sandbox (what a
|
||||
/// reconnect attaches by), its image and snapshot when the provider knows
|
||||
/// them, and the working directory. The clone fields stay unset: Petri's
|
||||
/// checkout copies the bound repository into the workspace and is not a
|
||||
/// clone Fabro made, and the workspace roots are the provider's own layout,
|
||||
/// read live.
|
||||
fn sandbox_instance(plan: &RunSandboxPlan, sandbox: &SandboxInstance) -> RunSandboxInstance {
|
||||
RunSandboxInstance {
|
||||
provider: provider_kind(&sandbox.provider).unwrap_or_else(|| plan.provider.clone()),
|
||||
image: sandbox
|
||||
.image
|
||||
.as_ref()
|
||||
.map(ToString::to_string)
|
||||
.or_else(|| plan.image.clone()),
|
||||
snapshot: sandbox.snapshot.as_ref().map(ToString::to_string),
|
||||
runtime: RunSandboxRuntime {
|
||||
id: sandbox.instance.to_string(),
|
||||
working_directory: sandbox.working_directory.to_string(),
|
||||
repo_cloned: None,
|
||||
clone_origin_url: None,
|
||||
clone_branch: None,
|
||||
workspace_root: None,
|
||||
repos_root: None,
|
||||
primary_repo_path: None,
|
||||
primary_repo_link: None,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn stage_outcome(status: &Status) -> StageOutcome {
|
||||
match status {
|
||||
Status::Success => StageOutcome::Succeeded,
|
||||
|
|
|
|||
|
|
@ -115,12 +115,35 @@ pub async fn attach_provider_sandbox(
|
|||
let provider = connect(&kind, access, run_id.as_ref()).await?;
|
||||
let id = SandboxId::try_new(sandbox_id)
|
||||
.map_err(|error| crate::Error::context(format!("Invalid {kind} sandbox id"), error))?;
|
||||
let handle = provider.attach(&id, events).await.map_err(|error| {
|
||||
crate::Error::context(
|
||||
format!("Failed to reconnect {kind} sandbox '{sandbox_id}'"),
|
||||
error,
|
||||
)
|
||||
})?;
|
||||
let handle = match provider.attach(&id, events.clone()).await {
|
||||
Ok(handle) => handle,
|
||||
// A host sandbox is the directory it designates. An id the host
|
||||
// provider minted for a long path lives only in the registry of the
|
||||
// process that created it (a run's Petri worker, say), so a
|
||||
// reconnect from another process designates the directory again:
|
||||
// the same workspace, whatever the id.
|
||||
Err(error)
|
||||
if kind.bundled() == Some(BundledProvider::Local)
|
||||
&& matches!(error, sandbox_driver::Error::NotFound { .. }) =>
|
||||
{
|
||||
let spec = DriverSpec::new(SandboxSource::HostDirectory)
|
||||
.working_directory(working_directory.clone());
|
||||
provider.create(&spec, events).await.map_err(|error| {
|
||||
crate::Error::context(
|
||||
format!(
|
||||
"Failed to reconnect {kind} sandbox '{sandbox_id}' at {working_directory}"
|
||||
),
|
||||
error,
|
||||
)
|
||||
})?
|
||||
}
|
||||
Err(error) => {
|
||||
return Err(crate::Error::context(
|
||||
format!("Failed to reconnect {kind} sandbox '{sandbox_id}'"),
|
||||
error,
|
||||
));
|
||||
}
|
||||
};
|
||||
let status = handle.describe().await?;
|
||||
let workspace = RepoWorkspace::attached(
|
||||
layout_source(&kind),
|
||||
|
|
|
|||
|
|
@ -86,6 +86,9 @@ static INSTA_FILTERS: &[(&str, &str)] = &[
|
|||
),
|
||||
(r#""nanos"(\s*:\s*)\d+"#, r#""nanos"$1"[NANOS]""#),
|
||||
(r"host-dir-[0-9a-f]+", "host-dir-[HEX]"),
|
||||
// A local sandbox's registry-minted id (a creation time, a process id
|
||||
// and a counter), for a directory too long for a path-derived id.
|
||||
(r"host-g[0-9a-f]+-\d+-\d+", "host-g[ID]"),
|
||||
(r"\\([\w\d])", "/$1"),
|
||||
];
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue