@passioncode-ai/passioncode 0.2.37 → 0.2.39
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.
- package/CHANGELOG.md +16 -0
- package/family.json +1 -1
- package/package.json +1 -1
- package/payload/.claude-plugin/marketplace.json +2 -2
- package/payload/manifest.json +8 -8
- package/payload/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +29 -11
- package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/dashboard-links.md +53 -3
- package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/operator-channel-and-repository.md +11 -3
- package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/provider-entry.md +3 -1
- package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +2 -2
- package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py +4 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +50 -14
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-button.js +38 -12
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.css +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/fabric-dashboard.js +2 -2
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/assets/dashboard-kit/kit.json +14 -4
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-kit.md +9 -2
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard-links.md +53 -3
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/dashboard.md +13 -4
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +10 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +99 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +1 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +5 -2
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/remote-placement.md +6 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/settings-backups.md +2 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/surfaces-and-auth.md +5 -3
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service.py +47 -3
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/check_service_rules.py +325 -2
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/dashboard_kit.py +2 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-actions.mjs +246 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-auth.mjs +4 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-backups.mjs +3 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +22 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +45 -3
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop-rules.mjs +45 -3
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop.mjs +2 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-protocols.mjs +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +1 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_actions.py +302 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_auth.py +7 -2
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_backups.py +2 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +22 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +58 -5
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +154 -8
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop_rules.py +45 -2
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_protocols.py +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +9 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +202 -18
- package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +29 -11
- package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/dashboard-links.md +53 -3
- package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/operator-channel-and-repository.md +11 -3
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +41 -9
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +2 -0
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +1 -1
- package/payload/plugins/passioncode/.claude-plugin/plugin.json +1 -1
|
@@ -50,7 +50,7 @@ REFUSAL_CODES = (
|
|
|
50
50
|
"unauthenticated", "token-expired", "wrong-audience", "scope-not-granted", "effect-above-ceiling", "no-grant",
|
|
51
51
|
"grant-expired", "grant-revoked", "limit-reached", "approval-required", "approval-invalid", "approval-reused",
|
|
52
52
|
"principal-unproven", "credential-source-refused", "chain-too-deep", "profile-forbids", "issuer-unavailable",
|
|
53
|
-
"unknown-state",
|
|
53
|
+
"unknown-state", "operator-decision-required", "decision-invalid", "human-step-required",
|
|
54
54
|
)
|
|
55
55
|
EFFECT_CLASSES = ("none", "draft", "publish", "send", "reply", "charge", "delete", "merge", "deploy", "change-policy")
|
|
56
56
|
DELEGATION_HEADER = "Fabric-Delegation"
|
|
@@ -92,7 +92,14 @@ REFUSAL_MESSAGES = {
|
|
|
92
92
|
"profile-forbids": "The caller's or the target's machine profile does not allow this call.",
|
|
93
93
|
"issuer-unavailable": "The estate's published keys cannot be read.",
|
|
94
94
|
"unknown-state": "The state cannot be established, so the call is refused.",
|
|
95
|
+
# DEC-0121 (AP-2, AP-4): the refusals of an action call; each answers 403.
|
|
96
|
+
"operator-decision-required": "This action is the operator's decision; ask the operator and call again with it in _meta.decision.",
|
|
97
|
+
"decision-invalid": "The call's decision is not valid for this action, object or time.",
|
|
98
|
+
"human-step-required": "This step is the person's own; hand them the link in error.data.url.",
|
|
95
99
|
}
|
|
100
|
+
# The detail each action-call refusal carries in error.data (the schema requires one, 1..80 characters).
|
|
101
|
+
_ACTION_DETAILS = {"operator-decision-required": "no-decision", "decision-invalid": "invalid-decision",
|
|
102
|
+
"human-step-required": "human-step", "approval-required": "no-approval"}
|
|
96
103
|
|
|
97
104
|
_B64URL = re.compile(r"^[A-Za-z0-9_-]+\Z")
|
|
98
105
|
# DL-2's grammar, as the contract's verifier types the claims (DEC-0045): an agent id, an opaque session or task id,
|
|
@@ -801,6 +808,20 @@ def refusal_from_denial(denial: Dict[str, Any]) -> Dict[str, Any]:
|
|
|
801
808
|
return {"error": error}
|
|
802
809
|
|
|
803
810
|
|
|
811
|
+
def refusal_from_action(answer: Any) -> Optional[Dict[str, Any]]:
|
|
812
|
+
"""DL-19 (DEC-0121): the refusal a service answers for an action call that fabric_actions.action_decision refused,
|
|
813
|
+
or None when it took the call. operator-decision-required, decision-invalid and approval-required carry their
|
|
814
|
+
detail; human-step-required carries `humanStep` and `url` (AP-4). A `fault` is the service's own and is never
|
|
815
|
+
sent: the caller reads unknown-state with the detail `service-fault`, as for anything else that is not an answer."""
|
|
816
|
+
if isinstance(answer, dict) and answer.get("ok") is True:
|
|
817
|
+
return None
|
|
818
|
+
code = answer.get("code") if isinstance(answer, dict) and answer.get("ok") is False else None
|
|
819
|
+
if not isinstance(code, str) or code not in _ACTION_DETAILS:
|
|
820
|
+
return _refuse("unknown-state", "service-fault")
|
|
821
|
+
data = answer.get("data")
|
|
822
|
+
return _refuse(code, _ACTION_DETAILS[code], dict(data) if isinstance(data, dict) and data else None)
|
|
823
|
+
|
|
824
|
+
|
|
804
825
|
def refusal_error(refusal: Dict[str, Any]) -> Dict[str, Any]:
|
|
805
826
|
"""The DL-19 body: {error: {code, message, data: {detail, ...}}}; never a token or a key."""
|
|
806
827
|
data = {"detail": refusal["detail"]}
|
|
@@ -36,6 +36,7 @@ from fabric_protocols import (
|
|
|
36
36
|
protocols_findings,
|
|
37
37
|
)
|
|
38
38
|
from fabric_connections import CONNECTIONS_EXTENSION_KEY, connections_view # noqa: E402 - the kit (FAA-62)
|
|
39
|
+
from fabric_actions import ACTIONS_EXTENSION_KEY, actions_extension, actions_extension_rules # noqa: E402 - DEC-0121
|
|
39
40
|
from fabric_usage import (
|
|
40
41
|
ROUTES,
|
|
41
42
|
)
|
|
@@ -46,7 +47,7 @@ from fabric_usage import (
|
|
|
46
47
|
DASHBOARD_EXTENSION_KEY = "https://fabric.passioncode.ai/agent-contract/extensions/dashboard/0.1"
|
|
47
48
|
|
|
48
49
|
|
|
49
|
-
DASHBOARD_KIT = "fabric-dashboard-kit@1.1.
|
|
50
|
+
DASHBOARD_KIT = "fabric-dashboard-kit@1.1.5"
|
|
50
51
|
|
|
51
52
|
|
|
52
53
|
REFERENCE_THEMES = ("graphite", "fabric", "paper", "forest", "ocean", "ember", "contrast")
|
|
@@ -217,6 +218,16 @@ SUBSCRIPTION_RUNNERS = {"claude-code": "claude", "kimi-code": "kimi", "codex": "
|
|
|
217
218
|
_PEER_ENDPOINT = re.compile(r"^https://(?P<host>([a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z]{2,63})(:([1-9][0-9]{0,3}|[1-5][0-9]{4}|6[0-4][0-9]{3}|65[0-4][0-9]{2}|655[0-2][0-9]|6553[0-5]))?(/[A-Za-z0-9._~!$&'()*+,;=:@%/-]*)?\Z", re.ASCII)
|
|
218
219
|
|
|
219
220
|
|
|
221
|
+
# FAA-120: source.repository is the schema's common `uri`: RFC 3986 characters only (unreserved, reserved and %HH),
|
|
222
|
+
# the same in both twins; an authority, when there is one, names a host and a port of digits up to 65535, as Fabric's
|
|
223
|
+
# `new URL` reads it. A user:password@ in it is a credential, never the name of a repository.
|
|
224
|
+
_SOURCE_URI = re.compile(r"^[A-Za-z][A-Za-z0-9+.-]*:(?:[A-Za-z0-9._~:/?#@!$&'()*+,;=-]|%[0-9A-Fa-f]{2})+\Z")
|
|
225
|
+
# Brackets belong to an IP-literal host only (RFC 3986 3.2.2); the literal is set aside before the character check.
|
|
226
|
+
_IP_LITERAL = re.compile(r"^([A-Za-z][A-Za-z0-9+.-]*://(?:[^/?#@\[\]]*@)?)\[[0-9A-Fa-f:.vV]+\]")
|
|
227
|
+
_URI_AUTHORITY = re.compile(r"^[A-Za-z][A-Za-z0-9+.-]*://(?:[^/?#@]*@)?(?P<host>\[[^\]]*\]|[^/?#:]*)(?::(?P<port>[^/?#]*))?")
|
|
228
|
+
_URI_PASSWORD = re.compile(r"^[A-Za-z][A-Za-z0-9+.-]*://[^/?#@]*:[^/?#@]*@")
|
|
229
|
+
|
|
230
|
+
|
|
220
231
|
_RESIDENCY = re.compile(r"^(A[B-Z]|[B-PR-WY][A-Z]|Q[A-L]|Z[A-Y])\Z", re.ASCII)
|
|
221
232
|
|
|
222
233
|
|
|
@@ -232,8 +243,16 @@ def _text(value: Any, low: int = 1, high: int = 200) -> bool:
|
|
|
232
243
|
return isinstance(value, str) and low <= len(value) <= high
|
|
233
244
|
|
|
234
245
|
|
|
246
|
+
def _authority_ok(uri: str) -> bool:
|
|
247
|
+
m = _URI_AUTHORITY.match(uri)
|
|
248
|
+
if not m:
|
|
249
|
+
return True
|
|
250
|
+
port = m.group("port")
|
|
251
|
+
return bool(m.group("host")) and (port is None or port == "" or (port.isdigit() and len(port) <= 5 and int(port) <= 65535))
|
|
252
|
+
|
|
253
|
+
|
|
235
254
|
def descriptor_member_problems(descriptor: Dict[str, Any]) -> List[str]:
|
|
236
|
-
"""DEC-0061 and DEC-0069 on a descriptor's `installedBy`, `runner` and `peer`, as the schema shapes them, with
|
|
255
|
+
"""DEC-0061 and DEC-0069 on a descriptor's `installedBy`, `runner` and `peer`, and `source` (FAA-120), as the schema shapes them, with
|
|
237
256
|
FAC-SEM-085 (a peer on a reserved name) and FAC-SEM-086 (a credential-shaped value in `runner` or `installedBy`,
|
|
238
257
|
named by its place, never its value)."""
|
|
239
258
|
import fabric_secrets # noqa: E402 - beside this file; the contract's detector (DEC-0044)
|
|
@@ -273,6 +292,21 @@ def descriptor_member_problems(descriptor: Dict[str, Any]) -> List[str]:
|
|
|
273
292
|
if runner.get("credentialKind") == "subscription" and runner.get("kind") not in SUBSCRIPTION_RUNNERS:
|
|
274
293
|
problems.append("a subscription runs only in its vendor's runner: runner.kind claude-code, kimi-code or codex")
|
|
275
294
|
places += [("/runner/%s" % _pointer(k), v) for k, v in runner.items()]
|
|
295
|
+
if "source" in descriptor:
|
|
296
|
+
source = descriptor.get("source")
|
|
297
|
+
if not isinstance(source, dict):
|
|
298
|
+
problems.append("source is {repository?}")
|
|
299
|
+
source = {}
|
|
300
|
+
problems += ["source has no member %s" % k for k in source if k != "repository"]
|
|
301
|
+
if "repository" in source:
|
|
302
|
+
repository = source["repository"]
|
|
303
|
+
if not (isinstance(repository, str) and _SOURCE_URI.match(_IP_LITERAL.sub(r"\1h", repository, count=1))):
|
|
304
|
+
problems.append("source.repository is a URI (RFC 3986), such as https://github.com/owner/name")
|
|
305
|
+
elif not _authority_ok(repository):
|
|
306
|
+
problems.append("source.repository names a host, and a port of digits up to 65535 when it has one")
|
|
307
|
+
elif _URI_PASSWORD.match(repository):
|
|
308
|
+
problems.append("source.repository carries no password: it names the repository, never a credential")
|
|
309
|
+
places.append(("/source/repository", repository))
|
|
276
310
|
if "peer" in descriptor:
|
|
277
311
|
peer = descriptor.get("peer")
|
|
278
312
|
if not isinstance(peer, dict):
|
|
@@ -299,7 +333,8 @@ def descriptor_member_problems(descriptor: Dict[str, Any]) -> List[str]:
|
|
|
299
333
|
problems.append("a service offered to others never runs on a subscription (DEC-0061)")
|
|
300
334
|
for place, value in places:
|
|
301
335
|
if isinstance(value, str) and fabric_secrets.secret_argument(value):
|
|
302
|
-
problems.append("FAC-SEM-086: %s holds a value shaped like a credential; it names
|
|
336
|
+
problems.append("FAC-SEM-086: %s holds a value shaped like a credential; it names %s" % (
|
|
337
|
+
place, "a repository" if place.startswith("/source/") else "a kind, a role or an installer"))
|
|
303
338
|
return problems
|
|
304
339
|
|
|
305
340
|
|
|
@@ -543,11 +578,16 @@ def build_well_known(*, service_id: str, instance: str, name: str, version: str,
|
|
|
543
578
|
update_available: Optional[str] = None, pid: Optional[int] = None,
|
|
544
579
|
extensions: Optional[Dict[str, Any]] = None,
|
|
545
580
|
configuration: Optional[Dict[str, Any]] = None,
|
|
546
|
-
protocols: Optional[Sequence[str]] = None
|
|
581
|
+
protocols: Optional[Sequence[str]] = None,
|
|
582
|
+
actions: Optional[Sequence[Dict[str, Any]]] = None) -> Dict[str, Any]:
|
|
547
583
|
"""The unauthenticated identity answer. `degraded` is always present; ready means empty.
|
|
548
584
|
`configuration` (DEC-0046) says what the operator can configure and what applies now; fabric_profiles builds it.
|
|
549
585
|
`extensions` is kept as given (a reader ignores keys it does not know); a dashboard declaration
|
|
550
|
-
in it must be valid (dashboard_extension builds one).
|
|
586
|
+
in it must be valid (dashboard_extension builds one).
|
|
587
|
+
`actions` (DEC-0121 AP-1) is the service's action table, every button its dashboard, app or page offers with the
|
|
588
|
+
tool that performs it and its gate; it is written under extensions/actions/0.1 by fabric_actions.actions_extension.
|
|
589
|
+
A declaration FAC-SEM-142 finds, passed either way, is refused: one that is not its schema's, an id named twice, a
|
|
590
|
+
humanStep that disagrees with the gate, or a tool `surfaces.mcp.capabilities` does not list."""
|
|
551
591
|
if status not in STATUSES:
|
|
552
592
|
raise ServiceError("status must be one of %s." % ", ".join(STATUSES))
|
|
553
593
|
degraded = list(degraded)
|
|
@@ -566,6 +606,12 @@ def build_well_known(*, service_id: str, instance: str, name: str, version: str,
|
|
|
566
606
|
}
|
|
567
607
|
if summary:
|
|
568
608
|
doc["summary"] = list(summary)[:6]
|
|
609
|
+
if actions is not None:
|
|
610
|
+
if extensions and ACTIONS_EXTENSION_KEY in extensions:
|
|
611
|
+
raise ServiceError("Actions declaration: pass the action table or a ready declaration, not both.")
|
|
612
|
+
mcp = surfaces.get("mcp")
|
|
613
|
+
capabilities = mcp.get("capabilities") if isinstance(mcp, dict) else None
|
|
614
|
+
extensions = dict(extensions or {}, **actions_extension(actions, capabilities if isinstance(capabilities, list) else None))
|
|
569
615
|
if extensions:
|
|
570
616
|
if DASHBOARD_EXTENSION_KEY in extensions:
|
|
571
617
|
problems = dashboard_extension_problems(extensions[DASHBOARD_EXTENSION_KEY])
|
|
@@ -576,6 +622,13 @@ def build_well_known(*, service_id: str, instance: str, name: str, version: str,
|
|
|
576
622
|
view = connections_view({"extensions": {CONNECTIONS_EXTENSION_KEY: extensions[CONNECTIONS_EXTENSION_KEY]}})
|
|
577
623
|
if view["reported"]:
|
|
578
624
|
raise ServiceError("Connections declaration: %s." % "; ".join(view["errors"]))
|
|
625
|
+
if ACTIONS_EXTENSION_KEY in extensions:
|
|
626
|
+
# DEC-0121 AP-1: a host reports an invalid declaration (FAC-SEM-142); a service never publishes one. The
|
|
627
|
+
# tools are compared with the capabilities this document's MCP surface lists.
|
|
628
|
+
findings = actions_extension_rules({"extensions": {ACTIONS_EXTENSION_KEY: extensions[ACTIONS_EXTENSION_KEY]},
|
|
629
|
+
"surfaces": surfaces})
|
|
630
|
+
if findings:
|
|
631
|
+
raise ServiceError("Actions declaration: %s." % "; ".join(f["message"] for f in findings))
|
|
579
632
|
doc["extensions"] = dict(extensions)
|
|
580
633
|
if configuration is not None:
|
|
581
634
|
problems = configuration_problems(configuration)
|
|
@@ -24,11 +24,14 @@ from pathlib import Path
|
|
|
24
24
|
import re
|
|
25
25
|
import secrets
|
|
26
26
|
import sys
|
|
27
|
+
import time
|
|
27
28
|
from typing import Any, Callable, Dict, List, Optional
|
|
28
29
|
|
|
29
30
|
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
30
31
|
import fabric_service as _fs # noqa: E402
|
|
31
32
|
from fabric_base import store_lock # noqa: E402 - one lock per store path (R3)
|
|
33
|
+
from fabric_actions import action_caller, readable_action # noqa: E402 - DEC-0121
|
|
34
|
+
from fabric_time import format_rfc3339 # noqa: E402 - the core's (FAA-65)
|
|
32
35
|
|
|
33
36
|
from fabric_interop_rules import ( # noqa: E402,F401 - the rule layer (FAA-92)
|
|
34
37
|
_days_from_civil,
|
|
@@ -77,6 +80,8 @@ from fabric_interop_rules import ( # noqa: E402,F401 - the rule layer (FAA-92)
|
|
|
77
80
|
intent_idempotency_key,
|
|
78
81
|
reads_null_usage,
|
|
79
82
|
request_meta,
|
|
83
|
+
action_gate,
|
|
84
|
+
action_refusal_tool_result,
|
|
80
85
|
usage_findings,
|
|
81
86
|
usage_toward,
|
|
82
87
|
_job_findings,
|
|
@@ -430,16 +435,26 @@ class JobStore:
|
|
|
430
435
|
# --- a minimal MCP tool server (streamable HTTP, JSON responses) ------------------------
|
|
431
436
|
|
|
432
437
|
class CallContext:
|
|
433
|
-
"""What a tool handler gets: the child span it runs in, and a way to start a job.
|
|
438
|
+
"""What a tool handler gets: the child span it runs in, and a way to start a job.
|
|
439
|
+
|
|
440
|
+
DEC-0121: `tier` is the caller's (DL-25), `caller` the identity the service authenticated (decision D9), `meta`
|
|
441
|
+
the request's `_meta`. For a tool registered with an action, `record` is action_decision's recordAs: "caller"
|
|
442
|
+
(the caller's own act) or {by, via, caller}, the act to put in the service's audit record, and `decision` the
|
|
443
|
+
operator's decision it took (its `instruction` stays in that record, never in events or a page another party
|
|
444
|
+
reads), else None. `input_records` maps each answered input request to its record when input_actions is on."""
|
|
434
445
|
|
|
435
446
|
def __init__(self, server: "McpToolServer", tool: str, arguments: Any, traceparent: str,
|
|
436
|
-
delegation: Optional[Dict[str, Any]] = None
|
|
447
|
+
delegation: Optional[Dict[str, Any]] = None, *, tier: Optional[str] = None, caller: Optional[str] = None,
|
|
448
|
+
meta: Optional[Dict[str, Any]] = None, record: Any = None, decision: Optional[Dict[str, Any]] = None,
|
|
449
|
+
input_records: Optional[Dict[str, Any]] = None):
|
|
437
450
|
self.server = server
|
|
438
451
|
self.tool = tool
|
|
439
452
|
self.arguments = arguments
|
|
440
453
|
self.traceparent = traceparent
|
|
441
454
|
# DEC-0039: the accepted delegation (principal, caller, chain, claims) when the server has a gate.
|
|
442
455
|
self.delegation = delegation
|
|
456
|
+
self.tier, self.caller, self.meta = tier, caller, {} if meta is None else meta
|
|
457
|
+
self.record, self.decision, self.input_records = record, decision, input_records
|
|
443
458
|
|
|
444
459
|
def start_job(self, poll_interval_ms: Optional[int] = None) -> Dict[str, Any]:
|
|
445
460
|
if self.server.jobs is None:
|
|
@@ -547,11 +562,26 @@ class McpToolServer:
|
|
|
547
562
|
def __init__(self, name: str, version: str, jobs: Optional[JobStore] = None,
|
|
548
563
|
on_job_started: Optional[Callable[[str, CallContext], None]] = None,
|
|
549
564
|
on_input: Optional[OnInput] = None,
|
|
550
|
-
authorize: Optional[Callable[[str, Any], Dict[str, Any]]] = None
|
|
565
|
+
authorize: Optional[Callable[[str, Any], Dict[str, Any]]] = None, *,
|
|
566
|
+
identify: Optional[Callable[[Any, str], Optional[str]]] = None,
|
|
567
|
+
approve: Optional[Callable[[str, Dict[str, Any], Any, str], Optional[Dict[str, Any]]]] = None,
|
|
568
|
+
elicited: Optional[Callable[[str, Dict[str, Any], Optional[str]], bool]] = None,
|
|
569
|
+
input_actions: bool = False, clock: Callable[[], float] = time.time,
|
|
570
|
+
on_fault: Optional[Callable[[str, str], None]] = None):
|
|
551
571
|
# authorize(tool, headers, tier=) runs before every handler (fabric_delegation.delegation_gate, DEC-0039):
|
|
552
572
|
# a refusal is answered in the DL-19 shape and the handler never runs. The built-in job tools are
|
|
553
573
|
# gated by the job's own capability, and answered only for the principal and caller that started it.
|
|
574
|
+
# DEC-0121: a tool added with an action runs action_gate after authorize and before its handler.
|
|
575
|
+
# identify(headers, tier) names the caller the service authenticated when no delegation did (D9; else
|
|
576
|
+
# local:T0 / local:T1); approve(tool, arguments, headers, digest) answers verify_approval's outcome for an
|
|
577
|
+
# approval the call presented, or None; elicited(tool, arguments, caller) is True only on the service's own
|
|
578
|
+
# record of an accepted elicitation. input_actions=True gates each answered input request as an action (D8).
|
|
579
|
+
# on_fault(tool, fault) hears a service fault the caller only sees as unknown-state (e.g. caller-missing: an
|
|
580
|
+
# action-gated server needs a tier, identify or authorize that names who calls).
|
|
554
581
|
self._authorize = authorize
|
|
582
|
+
self._identify, self._approve, self._elicited = identify, approve, elicited
|
|
583
|
+
self.input_actions, self._clock, self._on_fault = input_actions is True, clock, on_fault
|
|
584
|
+
self.actions: Dict[str, Dict[str, Any]] = {}
|
|
555
585
|
self.info = {"name": name, "version": version}
|
|
556
586
|
self.jobs = jobs
|
|
557
587
|
self.tools: List[Dict[str, Any]] = []
|
|
@@ -559,14 +589,101 @@ class McpToolServer:
|
|
|
559
589
|
self._on_job_started = on_job_started
|
|
560
590
|
self._on_input = on_input
|
|
561
591
|
|
|
562
|
-
def add_tool(self, tool: Dict[str, Any], handler: Handler
|
|
592
|
+
def add_tool(self, tool: Dict[str, Any], handler: Handler, action: Optional[Dict[str, Any]] = None, *,
|
|
593
|
+
target: Any = None, human_step_url: Optional[str] = None) -> None:
|
|
594
|
+
"""Serve a tool. `action` (DEC-0121 AP-1) is the declared action this tool performs, {id, title, tool, gate,
|
|
595
|
+
humanStep?}, its `tool` this tool's name: the server runs action_gate before the handler. `target` names the
|
|
596
|
+
object a call acts on: an argument's name, or a function of the arguments; `human_step_url` is the link of a
|
|
597
|
+
`human` action's step (an https URL or a path on the service's origin, AP-4)."""
|
|
563
598
|
if tool["name"] in self.handlers:
|
|
564
599
|
raise InteropError("Tool %s is served twice." % tool["name"])
|
|
565
600
|
if "outputSchema" in tool:
|
|
566
601
|
require_object_root(tool["name"], tool["outputSchema"])
|
|
602
|
+
if action is not None:
|
|
603
|
+
if not readable_action(action) or action["tool"] != tool["name"]:
|
|
604
|
+
raise InteropError("Tool %s: its action is not a declared action of this tool (DEC-0121)." % tool["name"])
|
|
605
|
+
if action["id"] in {a["action"]["id"] for a in self.actions.values()}:
|
|
606
|
+
raise InteropError("Action %s is declared twice." % action["id"])
|
|
607
|
+
if action["gate"] == "human" and not (isinstance(human_step_url, str) and re.match(r"https://|/", human_step_url)):
|
|
608
|
+
raise InteropError("Tool %s: a human step needs its link, an https URL or a path (DEC-0121 AP-4)." % tool["name"])
|
|
609
|
+
if not (target is None or isinstance(target, str) or callable(target)):
|
|
610
|
+
raise InteropError("Tool %s: target is an argument's name or a function of the arguments." % tool["name"])
|
|
611
|
+
self.actions[tool["name"]] = {"action": dict(action), "target": target, "url": human_step_url}
|
|
567
612
|
self.tools.append(tool)
|
|
568
613
|
self.handlers[tool["name"]] = handler
|
|
569
614
|
|
|
615
|
+
def action_table(self) -> List[Dict[str, Any]]:
|
|
616
|
+
"""The declared actions in the order they were added: build_well_known(actions=) writes them (DEC-0121 AP-1)."""
|
|
617
|
+
return [dict(entry["action"]) for entry in self.actions.values()]
|
|
618
|
+
|
|
619
|
+
def _caller(self, headers: Any, tier: str, verdict: Optional[Dict[str, Any]]) -> Optional[str]:
|
|
620
|
+
"""D9: the delegation's caller when a gate accepted a token, else identify(headers, tier), else the local floor."""
|
|
621
|
+
authenticated = (verdict or {}).get("caller")
|
|
622
|
+
if not authenticated and self._identify is not None:
|
|
623
|
+
authenticated = self._identify(headers, tier)
|
|
624
|
+
return action_caller(tier, authenticated)
|
|
625
|
+
|
|
626
|
+
def _gate_action(self, action: Dict[str, Any], name: str, arguments: Dict[str, Any], target: Any, url: Any,
|
|
627
|
+
headers: Any, tier: str, meta: Dict[str, Any], caller: Optional[str]) -> Dict[str, Any]:
|
|
628
|
+
"""action_gate for one call: the approval the call presented, the digest of {tool, arguments}, the clock."""
|
|
629
|
+
digest, approval = None, None
|
|
630
|
+
if action["gate"] == "relayed":
|
|
631
|
+
import fabric_delegation as _fd # noqa: E402 - only an action-gated server needs it
|
|
632
|
+
try:
|
|
633
|
+
digest = _fd.action_digest({"tool": name, "arguments": arguments})
|
|
634
|
+
except (TypeError, ValueError, RecursionError): # no canonical form (a lone surrogate, a non-finite number,
|
|
635
|
+
# nesting past the recursion limit), as intent_digest reads it: no digest to bind
|
|
636
|
+
return {"ok": False, "invalid": "Invalid params: the arguments have no canonical form."}
|
|
637
|
+
outcome = self._approve(name, arguments, headers, digest) if self._approve is not None else None
|
|
638
|
+
if isinstance(outcome, dict) and outcome.get("ok") is True:
|
|
639
|
+
approval = outcome
|
|
640
|
+
elif isinstance(outcome, dict) and outcome.get("code") not in (None, "approval-required"):
|
|
641
|
+
# DL-18: the presented approval's own refusal; an outcome that is not verify_approval's is the service's.
|
|
642
|
+
readable = outcome.get("code") in _fd.REFUSAL_CODES and isinstance(outcome.get("detail"), str) and outcome["detail"]
|
|
643
|
+
if readable:
|
|
644
|
+
return {"ok": False, "refusal": outcome}
|
|
645
|
+
return {"ok": False, "refusal": {"ok": False, "code": "unknown-state", "detail": "service-fault"},
|
|
646
|
+
"fault": "approval-outcome-unreadable"}
|
|
647
|
+
elicited = action["gate"] == "relayed" and self._elicited is not None and self._elicited(name, arguments, caller) is True
|
|
648
|
+
return action_gate(action, tier=tier, caller=caller, meta=meta, target=target, elicited=elicited, approval=approval,
|
|
649
|
+
action_digest=digest, human_step_url=url, now=format_rfc3339(self._clock() * 1000))
|
|
650
|
+
|
|
651
|
+
def _gate_inputs(self, arguments: Dict[str, Any], headers: Any, tier: str, meta: Dict[str, Any],
|
|
652
|
+
caller: Optional[str]) -> Dict[str, Any]:
|
|
653
|
+
"""D8: each input request an answer would take is an action (id its key, tool fabric.job.get, gate relayed),
|
|
654
|
+
one decision per call; the first refusal refuses the call and nothing is applied."""
|
|
655
|
+
job_id = str(arguments.get("id", ""))
|
|
656
|
+
job = self.jobs.get(job_id)
|
|
657
|
+
asked = job.get("inputRequests", {}) if job["status"] == "input_required" else {}
|
|
658
|
+
records: Dict[str, Any] = {}
|
|
659
|
+
for key, value in arguments["inputResponses"].items():
|
|
660
|
+
if key not in asked or not isinstance(value, dict) or value.get("action") not in ("accept", "decline", "cancel"):
|
|
661
|
+
continue
|
|
662
|
+
action = {"id": key, "title": "Answer %s" % key, "tool": "fabric.job.get", "gate": "relayed"}
|
|
663
|
+
gated = self._gate_action(action, "fabric.job.get", {"id": job_id, "inputResponses": {key: value}}, None, None,
|
|
664
|
+
headers, tier, meta, caller)
|
|
665
|
+
if not gated["ok"]:
|
|
666
|
+
return gated
|
|
667
|
+
records[key] = gated["record"]
|
|
668
|
+
return {"ok": True, "records": records}
|
|
669
|
+
|
|
670
|
+
def _fault(self, tool: str, gated: Dict[str, Any]) -> None:
|
|
671
|
+
"""Hand a service fault to on_fault; the caller saw only unknown-state. A hook that raises is the service's."""
|
|
672
|
+
if gated.get("fault") and self._on_fault is not None:
|
|
673
|
+
try:
|
|
674
|
+
self._on_fault(tool, str(gated["fault"]))
|
|
675
|
+
except Exception: # noqa: BLE001 - logging must not change the answer
|
|
676
|
+
pass
|
|
677
|
+
|
|
678
|
+
@staticmethod
|
|
679
|
+
def _target(target: Any, arguments: Dict[str, Any]) -> Optional[str]:
|
|
680
|
+
"""The object a call acts on: the named argument, or what the service's function answers; a string or None."""
|
|
681
|
+
try:
|
|
682
|
+
value = target(arguments) if callable(target) else arguments.get(target) if isinstance(target, str) else None
|
|
683
|
+
except Exception: # noqa: BLE001 - a function that cannot read this call names no object
|
|
684
|
+
return None
|
|
685
|
+
return value if isinstance(value, str) else None
|
|
686
|
+
|
|
570
687
|
def job_started(self, job_id: str, ctx: CallContext) -> None:
|
|
571
688
|
if self._on_job_started:
|
|
572
689
|
self._on_job_started(job_id, ctx)
|
|
@@ -641,7 +758,21 @@ class McpToolServer:
|
|
|
641
758
|
if not verdict.get("ok"):
|
|
642
759
|
import fabric_delegation as _fd # noqa: E402 - only a gated server needs it
|
|
643
760
|
return {"jsonrpc": "2.0", "id": rid, "result": _fd.refusal_tool_result(verdict, traceparent=span)}
|
|
644
|
-
|
|
761
|
+
caller, inputs = self._caller(headers, tier, verdict), None
|
|
762
|
+
responses = arguments.get("inputResponses")
|
|
763
|
+
if name == "fabric.job.get" and self.input_actions and isinstance(responses, dict) and responses:
|
|
764
|
+
try:
|
|
765
|
+
inputs = self._gate_inputs(arguments, headers, tier, meta, caller)
|
|
766
|
+
except UnknownJob:
|
|
767
|
+
return {"jsonrpc": "2.0", "id": rid, "result": unknown_job_result(str(arguments.get("id", "")), traceparent=span)}
|
|
768
|
+
if not inputs["ok"] and "invalid" in inputs:
|
|
769
|
+
return self._error(rid, -32602, inputs["invalid"])
|
|
770
|
+
if not inputs["ok"]:
|
|
771
|
+
self._fault(name, inputs)
|
|
772
|
+
import fabric_delegation as _fd # noqa: E402 - DL-19
|
|
773
|
+
return {"jsonrpc": "2.0", "id": rid, "result": _fd.refusal_tool_result(inputs["refusal"], traceparent=span)}
|
|
774
|
+
context = {"tier": tier, "caller": caller, "meta": meta, "input_records": inputs["records"] if inputs else None}
|
|
775
|
+
return {"jsonrpc": "2.0", "id": rid, "result": self._job_call(name, arguments, span, verdict, context)}
|
|
645
776
|
handler = self.handlers.get(name)
|
|
646
777
|
if handler is None:
|
|
647
778
|
return self._error(rid, -32602, "Unknown tool: %s." % name)
|
|
@@ -652,14 +783,26 @@ class McpToolServer:
|
|
|
652
783
|
import fabric_delegation as _fd # noqa: E402 - only a gated server needs it
|
|
653
784
|
return {"jsonrpc": "2.0", "id": rid, "result": _fd.refusal_tool_result(verdict, traceparent=span)}
|
|
654
785
|
delegation = verdict
|
|
786
|
+
caller, gated, entry = self._caller(headers, tier, delegation), {"record": None, "decision": None}, self.actions.get(name)
|
|
787
|
+
if entry is not None: # DEC-0121: after the authorize gate, before the handler
|
|
788
|
+
gated = self._gate_action(entry["action"], name, arguments, self._target(entry["target"], arguments), entry["url"],
|
|
789
|
+
headers, tier, meta, caller)
|
|
790
|
+
if not gated["ok"] and "invalid" in gated:
|
|
791
|
+
return self._error(rid, -32602, gated["invalid"])
|
|
792
|
+
if not gated["ok"]:
|
|
793
|
+
self._fault(name, gated)
|
|
794
|
+
import fabric_delegation as _fd # noqa: E402 - DL-19
|
|
795
|
+
return {"jsonrpc": "2.0", "id": rid, "result": _fd.refusal_tool_result(gated["refusal"], traceparent=span)}
|
|
796
|
+
context = CallContext(self, name, arguments, span, delegation, tier=tier, caller=caller, meta=meta,
|
|
797
|
+
record=gated["record"], decision=gated["decision"])
|
|
655
798
|
try:
|
|
656
|
-
structured = handler(arguments,
|
|
799
|
+
structured = handler(arguments, context)
|
|
657
800
|
except InteropError as exc:
|
|
658
801
|
return {"jsonrpc": "2.0", "id": rid, "result": tool_result({"error": {"code": "tool-error", "message": str(exc)}}, is_error=True, traceparent=span)}
|
|
659
802
|
return {"jsonrpc": "2.0", "id": rid, "result": tool_result(structured, traceparent=span)}
|
|
660
803
|
|
|
661
804
|
def _job_call(self, name: str, arguments: Dict[str, Any], span: str,
|
|
662
|
-
delegation: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
805
|
+
delegation: Optional[Dict[str, Any]] = None, context: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
663
806
|
job_id = str(arguments.get("id", ""))
|
|
664
807
|
try:
|
|
665
808
|
job_span = self.jobs.traceparent(job_id) or span
|
|
@@ -674,11 +817,14 @@ class McpToolServer:
|
|
|
674
817
|
state = self.jobs.get(job_id)
|
|
675
818
|
return tool_result({"job": state}, traceparent=job_span)
|
|
676
819
|
responses = arguments.get("inputResponses")
|
|
820
|
+
context = context or {}
|
|
821
|
+
if responses and context.get("input_records") is not None: # D8: only the keys the action gate took
|
|
822
|
+
responses = {key: responses[key] for key in context["input_records"]}
|
|
677
823
|
if responses:
|
|
678
824
|
matched = self.jobs.answer(job_id, responses)
|
|
679
825
|
if matched and self._on_input:
|
|
680
826
|
private = self.jobs.private(job_id)
|
|
681
|
-
self._on_input(job_id, matched, CallContext(self, private["capability"], private["input"], job_span, delegation))
|
|
827
|
+
self._on_input(job_id, matched, CallContext(self, private["capability"], private["input"], job_span, delegation, **context))
|
|
682
828
|
return tool_result({"job": self.jobs.get(job_id)}, traceparent=job_span)
|
|
683
829
|
except UnknownJob:
|
|
684
830
|
return unknown_job_result(job_id, traceparent=span)
|
|
@@ -17,6 +17,7 @@ if str(_Path(__file__).resolve().parent) not in _sys.path: # the kit loads from
|
|
|
17
17
|
_sys.path.insert(0, str(_Path(__file__).resolve().parent))
|
|
18
18
|
|
|
19
19
|
from fabric_base import ServiceError # noqa: E402
|
|
20
|
+
from fabric_actions import action_decision, decision_shape # noqa: E402 - DEC-0121
|
|
20
21
|
|
|
21
22
|
PROTOCOL = "fabric-interop/0.1"
|
|
22
23
|
EXTENSION_KEY = "https://fabric.passioncode.ai/agent-contract/extensions/interop/0.1"
|
|
@@ -288,14 +289,56 @@ def reads_null_usage(request_meta: Any) -> bool:
|
|
|
288
289
|
return isinstance(protocols, list) and INTEROP_NULL_USAGE in protocols
|
|
289
290
|
|
|
290
291
|
|
|
291
|
-
def request_meta(traceparent: Optional[str] = None) -> Dict[str, Any]:
|
|
292
|
-
"""A request's `_meta` from this kit as a caller: its trace,
|
|
292
|
+
def request_meta(traceparent: Optional[str] = None, decision: Any = None) -> Dict[str, Any]:
|
|
293
|
+
"""A request's `_meta` from this kit as a caller: its trace, the interop revisions it reads (DEC-0087) and, for a
|
|
294
|
+
relayed action, the operator's decision (DEC-0121 AP-2; build one with fabric_actions.operator_decision). A
|
|
295
|
+
decision travels here only, never in a tool's arguments; one decision.schema.json refuses raises InteropError."""
|
|
293
296
|
meta: Dict[str, Any] = {"protocols": list(PROTOCOLS_READ)}
|
|
294
297
|
if parse_traceparent(traceparent):
|
|
295
298
|
meta["traceparent"] = traceparent
|
|
299
|
+
if decision is not None:
|
|
300
|
+
if not decision_shape(decision):
|
|
301
|
+
raise InteropError("The decision is not one decision.schema.json reads; it is not sent.")
|
|
302
|
+
meta["decision"] = dict(decision)
|
|
296
303
|
return meta
|
|
297
304
|
|
|
298
305
|
|
|
306
|
+
def action_gate(action: Any, *, tier: Any, caller: Any, meta: Any = None, target: Optional[str] = None,
|
|
307
|
+
elicited: bool = False, approval: Any = None, action_digest: Optional[str] = None,
|
|
308
|
+
human_step_url: Optional[str] = None, now: Any = None) -> Dict[str, Any]:
|
|
309
|
+
"""One call of a declared action, before its handler runs (DEC-0121 AP-2, AP-4): fabric_actions.action_decision over
|
|
310
|
+
the call's `_meta.decision` (read from `meta` only, never from arguments, decision D11), the service's authenticated
|
|
311
|
+
`caller` (fabric_actions.action_caller), `target` (the object the call acts on), `elicited` (the service's own
|
|
312
|
+
record of an accepted elicitation), `approval` (verify_approval's accepted outcome), `action_digest`,
|
|
313
|
+
`human_step_url` and `now` (RFC 3339). Answers {ok: True, record, decision}, `decision` being the one taken as the
|
|
314
|
+
operator's (None otherwise), or {ok: False, refusal} in the DL-19 form (fabric_delegation.refusal_tool_result
|
|
315
|
+
sends it); a service's own fault is the refusal unknown-state with `fault` beside it for the service's log."""
|
|
316
|
+
import fabric_delegation as _fd # noqa: E402 - beside this file: DL-19
|
|
317
|
+
inputs: Dict[str, Any] = {"action": action, "tier": tier, "caller": caller, "elicited": elicited is True, "now": now}
|
|
318
|
+
if isinstance(meta, dict) and "decision" in meta:
|
|
319
|
+
inputs["decision"] = meta["decision"]
|
|
320
|
+
for name, value in (("target", target), ("approval", approval), ("actionDigest", action_digest), ("humanStepUrl", human_step_url)):
|
|
321
|
+
if value is not None:
|
|
322
|
+
inputs[name] = value
|
|
323
|
+
answer = action_decision(inputs)
|
|
324
|
+
if answer.get("ok") is True:
|
|
325
|
+
record = answer["recordAs"]
|
|
326
|
+
taken = isinstance(record, dict) and record.get("via") == "relayed-instruction"
|
|
327
|
+
return {"ok": True, "record": record, "decision": inputs["decision"] if taken else None}
|
|
328
|
+
out: Dict[str, Any] = {"ok": False, "refusal": _fd.refusal_from_action(answer)}
|
|
329
|
+
if "fault" in answer:
|
|
330
|
+
out["fault"] = answer["fault"]
|
|
331
|
+
return out
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def action_refusal_tool_result(answer: Any, traceparent: Optional[str] = None) -> Optional[Dict[str, Any]]:
|
|
335
|
+
"""The tools/call result a service's own MCP server answers for an action_decision answer: None when the action
|
|
336
|
+
takes the call, else DL-19's refusal as structuredContent with isError true (DEC-0121)."""
|
|
337
|
+
import fabric_delegation as _fd # noqa: E402 - beside this file: DL-19
|
|
338
|
+
refusal = _fd.refusal_from_action(answer)
|
|
339
|
+
return None if refusal is None else _fd.refusal_tool_result(refusal, traceparent=traceparent)
|
|
340
|
+
|
|
341
|
+
|
|
299
342
|
def usage_findings(usage: Any, at: str) -> List[Dict[str, str]]:
|
|
300
343
|
"""FAC-SEM-119 on a usage: partial false asserts every number known, so it stands beside no null."""
|
|
301
344
|
if not isinstance(usage, dict) or usage.get("partial", _ABSENT) is not False:
|
|
@@ -15,7 +15,7 @@ if str(_Path(__file__).resolve().parent) not in _sys.path: # the kit loads from
|
|
|
15
15
|
PROTOCOL_REVISIONS: Dict[str, Tuple[str, ...]] = {
|
|
16
16
|
"access-log": ("1",), "approval": ("1",), "activity-summary": ("1",), "agent-directory": ("1",), "call-record": ("1",), "coding-agents": ("1",), "credential-decision": ("1",), "credential-request": ("1",), "credential-revocation": ("1",), "enroll-token": ("1",), "entitlement": ("1",),
|
|
17
17
|
"entitlement-key-set": ("1",), "estate-record": ("1",), "execution-packet": ("1",),
|
|
18
|
-
"fabric-activity": ("0.1",), "fabric-agent-call": ("1",), "fabric-connections": ("0.1",), "fabric-credential-envelope": ("1",), "fabric-load": ("0.1",), "fabric-dashboard": ("0.1",), "fabric-delegation": ("0.1",), "fabric-device": ("0.1",),
|
|
18
|
+
"fabric-actions": ("0.1",), "fabric-activity": ("0.1",), "fabric-agent-call": ("1",), "fabric-connections": ("0.1",), "fabric-credential-envelope": ("1",), "fabric-load": ("0.1",), "fabric-dashboard": ("0.1",), "fabric-delegation": ("0.1",), "fabric-device": ("0.1",),
|
|
19
19
|
"fabric-interop": ("0.1", "0.2"), "fabric-local-channel": ("0.1",), "fabric-local-runner": ("0.1",),
|
|
20
20
|
"fabric-operator-channel": ("0.1",), "fabric-package": ("1",), "fabric-project-comms": ("0.1",), "fabric-proposal": ("1",),
|
|
21
21
|
"fabric-provider": ("0.1",), "fabric-relay": ("0.1",), "fabric-secret-envelope": ("1",), "fabric-service": ("0.1",),
|
|
@@ -181,6 +181,15 @@ from fabric_backups import ( # noqa: F401
|
|
|
181
181
|
newest_intact_backup,
|
|
182
182
|
restore_plan,
|
|
183
183
|
)
|
|
184
|
+
from fabric_actions import ( # noqa: F401
|
|
185
|
+
ACTIONS_EXTENSION_KEY,
|
|
186
|
+
action_decision,
|
|
187
|
+
action_rules,
|
|
188
|
+
actions_extension,
|
|
189
|
+
actions_extension_rules,
|
|
190
|
+
actions_view,
|
|
191
|
+
decision_shape,
|
|
192
|
+
)
|
|
184
193
|
from fabric_connections import ( # noqa: F401
|
|
185
194
|
CONNECTIONS_EXTENSION_KEY,
|
|
186
195
|
connections_extension,
|