ferp 0.7.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ferp/__init__.py +3 -0
- ferp/__main__.py +4 -0
- ferp/__version__.py +1 -0
- ferp/app.py +9 -0
- ferp/cli.py +160 -0
- ferp/core/__init__.py +0 -0
- ferp/core/app.py +1312 -0
- ferp/core/bundle_installer.py +245 -0
- ferp/core/command_provider.py +77 -0
- ferp/core/dependency_manager.py +59 -0
- ferp/core/fs_controller.py +70 -0
- ferp/core/fs_watcher.py +144 -0
- ferp/core/messages.py +49 -0
- ferp/core/path_actions.py +124 -0
- ferp/core/paths.py +3 -0
- ferp/core/protocols.py +8 -0
- ferp/core/script_controller.py +515 -0
- ferp/core/script_protocol.py +35 -0
- ferp/core/script_runner.py +421 -0
- ferp/core/settings.py +16 -0
- ferp/core/settings_store.py +69 -0
- ferp/core/state.py +156 -0
- ferp/core/task_store.py +164 -0
- ferp/core/transcript_logger.py +95 -0
- ferp/domain/__init__.py +0 -0
- ferp/domain/scripts.py +29 -0
- ferp/fscp/host/__init__.py +11 -0
- ferp/fscp/host/host.py +439 -0
- ferp/fscp/host/managed_process.py +113 -0
- ferp/fscp/host/process_registry.py +124 -0
- ferp/fscp/protocol/__init__.py +13 -0
- ferp/fscp/protocol/errors.py +2 -0
- ferp/fscp/protocol/messages.py +55 -0
- ferp/fscp/protocol/schemas/__init__.py +0 -0
- ferp/fscp/protocol/schemas/fscp/1.0/cancel.json +16 -0
- ferp/fscp/protocol/schemas/fscp/1.0/definitions.json +29 -0
- ferp/fscp/protocol/schemas/fscp/1.0/discriminator.json +14 -0
- ferp/fscp/protocol/schemas/fscp/1.0/envelope.json +13 -0
- ferp/fscp/protocol/schemas/fscp/1.0/exit.json +20 -0
- ferp/fscp/protocol/schemas/fscp/1.0/init.json +36 -0
- ferp/fscp/protocol/schemas/fscp/1.0/input_response.json +21 -0
- ferp/fscp/protocol/schemas/fscp/1.0/log.json +21 -0
- ferp/fscp/protocol/schemas/fscp/1.0/message.json +23 -0
- ferp/fscp/protocol/schemas/fscp/1.0/progress.json +23 -0
- ferp/fscp/protocol/schemas/fscp/1.0/request_input.json +47 -0
- ferp/fscp/protocol/schemas/fscp/1.0/result.json +16 -0
- ferp/fscp/protocol/schemas/fscp/__init__.py +0 -0
- ferp/fscp/protocol/state.py +16 -0
- ferp/fscp/protocol/validator.py +123 -0
- ferp/fscp/scripts/__init__.py +0 -0
- ferp/fscp/scripts/runtime/__init__.py +4 -0
- ferp/fscp/scripts/runtime/__main__.py +40 -0
- ferp/fscp/scripts/runtime/errors.py +14 -0
- ferp/fscp/scripts/runtime/io.py +64 -0
- ferp/fscp/scripts/runtime/script.py +149 -0
- ferp/fscp/scripts/runtime/state.py +17 -0
- ferp/fscp/scripts/runtime/worker.py +13 -0
- ferp/fscp/scripts/sdk.py +548 -0
- ferp/fscp/transcript/__init__.py +3 -0
- ferp/fscp/transcript/events.py +14 -0
- ferp/resources/__init__.py +0 -0
- ferp/services/__init__.py +3 -0
- ferp/services/file_listing.py +120 -0
- ferp/services/monday_sync.py +155 -0
- ferp/services/releases.py +214 -0
- ferp/services/scripts.py +90 -0
- ferp/services/update_check.py +130 -0
- ferp/styles/index.tcss +638 -0
- ferp/themes/themes.py +238 -0
- ferp/widgets/__init__.py +17 -0
- ferp/widgets/dialogs.py +167 -0
- ferp/widgets/file_tree.py +991 -0
- ferp/widgets/forms.py +146 -0
- ferp/widgets/output_panel.py +244 -0
- ferp/widgets/panels.py +13 -0
- ferp/widgets/process_list.py +158 -0
- ferp/widgets/readme_modal.py +59 -0
- ferp/widgets/scripts.py +192 -0
- ferp/widgets/task_capture.py +74 -0
- ferp/widgets/task_list.py +493 -0
- ferp/widgets/top_bar.py +110 -0
- ferp-0.7.1.dist-info/METADATA +128 -0
- ferp-0.7.1.dist-info/RECORD +87 -0
- ferp-0.7.1.dist-info/WHEEL +5 -0
- ferp-0.7.1.dist-info/entry_points.txt +2 -0
- ferp-0.7.1.dist-info/licenses/LICENSE +21 -0
- ferp-0.7.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
from dataclasses import dataclass
|
|
2
|
+
from enum import Enum
|
|
3
|
+
from typing import Any, Mapping
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class MessageDirection(Enum):
|
|
7
|
+
SEND = "send"
|
|
8
|
+
RECV = "recv"
|
|
9
|
+
INTERNAL = "interal"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class MessageType(Enum):
|
|
13
|
+
# Host -> Script
|
|
14
|
+
INIT = "init"
|
|
15
|
+
INPUT_RESPONSE = "input_response"
|
|
16
|
+
CANCEL = "cancel"
|
|
17
|
+
|
|
18
|
+
# Script -> Host
|
|
19
|
+
LOG = "log"
|
|
20
|
+
PROGRESS = "progress"
|
|
21
|
+
REQUEST_INPUT = "request_input"
|
|
22
|
+
RESULT = "result"
|
|
23
|
+
EXIT = "exit"
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
PROTOCOL = "ferp/1.0"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@dataclass(frozen=True, slots=True)
|
|
30
|
+
class Message:
|
|
31
|
+
type: MessageType
|
|
32
|
+
payload: Mapping[str, Any]
|
|
33
|
+
|
|
34
|
+
def to_dict(self) -> dict[str, Any]:
|
|
35
|
+
return {
|
|
36
|
+
"protocol": PROTOCOL,
|
|
37
|
+
"type": self.type.value,
|
|
38
|
+
"payload": dict(self.payload),
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
@staticmethod
|
|
42
|
+
def from_dict(data: dict[str, Any]) -> "Message":
|
|
43
|
+
if data.get("protocol") != PROTOCOL:
|
|
44
|
+
raise ValueError(f"Unsupported protocol: {data.get('protocol')}")
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
msg_type = MessageType(data["type"])
|
|
48
|
+
except ValueError as exc:
|
|
49
|
+
raise ValueError(f"Unknown message type: {data.get('type')}") from exc
|
|
50
|
+
|
|
51
|
+
payload = data.get("payload")
|
|
52
|
+
if not isinstance(payload, dict):
|
|
53
|
+
raise ValueError("payload must be an object")
|
|
54
|
+
|
|
55
|
+
return Message(type=msg_type, payload=payload)
|
|
File without changes
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$id": "https://ferp.dev/schema/fscp/1.0/cancel.json",
|
|
3
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
|
+
"allOf": [
|
|
5
|
+
{ "$ref": "envelope.json" },
|
|
6
|
+
{
|
|
7
|
+
"properties": {
|
|
8
|
+
"type": { "const": "cancel" },
|
|
9
|
+
"payload": {
|
|
10
|
+
"type": "object",
|
|
11
|
+
"additionalProperties": false
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$id": "https://ferp.dev/schema/fscp/1.0/definitions.json",
|
|
3
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
|
+
"definitions": {
|
|
5
|
+
"protocol": {
|
|
6
|
+
"type": "string",
|
|
7
|
+
"const": "ferp/1.0"
|
|
8
|
+
},
|
|
9
|
+
|
|
10
|
+
"messageType": {
|
|
11
|
+
"type": "string",
|
|
12
|
+
"enum": [
|
|
13
|
+
"init",
|
|
14
|
+
"input_response",
|
|
15
|
+
"cancel",
|
|
16
|
+
"log",
|
|
17
|
+
"progress",
|
|
18
|
+
"request_input",
|
|
19
|
+
"result",
|
|
20
|
+
"exit"
|
|
21
|
+
]
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
"logLevel": {
|
|
25
|
+
"type": "string",
|
|
26
|
+
"enum": ["info", "warn", "error", "debug"]
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$id": "https://ferp.dev/schema/fscp/1.0/message.json",
|
|
3
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
|
+
"oneOf": [
|
|
5
|
+
{ "$ref": "init.json" },
|
|
6
|
+
{ "$ref": "input_response.json" },
|
|
7
|
+
{ "$ref": "cancel.json" },
|
|
8
|
+
{ "$ref": "log.json" },
|
|
9
|
+
{ "$ref": "progress.json" },
|
|
10
|
+
{ "$ref": "request_input.json" },
|
|
11
|
+
{ "$ref": "result.json" },
|
|
12
|
+
{ "$ref": "exit.json" }
|
|
13
|
+
]
|
|
14
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$id": "https://ferp.dev/schema/fscp/1.0/envelope.json",
|
|
3
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
|
+
|
|
5
|
+
"type": "object",
|
|
6
|
+
"required": ["protocol", "type", "payload"],
|
|
7
|
+
"properties": {
|
|
8
|
+
"protocol": { "$ref": "definitions.json#/definitions/protocol" },
|
|
9
|
+
"type": { "$ref": "definitions.json#/definitions/messageType" },
|
|
10
|
+
"payload": { "type": "object" }
|
|
11
|
+
},
|
|
12
|
+
"additionalProperties": false
|
|
13
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$id": "https://ferp.dev/schema/fscp/1.0/exit.json",
|
|
3
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
|
+
"allOf": [
|
|
5
|
+
{ "$ref": "envelope.json" },
|
|
6
|
+
{
|
|
7
|
+
"properties": {
|
|
8
|
+
"type": { "const": "exit" },
|
|
9
|
+
"payload": {
|
|
10
|
+
"type": "object",
|
|
11
|
+
"required": ["code"],
|
|
12
|
+
"properties": {
|
|
13
|
+
"code": { "type": "integer" }
|
|
14
|
+
},
|
|
15
|
+
"additionalProperties": false
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
]
|
|
20
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$id": "https://ferp.dev/schema/fscp/1.0/init.json",
|
|
3
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
|
+
"allOf": [
|
|
5
|
+
{ "$ref": "envelope.json" },
|
|
6
|
+
{
|
|
7
|
+
"properties": {
|
|
8
|
+
"type": { "const": "init" },
|
|
9
|
+
"payload": {
|
|
10
|
+
"type": "object",
|
|
11
|
+
"required": ["target"],
|
|
12
|
+
"properties": {
|
|
13
|
+
"target": {
|
|
14
|
+
"type": "object",
|
|
15
|
+
"required": ["path", "kind"],
|
|
16
|
+
"properties": {
|
|
17
|
+
"path": { "type": "string" },
|
|
18
|
+
"kind": { "enum": ["file", "directory"] }
|
|
19
|
+
},
|
|
20
|
+
"additionalProperties": false
|
|
21
|
+
},
|
|
22
|
+
"params": {
|
|
23
|
+
"type": "object",
|
|
24
|
+
"additionalProperties": true
|
|
25
|
+
},
|
|
26
|
+
"environment": {
|
|
27
|
+
"type": "object",
|
|
28
|
+
"additionalProperties": true
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
"additionalProperties": false
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$id": "https://ferp.dev/schema/fscp/1.0/input_response.json",
|
|
3
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
|
+
"allOf": [
|
|
5
|
+
{ "$ref": "envelope.json" },
|
|
6
|
+
{
|
|
7
|
+
"properties": {
|
|
8
|
+
"type": { "const": "input_response" },
|
|
9
|
+
"payload": {
|
|
10
|
+
"type": "object",
|
|
11
|
+
"required": ["id", "value"],
|
|
12
|
+
"properties": {
|
|
13
|
+
"id": { "type": "string" },
|
|
14
|
+
"value": { "type": "string" }
|
|
15
|
+
},
|
|
16
|
+
"additionalProperties": false
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$id": "https://ferp.dev/schema/fscp/1.0/log.json",
|
|
3
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
|
+
"allOf": [
|
|
5
|
+
{ "$ref": "envelope.json" },
|
|
6
|
+
{
|
|
7
|
+
"properties": {
|
|
8
|
+
"type": { "const": "log" },
|
|
9
|
+
"payload": {
|
|
10
|
+
"type": "object",
|
|
11
|
+
"required": ["level", "message"],
|
|
12
|
+
"properties": {
|
|
13
|
+
"level": { "$ref": "definitions.json#/definitions/logLevel" },
|
|
14
|
+
"message": { "type": "string" }
|
|
15
|
+
},
|
|
16
|
+
"additionalProperties": false
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
]
|
|
21
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$id": "https://ferp.dev/schema/fscp/1.0/message.json",
|
|
3
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
|
+
"type": "object",
|
|
5
|
+
"required": ["protocol", "type", "payload"],
|
|
6
|
+
"properties": {
|
|
7
|
+
"protocol": { "const": "ferp/1.0" },
|
|
8
|
+
"type": {
|
|
9
|
+
"enum": [
|
|
10
|
+
"init",
|
|
11
|
+
"input_response",
|
|
12
|
+
"cancel",
|
|
13
|
+
"log",
|
|
14
|
+
"progress",
|
|
15
|
+
"request_input",
|
|
16
|
+
"result",
|
|
17
|
+
"exit"
|
|
18
|
+
]
|
|
19
|
+
},
|
|
20
|
+
"payload": { "type": "object" }
|
|
21
|
+
},
|
|
22
|
+
"additionalProperties": false
|
|
23
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$id": "https://ferp.dev/schema/fscp/1.0/progress.json",
|
|
3
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
|
+
"allOf": [
|
|
5
|
+
{ "$ref": "envelope.json" },
|
|
6
|
+
{
|
|
7
|
+
"properties": {
|
|
8
|
+
"type": { "const": "progress" },
|
|
9
|
+
"payload": {
|
|
10
|
+
"type": "object",
|
|
11
|
+
"required": ["current"],
|
|
12
|
+
"properties": {
|
|
13
|
+
"current": { "type": "number", "minimum": 0 },
|
|
14
|
+
"total": { "type": "number", "minimum": 0 },
|
|
15
|
+
"unit": { "type": "string" },
|
|
16
|
+
"message": { "type": "string" }
|
|
17
|
+
},
|
|
18
|
+
"additionalProperties": false
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
]
|
|
23
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$id": "https://ferp.dev/schema/fscp/1.0/request_input.json",
|
|
3
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
|
+
"allOf": [
|
|
5
|
+
{ "$ref": "envelope.json" },
|
|
6
|
+
{
|
|
7
|
+
"properties": {
|
|
8
|
+
"type": { "const": "request_input" },
|
|
9
|
+
"payload": {
|
|
10
|
+
"type": "object",
|
|
11
|
+
"required": ["id", "prompt"],
|
|
12
|
+
"properties": {
|
|
13
|
+
"id": { "type": "string" },
|
|
14
|
+
"prompt": { "type": "string" },
|
|
15
|
+
"default": { "type": "string" },
|
|
16
|
+
"secret": { "type": "boolean" },
|
|
17
|
+
"mode": { "enum": ["input", "confirm"] },
|
|
18
|
+
"show_text_input": { "type": "boolean" },
|
|
19
|
+
"suggestions": {
|
|
20
|
+
"type": "array",
|
|
21
|
+
"items": { "type": "string" }
|
|
22
|
+
},
|
|
23
|
+
"fields": {
|
|
24
|
+
"type": "array",
|
|
25
|
+
"items": {
|
|
26
|
+
"type": "object",
|
|
27
|
+
"required": ["id", "type", "label"],
|
|
28
|
+
"properties": {
|
|
29
|
+
"id": { "type": "string" },
|
|
30
|
+
"type": { "enum": ["bool", "multi_select"] },
|
|
31
|
+
"label": { "type": "string" },
|
|
32
|
+
"default": { "type": ["boolean", "array"] },
|
|
33
|
+
"options": {
|
|
34
|
+
"type": "array",
|
|
35
|
+
"items": { "type": "string" }
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"additionalProperties": false
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"additionalProperties": false
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
]
|
|
47
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$id": "https://ferp.dev/schema/fscp/1.0/result.json",
|
|
3
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
4
|
+
"allOf": [
|
|
5
|
+
{ "$ref": "envelope.json" },
|
|
6
|
+
{
|
|
7
|
+
"properties": {
|
|
8
|
+
"type": { "const": "result" },
|
|
9
|
+
"payload": {
|
|
10
|
+
"type": "object",
|
|
11
|
+
"additionalProperties": true
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
]
|
|
16
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from enum import Enum, auto
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class HostState(Enum):
|
|
7
|
+
CREATED = auto() # before subprocess spawn
|
|
8
|
+
PROCESS_STARTED = auto() # pipes attached, init not sent
|
|
9
|
+
INIT_SENT = auto() # init sent, no stdout yet
|
|
10
|
+
RUNNING = auto() # normal streaming
|
|
11
|
+
AWAITING_INPUT = auto() # request_input outstanding
|
|
12
|
+
CANCELLING = auto() # cancel sent
|
|
13
|
+
EXIT_RECEIVED = auto() # exit seen, process may still run
|
|
14
|
+
TERMINATED = auto() # process ended
|
|
15
|
+
ERR_PROTOCOL = auto()
|
|
16
|
+
ERR_TRANSPORT = auto()
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import json
|
|
2
|
+
from enum import Enum, auto
|
|
3
|
+
from importlib.resources import files
|
|
4
|
+
from typing import Dict, Set
|
|
5
|
+
|
|
6
|
+
from jsonschema import Draft202012Validator
|
|
7
|
+
from referencing import Registry, Resource
|
|
8
|
+
|
|
9
|
+
from ferp.fscp.protocol.messages import Message, MessageType
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class ProtocolError(RuntimeError):
|
|
13
|
+
"""Raised on FSCP protocol violations."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Endpoint(Enum):
|
|
17
|
+
HOST = auto()
|
|
18
|
+
SCRIPT = auto()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ProtocolValidator:
|
|
22
|
+
"""
|
|
23
|
+
Enforces FSCP message directionality and schema validity.
|
|
24
|
+
"""
|
|
25
|
+
|
|
26
|
+
HOST_TO_SCRIPT: Set[MessageType] = {
|
|
27
|
+
MessageType.INIT,
|
|
28
|
+
MessageType.INPUT_RESPONSE,
|
|
29
|
+
MessageType.CANCEL,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
SCRIPT_TO_HOST: Set[MessageType] = {
|
|
33
|
+
MessageType.LOG,
|
|
34
|
+
MessageType.PROGRESS,
|
|
35
|
+
MessageType.REQUEST_INPUT,
|
|
36
|
+
MessageType.RESULT,
|
|
37
|
+
MessageType.EXIT,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
ALL_MESSAGES: Set[MessageType] = HOST_TO_SCRIPT | SCRIPT_TO_HOST
|
|
41
|
+
|
|
42
|
+
def __init__(self) -> None:
|
|
43
|
+
self._registry = self._load_all_schemas()
|
|
44
|
+
self._validators = self._load_validators()
|
|
45
|
+
|
|
46
|
+
# ----------------------------
|
|
47
|
+
# Schema loading
|
|
48
|
+
# ----------------------------
|
|
49
|
+
|
|
50
|
+
def _load_all_schemas(self) -> Registry:
|
|
51
|
+
base = files("ferp.fscp.protocol.schemas") / "fscp" / "1.0"
|
|
52
|
+
registry = Registry()
|
|
53
|
+
|
|
54
|
+
for entry in base.iterdir():
|
|
55
|
+
if not entry.name.endswith(".json"):
|
|
56
|
+
continue
|
|
57
|
+
|
|
58
|
+
schema = json.loads(entry.read_text())
|
|
59
|
+
registry = registry.with_resource(
|
|
60
|
+
schema["$id"],
|
|
61
|
+
Resource.from_contents(schema),
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
return registry
|
|
65
|
+
|
|
66
|
+
def _load_validators(self) -> Dict[MessageType, Draft202012Validator]:
|
|
67
|
+
validators: Dict[MessageType, Draft202012Validator] = {}
|
|
68
|
+
|
|
69
|
+
for name in [
|
|
70
|
+
"init",
|
|
71
|
+
"input_response",
|
|
72
|
+
"cancel",
|
|
73
|
+
"log",
|
|
74
|
+
"progress",
|
|
75
|
+
"request_input",
|
|
76
|
+
"result",
|
|
77
|
+
"exit",
|
|
78
|
+
]:
|
|
79
|
+
schema = json.loads(
|
|
80
|
+
(
|
|
81
|
+
files("ferp.fscp.protocol.schemas")
|
|
82
|
+
/ "fscp"
|
|
83
|
+
/ "1.0"
|
|
84
|
+
/ f"{name}.json"
|
|
85
|
+
).read_text()
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
validators[MessageType[name.upper()]] = Draft202012Validator(
|
|
89
|
+
schema=schema,
|
|
90
|
+
registry=self._registry,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
return validators
|
|
94
|
+
|
|
95
|
+
# ----------------------------
|
|
96
|
+
# Validation
|
|
97
|
+
# ----------------------------
|
|
98
|
+
|
|
99
|
+
def validate(self, msg: Message, *, sender: Endpoint) -> None:
|
|
100
|
+
# Directionality
|
|
101
|
+
if sender is Endpoint.HOST:
|
|
102
|
+
if msg.type not in self.HOST_TO_SCRIPT:
|
|
103
|
+
raise ProtocolError(f"Host is not allowed to send '{msg.type.value}'")
|
|
104
|
+
|
|
105
|
+
elif sender is Endpoint.SCRIPT:
|
|
106
|
+
if msg.type not in self.SCRIPT_TO_HOST:
|
|
107
|
+
raise ProtocolError(f"Script is not allowed to send '{msg.type.value}'")
|
|
108
|
+
|
|
109
|
+
else:
|
|
110
|
+
raise ProtocolError("Unknown sender endpoint")
|
|
111
|
+
|
|
112
|
+
# Schema validation
|
|
113
|
+
try:
|
|
114
|
+
validator = self._validators[msg.type]
|
|
115
|
+
except KeyError:
|
|
116
|
+
raise ProtocolError(f"No schema registered for '{msg.type.value}'")
|
|
117
|
+
|
|
118
|
+
instance = msg.to_dict()
|
|
119
|
+
|
|
120
|
+
errors = sorted(validator.iter_errors(instance), key=str)
|
|
121
|
+
if errors:
|
|
122
|
+
err = errors[0]
|
|
123
|
+
raise ProtocolError(f"Invalid {msg.type.value} message: {err.message}")
|
|
File without changes
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
FSCP reference script executable.
|
|
4
|
+
|
|
5
|
+
This module is intentionally thin:
|
|
6
|
+
- No protocol logic
|
|
7
|
+
- No business logic
|
|
8
|
+
- No error recovery
|
|
9
|
+
|
|
10
|
+
It exists only to bootstrap ScriptRuntime and exit cleanly.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import sys
|
|
14
|
+
import traceback
|
|
15
|
+
|
|
16
|
+
from ferp.fscp.scripts.runtime.errors import ScriptError
|
|
17
|
+
from ferp.fscp.scripts.runtime.script import ScriptRuntime
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def main() -> int:
|
|
21
|
+
runtime = ScriptRuntime()
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
runtime.run()
|
|
25
|
+
return 0
|
|
26
|
+
|
|
27
|
+
except ScriptError as exc:
|
|
28
|
+
# ScriptError means we violated the FSCP contract or hit a fatal state.
|
|
29
|
+
# Best effort: emit to stderr only (never stdout).
|
|
30
|
+
print(f"[FSCP SCRIPT ERROR] {exc}", file=sys.stderr)
|
|
31
|
+
return 2
|
|
32
|
+
|
|
33
|
+
except Exception:
|
|
34
|
+
# Truly unexpected failure.
|
|
35
|
+
traceback.print_exc(file=sys.stderr)
|
|
36
|
+
return 3
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
if __name__ == "__main__":
|
|
40
|
+
sys.exit(main())
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
class ScriptError(Exception):
|
|
2
|
+
"""Base class for all script runtime errors."""
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class ProtocolViolation(ScriptError):
|
|
6
|
+
"""Host sent an invalid or illegal protocol message."""
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class InvalidStateTransition(ScriptError):
|
|
10
|
+
"""Message not allowed in current script state."""
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class FatalScriptError(ScriptError):
|
|
14
|
+
"""Unhandled internal error."""
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import sys
|
|
3
|
+
from multiprocessing.connection import Connection
|
|
4
|
+
from typing import Any, Dict, Optional
|
|
5
|
+
|
|
6
|
+
_connection: Optional[Connection] = None
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def configure_connection(conn: Connection) -> None:
|
|
10
|
+
"""Attach a multiprocessing pipe connection for script IO."""
|
|
11
|
+
global _connection
|
|
12
|
+
_connection = conn
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def read_message() -> Dict[str, Any]:
|
|
16
|
+
"""Read a single FSCP message from the configured transport."""
|
|
17
|
+
if _connection is not None:
|
|
18
|
+
try:
|
|
19
|
+
payload = _connection.recv()
|
|
20
|
+
except EOFError as exc:
|
|
21
|
+
raise EOFError("Host closed pipe") from exc
|
|
22
|
+
|
|
23
|
+
if not isinstance(payload, dict):
|
|
24
|
+
raise ValueError("Invalid payload received from host")
|
|
25
|
+
|
|
26
|
+
return payload
|
|
27
|
+
|
|
28
|
+
line = sys.stdin.readline()
|
|
29
|
+
if not line:
|
|
30
|
+
raise EOFError("Host closed stdin")
|
|
31
|
+
|
|
32
|
+
try:
|
|
33
|
+
return json.loads(line)
|
|
34
|
+
except json.JSONDecodeError as exc:
|
|
35
|
+
raise ValueError(f"Invalid JSON from host: {exc}") from exc
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def try_read_message() -> Dict[str, Any] | None:
|
|
39
|
+
"""Attempt to read a single message without blocking."""
|
|
40
|
+
if _connection is None:
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
if not _connection.poll(0):
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
try:
|
|
47
|
+
payload = _connection.recv()
|
|
48
|
+
except EOFError as exc:
|
|
49
|
+
raise EOFError("Host closed pipe") from exc
|
|
50
|
+
|
|
51
|
+
if not isinstance(payload, dict):
|
|
52
|
+
raise ValueError("Invalid payload received from host")
|
|
53
|
+
|
|
54
|
+
return payload
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def write_message(msg: Dict[str, Any]) -> None:
|
|
58
|
+
"""Write a single FSCP message to the configured transport."""
|
|
59
|
+
if _connection is not None:
|
|
60
|
+
_connection.send(msg)
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
sys.stdout.write(json.dumps(msg) + "\n")
|
|
64
|
+
sys.stdout.flush()
|