@passioncode-ai/passioncode 0.2.40 → 0.2.42
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/hooks/hooks.json +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +3 -3
- package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/references/operator-channel-and-repository.md +4 -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/building-fabric-services/SKILL.md +2 -2
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +3 -2
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +22 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/estate-record.md +44 -6
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +46 -8
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +6 -4
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +7 -2
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-actions.mjs +30 -15
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-context.mjs +5 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-delegation.mjs +17 -9
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-estate-rules.mjs +606 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-estate.mjs +48 -525
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop-rules.mjs +43 -6
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop.mjs +14 -4
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-mandate.mjs +48 -15
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-protocols.mjs +4 -3
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_actions.py +39 -16
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_context.py +8 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +22 -11
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_estate.py +86 -642
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_estate_rules.py +766 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +70 -12
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop_rules.py +59 -8
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_mandate.py +56 -16
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_protocols.py +4 -2
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +220 -21
- package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +3 -3
- package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/operator-channel-and-repository.md +4 -1
- package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/resilience-and-autonomy.md +7 -0
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +69 -34
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +27 -15
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +13 -7
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +8 -0
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +17 -9
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +60 -1
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mail.py +10 -3
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mcp.py +39 -4
- package/payload/plugins/passioncode/.claude-plugin/plugin.json +1 -1
|
@@ -32,7 +32,7 @@ sys.path.insert(0, str(Path(__file__).resolve().parent))
|
|
|
32
32
|
import fabric_service as _fs # noqa: E402
|
|
33
33
|
from fabric_base import store_lock # noqa: E402 - one lock per store path (R3)
|
|
34
34
|
from fabric_actions import action_caller, readable_action # noqa: E402 - DEC-0121
|
|
35
|
-
from fabric_mandate import EFFECT_CLASSES, is_agent_id # noqa: E402 - DEC-0125
|
|
35
|
+
from fabric_mandate import EFFECT_CLASSES, OPERATOR_SOURCE_PROTOCOL, is_agent_id # noqa: E402 - DEC-0125, DEC-0126
|
|
36
36
|
from fabric_time import format_rfc3339 # noqa: E402 - the core's (FAA-65)
|
|
37
37
|
from fabric_page_links import ( # noqa: E402 - DEC-0123
|
|
38
38
|
PageLinkError, dashboards_link, input_request_link_findings, page_link_findings, parse_dashboards_link)
|
|
@@ -84,6 +84,7 @@ from fabric_interop_rules import ( # noqa: E402,F401 - the rule layer (FAA-92)
|
|
|
84
84
|
intent_idempotency_key,
|
|
85
85
|
reads_null_usage,
|
|
86
86
|
request_meta,
|
|
87
|
+
job_step,
|
|
87
88
|
action_gate,
|
|
88
89
|
action_refusal_tool_result,
|
|
89
90
|
usage_findings,
|
|
@@ -368,9 +369,12 @@ class JobStore:
|
|
|
368
369
|
def traceparent(self, job_id: str) -> Optional[str]:
|
|
369
370
|
return self._load(job_id)["private"].get("traceparent")
|
|
370
371
|
|
|
371
|
-
def _transition(self, job_id: str, status: str, **fields: Any) -> Dict[str, Any]:
|
|
372
|
+
def _transition(self, job_id: str, status: str, private: Optional[Dict[str, Any]] = None, **fields: Any) -> Dict[str, Any]:
|
|
372
373
|
with self._lock:
|
|
373
374
|
record = self._load(job_id)
|
|
375
|
+
if private is not None and "steps" in private: # in the same file write as the transition
|
|
376
|
+
# Each question replaces the kept steps: a step outlives neither its question nor its answer.
|
|
377
|
+
record["private"]["steps"] = dict(private["steps"])
|
|
374
378
|
job = record["job"]
|
|
375
379
|
if job["status"] in TERMINAL:
|
|
376
380
|
raise InteropError("Job %s is already %s; a terminal job does not change." % (job_id, job["status"]))
|
|
@@ -383,8 +387,14 @@ class JobStore:
|
|
|
383
387
|
def working(self, job_id: str, message: Optional[str] = None) -> Dict[str, Any]:
|
|
384
388
|
return self._transition(job_id, "working", statusMessage=message)
|
|
385
389
|
|
|
386
|
-
def request_input(self, job_id: str, input_requests: Dict[str, Dict[str, Any]], message: Optional[str] = None
|
|
387
|
-
|
|
390
|
+
def request_input(self, job_id: str, input_requests: Dict[str, Dict[str, Any]], message: Optional[str] = None,
|
|
391
|
+
steps: Optional[Dict[str, Dict[str, Any]]] = None) -> Dict[str, Any]:
|
|
392
|
+
"""input_required with MCP elicitation requests, keyed; build them with choice_request / form_request / url_request.
|
|
393
|
+
`steps` (DEC-0125) maps a request key to the approval step it asks for, {effect, spendUsd?, project?}, kept in the
|
|
394
|
+
job's private part so an answer's `_meta.mandate` is read for that step (CallContext.approval_step writes it)."""
|
|
395
|
+
for key, step in (steps or {}).items():
|
|
396
|
+
if key not in (input_requests or {}) or not isinstance(step, dict) or step.get("effect") not in EFFECT_CLASSES:
|
|
397
|
+
raise InteropError("Step %r: an input request's key with its effect class (DEC-0125)." % key)
|
|
388
398
|
if not input_requests:
|
|
389
399
|
raise InteropError("input_required needs at least one input request.")
|
|
390
400
|
for key, request in input_requests.items():
|
|
@@ -392,7 +402,8 @@ class JobStore:
|
|
|
392
402
|
raise InteropError("Input request %r must be a keyed elicitation/create request." % key)
|
|
393
403
|
for finding in input_request_link_findings({"inputRequests": input_requests}): # FAC-SEM-143 (D12)
|
|
394
404
|
raise InteropError("%s: %s" % (finding["instancePath"], finding["message"]))
|
|
395
|
-
return self._transition(job_id, "input_required",
|
|
405
|
+
return self._transition(job_id, "input_required", {"steps": dict(steps or {})},
|
|
406
|
+
inputRequests=input_requests, statusMessage=message)
|
|
396
407
|
|
|
397
408
|
def answer(self, job_id: str, input_responses: Dict[str, Any]) -> Dict[str, Any]:
|
|
398
409
|
"""Apply inputResponses to an input_required job; unknown keys are ignored (MCP Tasks rule).
|
|
@@ -405,8 +416,8 @@ class JobStore:
|
|
|
405
416
|
matched = {key: value for key, value in (input_responses or {}).items()
|
|
406
417
|
if key in job.get("inputRequests", {}) and isinstance(value, dict)
|
|
407
418
|
and value.get("action") in ("accept", "decline", "cancel")}
|
|
408
|
-
if matched:
|
|
409
|
-
self.
|
|
419
|
+
if matched: # the answered question's kept steps go in the same write: none outlives its answer
|
|
420
|
+
self._transition(job_id, "working", {"steps": {}})
|
|
410
421
|
return matched
|
|
411
422
|
|
|
412
423
|
def complete(self, job_id: str, envelope: Dict[str, Any]) -> Dict[str, Any]:
|
|
@@ -468,7 +479,9 @@ class CallContext:
|
|
|
468
479
|
(the caller's own act) or {by, via, caller}, the act to put in the service's audit record, and `decision` the
|
|
469
480
|
operator's decision it took (its `instruction` stays in that record, never in events or a page another party
|
|
470
481
|
reads), else None. `input_records` maps each answered input request to its record when input_actions is on.
|
|
471
|
-
DEC-0125: `mandate` is the operator's mandate the call was taken under (record `via: mandate`), else None.
|
|
482
|
+
DEC-0125: `mandate` is the operator's mandate the call was taken under (record `via: mandate`), else None.
|
|
483
|
+
DEC-0126: `record` carries `source` when the decision or the mandate does (OA-6); like `instruction` it stays in the
|
|
484
|
+
service's own audit record, never an event, a usage report, a log line or a page another party reads (OA-7)."""
|
|
472
485
|
|
|
473
486
|
def __init__(self, server: "McpToolServer", tool: str, arguments: Any, traceparent: str,
|
|
474
487
|
delegation: Optional[Dict[str, Any]] = None, *, tier: Optional[str] = None, caller: Optional[str] = None,
|
|
@@ -491,6 +504,28 @@ class CallContext:
|
|
|
491
504
|
self.server.job_started(handle["job"]["id"], self)
|
|
492
505
|
return handle
|
|
493
506
|
|
|
507
|
+
def approval_step(self, job_id: str, key: str, request: Dict[str, Any], *, effect: str, spend_usd: Any = None,
|
|
508
|
+
project: Optional[str] = None, message: Optional[str] = None) -> Any:
|
|
509
|
+
"""The job's own approval step `key` (interop C3.3, DEC-0125): on a server with agent_id, the mandate this call
|
|
510
|
+
carried (`_meta.mandate`) is read for it (job_step). Covered: answers the record (`via: mandate`, `source` when
|
|
511
|
+
the mandate carries one) and asks nothing; the job goes on as the operator's decision. Otherwise the job waits
|
|
512
|
+
input_required on `request`, its message naming the part not covered (or `message`), and answers None; the step
|
|
513
|
+
is kept so an answer's `_meta.mandate` that covers it is read when input_actions is on."""
|
|
514
|
+
if self.server.jobs is None:
|
|
515
|
+
raise InteropError("This server has no job store.")
|
|
516
|
+
step: Dict[str, Any] = {"message": None}
|
|
517
|
+
if self.server.agent_id is not None:
|
|
518
|
+
caller = (self.delegation or {}).get("caller")
|
|
519
|
+
step = job_step(self.meta.get("mandate"), tier=self.tier, now=format_rfc3339(self.server._clock() * 1000),
|
|
520
|
+
self_id=self.server.agent_id, capability=self.tool, key=key, effect=effect, caller_agent=caller,
|
|
521
|
+
spend_usd=spend_usd, project=project)
|
|
522
|
+
if step["covered"]:
|
|
523
|
+
return step["record"]
|
|
524
|
+
kept = {"effect": effect, **({"spendUsd": spend_usd} if spend_usd is not None else {}),
|
|
525
|
+
**({"project": project} if project is not None else {})}
|
|
526
|
+
self.server.jobs.request_input(job_id, {key: request}, step["message"] or message, steps={key: kept})
|
|
527
|
+
return None
|
|
528
|
+
|
|
494
529
|
|
|
495
530
|
Handler = Callable[[Any, CallContext], Any]
|
|
496
531
|
OnInput = Callable[[str, Dict[str, Any], CallContext], None]
|
|
@@ -626,6 +661,18 @@ class McpToolServer:
|
|
|
626
661
|
self.handlers: Dict[str, Handler] = {}
|
|
627
662
|
self._on_job_started = on_job_started
|
|
628
663
|
self._on_input = on_input
|
|
664
|
+
# DEC-0126 OA-7: a server that reads `source` (a decision's on a relayed action, a mandate's with agent_id) lists
|
|
665
|
+
# operator-source/1 in its well-known protocols; well_known_protocols() hands the list to build_well_known.
|
|
666
|
+
self.reads_source = agent_id is not None
|
|
667
|
+
|
|
668
|
+
def well_known_protocols(self, protocols: Any = ()) -> List[str]:
|
|
669
|
+
"""The well-known `protocols` (DEC-0067) a service on this server lists: `protocols` and, when it reads `source`
|
|
670
|
+
(agent_id given, or a relayed action served), operator-source/1 (DEC-0126 OA-7), each once."""
|
|
671
|
+
listed = list(protocols)
|
|
672
|
+
if (self.reads_source or any(e["action"]["gate"] == "relayed" for e in self.actions.values())) \
|
|
673
|
+
and OPERATOR_SOURCE_PROTOCOL not in listed:
|
|
674
|
+
listed.append(OPERATOR_SOURCE_PROTOCOL)
|
|
675
|
+
return listed
|
|
629
676
|
|
|
630
677
|
def add_tool(self, tool: Dict[str, Any], handler: Handler, action: Optional[Dict[str, Any]] = None, *,
|
|
631
678
|
target: Any = None, human_step_url: Optional[str] = None, effect: Optional[str] = None,
|
|
@@ -734,19 +781,30 @@ class McpToolServer:
|
|
|
734
781
|
return _UNREADABLE
|
|
735
782
|
|
|
736
783
|
def _gate_inputs(self, arguments: Dict[str, Any], headers: Any, tier: str, meta: Dict[str, Any],
|
|
737
|
-
caller: Optional[str]) -> Dict[str, Any]:
|
|
784
|
+
caller: Optional[str], verdict: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
738
785
|
"""D8: each input request an answer would take is an action (id its key, tool fabric.job.get, gate relayed),
|
|
739
|
-
one decision per call; the first refusal refuses the call and nothing is applied.
|
|
786
|
+
one decision per call; the first refusal refuses the call and nothing is applied. DEC-0125: on a server with
|
|
787
|
+
agent_id, a step approval_step kept is read under the call's `_meta.mandate` too (C3.3)."""
|
|
740
788
|
job_id = str(arguments.get("id", ""))
|
|
741
789
|
job = self.jobs.get(job_id)
|
|
790
|
+
steps = self.jobs.private(job_id).get("steps") or {}
|
|
742
791
|
asked = job.get("inputRequests", {}) if job["status"] == "input_required" else {}
|
|
743
792
|
records: Dict[str, Any] = {}
|
|
744
793
|
for key, value in arguments["inputResponses"].items():
|
|
745
794
|
if key not in asked or not isinstance(value, dict) or value.get("action") not in ("accept", "decline", "cancel"):
|
|
746
795
|
continue
|
|
747
796
|
action = {"id": key, "title": "Answer %s" % key, "tool": "fabric.job.get", "gate": "relayed"}
|
|
797
|
+
step, mandate = steps.get(key), {}
|
|
798
|
+
if self.agent_id is not None and isinstance(step, dict):
|
|
799
|
+
# A kept step's mandate is read as approval_step read it, under the job's own capability; the action
|
|
800
|
+
# itself stays fabric.job.get, so it is readable whatever the tool is named.
|
|
801
|
+
capability = self.jobs.private(job_id).get("capability")
|
|
802
|
+
agent = (verdict or {}).get("caller")
|
|
803
|
+
mandate = {"self_id": self.agent_id, "effect": step.get("effect"), "spend_usd": step.get("spendUsd"),
|
|
804
|
+
"project": step.get("project"), "caller_agent": agent if isinstance(agent, str) and agent else None,
|
|
805
|
+
"mandate_capability": capability if isinstance(capability, str) else None}
|
|
748
806
|
gated = self._gate_action(action, "fabric.job.get", {"id": job_id, "inputResponses": {key: value}}, None, None,
|
|
749
|
-
headers, tier, meta, caller)
|
|
807
|
+
headers, tier, meta, caller, mandate)
|
|
750
808
|
if not gated["ok"]:
|
|
751
809
|
return gated
|
|
752
810
|
records[key] = gated["record"]
|
|
@@ -847,7 +905,7 @@ class McpToolServer:
|
|
|
847
905
|
responses = arguments.get("inputResponses")
|
|
848
906
|
if name == "fabric.job.get" and self.input_actions and isinstance(responses, dict) and responses:
|
|
849
907
|
try:
|
|
850
|
-
inputs = self._gate_inputs(arguments, headers, tier, meta, caller)
|
|
908
|
+
inputs = self._gate_inputs(arguments, headers, tier, meta, caller, verdict)
|
|
851
909
|
except UnknownJob:
|
|
852
910
|
return {"jsonrpc": "2.0", "id": rid, "result": unknown_job_result(str(arguments.get("id", "")), traceparent=span)}
|
|
853
911
|
if not inputs["ok"] and "invalid" in inputs:
|
|
@@ -18,7 +18,8 @@ if str(_Path(__file__).resolve().parent) not in _sys.path: # the kit loads from
|
|
|
18
18
|
|
|
19
19
|
from fabric_base import ServiceError # noqa: E402
|
|
20
20
|
from fabric_actions import action_decision, decision_shape # noqa: E402 - DEC-0121
|
|
21
|
-
from fabric_mandate import
|
|
21
|
+
from fabric_mandate import ( # noqa: E402 - DEC-0125, DEC-0126
|
|
22
|
+
EFFECT_CLASSES, OPERATOR_SOURCE_PROTOCOL, mandate_covers, mandate_problem, without_source)
|
|
22
23
|
|
|
23
24
|
PROTOCOL = "fabric-interop/0.1"
|
|
24
25
|
EXTENSION_KEY = "https://fabric.passioncode.ai/agent-contract/extensions/interop/0.1"
|
|
@@ -290,32 +291,80 @@ def reads_null_usage(request_meta: Any) -> bool:
|
|
|
290
291
|
return isinstance(protocols, list) and INTEROP_NULL_USAGE in protocols
|
|
291
292
|
|
|
292
293
|
|
|
293
|
-
def request_meta(traceparent: Optional[str] = None, decision: Any = None, mandate: Any = None
|
|
294
|
+
def request_meta(traceparent: Optional[str] = None, decision: Any = None, mandate: Any = None,
|
|
295
|
+
target_protocols: Any = None) -> Dict[str, Any]:
|
|
294
296
|
"""A request's `_meta` from this kit as a caller: its trace, the interop revisions it reads (DEC-0087), for a
|
|
295
297
|
relayed action the operator's decision (DEC-0121 AP-2; build one with fabric_actions.operator_decision), and the
|
|
296
298
|
operator's mandate for the task the call serves (DEC-0125 MD-3; state it with fabric_mandate.operator_mandate and
|
|
297
299
|
hand it on to the callee with fabric_mandate.hand_on before you send it). Both travel here only, never in a tool's
|
|
298
|
-
arguments; a decision decision.schema.json refuses or a mandate FAC-SEM-144 refuses raises InteropError.
|
|
300
|
+
arguments; a decision decision.schema.json refuses or a mandate FAC-SEM-144 refuses raises InteropError.
|
|
301
|
+
|
|
302
|
+
DEC-0126 OA-7: `target_protocols` is the callee's well-known `protocols`. A decision's or a mandate's `source` is
|
|
303
|
+
sent only when that list names `operator-source/1`; otherwise (no list, another list, a board Project, a session,
|
|
304
|
+
Fabric's `agent.call`, whose directory lists no `protocols`) it is left out of `_meta` and stays in the caller's
|
|
305
|
+
own record, the decision and the mandate given here, which this never changes."""
|
|
299
306
|
meta: Dict[str, Any] = {"protocols": list(PROTOCOLS_READ)}
|
|
307
|
+
reads_source = isinstance(target_protocols, (list, tuple)) and OPERATOR_SOURCE_PROTOCOL in target_protocols
|
|
300
308
|
if parse_traceparent(traceparent):
|
|
301
309
|
meta["traceparent"] = traceparent
|
|
302
310
|
if decision is not None:
|
|
303
311
|
if not decision_shape(decision):
|
|
304
312
|
raise InteropError("The decision is not one decision.schema.json reads; it is not sent.")
|
|
305
|
-
meta["decision"] =
|
|
313
|
+
meta["decision"] = json.loads(json.dumps(decision if reads_source else without_source(decision)))
|
|
306
314
|
if mandate is not None:
|
|
307
315
|
problem = mandate_problem(mandate)
|
|
308
316
|
if problem:
|
|
309
317
|
raise InteropError("The mandate is not one FAC-SEM-144 reads (%s); it is not sent." % problem)
|
|
310
|
-
meta["mandate"] = json.loads(json.dumps(mandate))
|
|
318
|
+
meta["mandate"] = json.loads(json.dumps(mandate if reads_source else without_source(mandate)))
|
|
311
319
|
return meta
|
|
312
320
|
|
|
313
321
|
|
|
322
|
+
def job_step(mandate: Any, *, tier: Any, now: Any, self_id: Any, capability: Any, key: Any, effect: Any,
|
|
323
|
+
caller_agent: Any = None, spend_usd: Any = None, project: Any = None) -> Dict[str, Any]:
|
|
324
|
+
"""A job's own approval step under the mandate its start call carried (interop C3.3, DEC-0125 MD-3, MD-4;
|
|
325
|
+
conformance: "a job's approval step included"): the step is the call {capability: the job's, action: the input
|
|
326
|
+
request's `key`, effect, spendUsd?, project?} that mandate_covers reads, with `self_id` the service's agent id and
|
|
327
|
+
`caller_agent` only an agent id it authenticated for the start call.
|
|
328
|
+
|
|
329
|
+
- {"covered": True, "record"}: the step goes on as the operator's decision, no input request; `record` is
|
|
330
|
+
mandate_covers' recordAs (`via: mandate`, the task, the chain and `source` when the mandate carries one).
|
|
331
|
+
- {"covered": False, "message", "detail"}: the step waits input_required, `message` its statusMessage naming the
|
|
332
|
+
part not covered, or saying the mandate expired or cannot be read; never the operator's words or `source`.
|
|
333
|
+
- {"covered": False, "message": None}: no mandate, or a T2 caller's (MD-5): ask as before; with `fault` when the
|
|
334
|
+
service's own clock does not read."""
|
|
335
|
+
if mandate is None:
|
|
336
|
+
return {"covered": False, "message": None}
|
|
337
|
+
if not (isinstance(effect, str) and effect in EFFECT_CLASSES):
|
|
338
|
+
raise InteropError("A job's approval step names its effect class (one of %s; DEC-0125)." % ", ".join(EFFECT_CLASSES))
|
|
339
|
+
call: Dict[str, Any] = {"capability": capability, "action": key, "effect": effect}
|
|
340
|
+
if isinstance(spend_usd, (int, float)) and not isinstance(spend_usd, bool):
|
|
341
|
+
call["spendUsd"] = spend_usd
|
|
342
|
+
if isinstance(project, str):
|
|
343
|
+
call["project"] = project
|
|
344
|
+
covers: Dict[str, Any] = {"mandate": mandate, "tier": tier, "now": now, "self": self_id, "call": call}
|
|
345
|
+
if isinstance(caller_agent, str) and caller_agent:
|
|
346
|
+
covers["callerAgent"] = caller_agent
|
|
347
|
+
answer = mandate_covers(covers)
|
|
348
|
+
if answer.get("ok") is True:
|
|
349
|
+
return {"covered": True, "record": answer["recordAs"]}
|
|
350
|
+
if "ignored" in answer or "fault" in answer:
|
|
351
|
+
return dict({"covered": False, "message": None}, **({"fault": answer["fault"]} if "fault" in answer else {}))
|
|
352
|
+
detail = answer["data"]["detail"]
|
|
353
|
+
if detail == "mandate-expired":
|
|
354
|
+
said = "The operator's mandate for this task expired before this step"
|
|
355
|
+
elif detail.startswith("outside-mandate"):
|
|
356
|
+
said = "The operator's mandate does not cover this step (%s)" % detail
|
|
357
|
+
else:
|
|
358
|
+
said = "The operator's mandate cannot be read for this step (%s)" % detail
|
|
359
|
+
return {"covered": False, "detail": detail,
|
|
360
|
+
"message": said + ": answer it, or call fabric.job.get with a mandate that covers it in _meta.mandate."}
|
|
361
|
+
|
|
362
|
+
|
|
314
363
|
def action_gate(action: Any, *, tier: Any, caller: Any, meta: Any = None, target: Optional[str] = None,
|
|
315
364
|
elicited: bool = False, approval: Any = None, action_digest: Optional[str] = None,
|
|
316
365
|
human_step_url: Optional[str] = None, now: Any = None, self_id: Optional[str] = None,
|
|
317
366
|
effect: Optional[str] = None, caller_agent: Optional[str] = None, spend_usd: Any = None,
|
|
318
|
-
project: Optional[str] = None) -> Dict[str, Any]:
|
|
367
|
+
project: Optional[str] = None, mandate_capability: Optional[str] = None) -> Dict[str, Any]:
|
|
319
368
|
"""One call of a declared action, before its handler runs (DEC-0121 AP-2, AP-4): fabric_actions.action_decision over
|
|
320
369
|
the call's `_meta.decision` (read from `meta` only, never from arguments, decision D11), the service's authenticated
|
|
321
370
|
`caller` (fabric_actions.action_caller), `target` (the object the call acts on), `elicited` (the service's own
|
|
@@ -327,7 +376,8 @@ def action_gate(action: Any, *, tier: Any, caller: Any, meta: Any = None, target
|
|
|
327
376
|
DEC-0125: a service that names its own agent id (`self_id`) reads the call's `_meta.mandate` when it carries no
|
|
328
377
|
decision, given `effect` (the action's effect class, required: without it the call is the service's fault
|
|
329
378
|
effect-missing), `caller_agent` (only an agent id the service authenticated: a delegation's caller, its own
|
|
330
|
-
per-agent credential; never a name the call carries), `spend_usd` (its own estimate) and `
|
|
379
|
+
per-agent credential; never a name the call carries), `spend_usd` (its own estimate), `project`, and `mandate_capability` (a job's kept step: the job's capability
|
|
380
|
+
the mandate is read under, not the action's tool). A covered call
|
|
331
381
|
answers {ok: True, record: {by: operator, via: mandate, task, chain, caller}, decision: None, mandate}; without
|
|
332
382
|
`self_id` the mandate is not read and the call is answered as before it existed."""
|
|
333
383
|
import fabric_delegation as _fd # noqa: E402 - beside this file: DL-19
|
|
@@ -339,7 +389,8 @@ def action_gate(action: Any, *, tier: Any, caller: Any, meta: Any = None, target
|
|
|
339
389
|
inputs[name] = value
|
|
340
390
|
if self_id is not None and isinstance(meta, dict) and "mandate" in meta:
|
|
341
391
|
inputs.update({"mandate": meta["mandate"], "self": self_id})
|
|
342
|
-
for name, value in (("effect", effect), ("callerAgent", caller_agent), ("spendUsd", spend_usd), ("project", project)
|
|
392
|
+
for name, value in (("effect", effect), ("callerAgent", caller_agent), ("spendUsd", spend_usd), ("project", project),
|
|
393
|
+
("mandateCapability", mandate_capability)):
|
|
343
394
|
if value is not None:
|
|
344
395
|
inputs[name] = value
|
|
345
396
|
answer = action_decision(inputs)
|
|
@@ -8,9 +8,14 @@ the task, narrowed at each hand-over. Part of the service kit (layer L2); the tw
|
|
|
8
8
|
- ``mandate_problem(mandate)``: ``mandate-malformed``, ``mandate-widened`` or None (FAC-SEM-144);
|
|
9
9
|
``mandate_shape(mandate)``: ``mandate.schema.json`` alone; ``mandate_rules(kind, value)``: the findings.
|
|
10
10
|
- ``scope_within(child, parent)``, ``effective_scope(mandate)``, ``effect_closure(ceiling)``.
|
|
11
|
-
- ``operator_mandate(task, instruction, scope, lead,
|
|
12
|
-
operator's own words (MD-1); ``hand_on(mandate, callee, at, narrower=None)``: the one it sends to the agent
|
|
13
|
-
(MD-2), never wider. Send it in ``_meta.mandate`` (``fabric_interop.request_meta(mandate=)``).
|
|
11
|
+
- ``operator_mandate(task, instruction, scope, lead, ..., source=None)``: the mandate the agent leading a task states
|
|
12
|
+
from the operator's own words (MD-1); ``hand_on(mandate, callee, at, narrower=None)``: the one it sends to the agent
|
|
13
|
+
it calls (MD-2), never wider. Send it in ``_meta.mandate`` (``fabric_interop.request_meta(mandate=)``).
|
|
14
|
+
- DEC-0126: ``source`` ({project, session or thread}) names where the operator said the words when they reached this
|
|
15
|
+
agent relayed from another of their agents (OA-2); ``source_shape`` reads it, ``mandate_covers`` returns it in
|
|
16
|
+
``recordAs`` (OA-6), ``hand_on`` keeps it unchanged, and ``without_source`` drops it from a copy for a target that
|
|
17
|
+
does not list ``operator-source/1`` (OA-7; ``request_meta(target_protocols=)`` does it for you). Like
|
|
18
|
+
``instruction`` it stays in the receiver's own record: never an event, a usage report, a log line or a page.
|
|
14
19
|
|
|
15
20
|
The kit has no JSON Schema validator: the shape is written here from the schema, lengths in code points and patterns
|
|
16
21
|
over the whole string, as the contract's validator reads them. Never raises on what it reads. Standard library only.
|
|
@@ -45,6 +50,10 @@ EFFECT_CLASSES = ("none", "draft", "publish", "send", "reply", "charge", "delete
|
|
|
45
50
|
_READS_AND_DRAFTS = ("none", "draft")
|
|
46
51
|
_NEVER_BY_MANDATE = "change-policy" # approving one's own promotion, grant, checker rule or policy change (AP-5)
|
|
47
52
|
_MEMBERS = ("kind", "by", "task", "instruction", "at", "expiresAt", "scope", "chain")
|
|
53
|
+
_OPTIONAL = ("source",) # DEC-0126 OA-2
|
|
54
|
+
_SOURCE_MEMBERS = ("project", "session", "thread")
|
|
55
|
+
#: The register name of the protocol a target lists when it reads `source` (DEC-0126 OA-7).
|
|
56
|
+
OPERATOR_SOURCE_PROTOCOL = "operator-source/1"
|
|
48
57
|
_SCOPE_MEMBERS = ("capabilities", "actions", "effects", "perCallMaxUsd", "projects")
|
|
49
58
|
_STEP_MEMBERS = ("agent", "at", "scope")
|
|
50
59
|
_AGENT_ID = re.compile(r"[a-z][a-z0-9-]{1,62}(\.[a-z][a-z0-9-]{0,31})?", re.ASCII) # common.schema.json agentId
|
|
@@ -97,6 +106,24 @@ def _scope_shape(scope: Any) -> bool:
|
|
|
97
106
|
return all(checks[key](value) for key, value in scope.items())
|
|
98
107
|
|
|
99
108
|
|
|
109
|
+
def source_shape(source: Any) -> bool:
|
|
110
|
+
"""`mandate.schema.json#/$defs/source` (DEC-0126 OA-2), which `decision.schema.json` refers to: an object of
|
|
111
|
+
`project` and `session` or `thread` (or both), each 1..128 characters of `[A-Za-z0-9][A-Za-z0-9._:-]*`, nothing else."""
|
|
112
|
+
if not isinstance(source, dict) or any(key not in _SOURCE_MEMBERS for key in source) or "project" not in source:
|
|
113
|
+
return False
|
|
114
|
+
if "session" not in source and "thread" not in source:
|
|
115
|
+
return False
|
|
116
|
+
return all(_string(value, 1, 128) and _matches(_PROJECT, value) for value in source.values())
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def without_source(value: Any) -> Any:
|
|
120
|
+
"""A copy of a decision or a mandate without `source`, for a target that does not list `operator-source/1`
|
|
121
|
+
(DEC-0126 OA-7): the caller keeps the original in its own record (OA-6). Anything else is returned unchanged."""
|
|
122
|
+
if isinstance(value, dict) and "source" in value:
|
|
123
|
+
return {key: item for key, item in value.items() if key != "source"}
|
|
124
|
+
return value
|
|
125
|
+
|
|
126
|
+
|
|
100
127
|
def _time(value: Any) -> bool:
|
|
101
128
|
# Every value the strict reading accepts, ajv-formats' date-time accepts too, and mandate_problem reads the times
|
|
102
129
|
# strictly next, so reading them strictly here gives mandate_problem's answer (mandate-malformed either way).
|
|
@@ -115,9 +142,9 @@ def mandate_shape(mandate: Any) -> bool:
|
|
|
115
142
|
are read strictly (the contract's strictTime): ajv-formats' date-time also admits a space for T, an offset without
|
|
116
143
|
its colon and a leap second, which this refuses, as fabric_actions.decision_shape does (0.50.0 brief D5); FAC-SEM-144
|
|
117
144
|
reads them strictly next, so mandate_problem answers as the contract does either way."""
|
|
118
|
-
if not isinstance(mandate, dict) or any(key not in _MEMBERS for key in mandate):
|
|
145
|
+
if not isinstance(mandate, dict) or any(key not in _MEMBERS and key not in _OPTIONAL for key in mandate):
|
|
119
146
|
return False
|
|
120
|
-
if any(key not in mandate for key in _MEMBERS):
|
|
147
|
+
if any(key not in mandate for key in _MEMBERS) or ("source" in mandate and not source_shape(mandate["source"])):
|
|
121
148
|
return False
|
|
122
149
|
chain = mandate["chain"]
|
|
123
150
|
return (isinstance(mandate["kind"], str) and mandate["kind"] == "mandate/1"
|
|
@@ -145,7 +172,7 @@ def _ceiling_within(child: Any, parent: Any) -> bool:
|
|
|
145
172
|
|
|
146
173
|
|
|
147
174
|
def _naming_within(child: Dict[str, Any], parent: Dict[str, Any]) -> bool:
|
|
148
|
-
"""DEC-0136 MD-2a
|
|
175
|
+
"""DEC-0136 MD-2a: a naming list (`capabilities`, `actions`) left out names no
|
|
149
176
|
relayed action or external effect (MD-3), so a hand-over never adds one the scope in force leaves out; `actions`
|
|
150
177
|
may be added where `capabilities` is listed, and narrows the relayed actions those name."""
|
|
151
178
|
caps, acts = get(parent, "capabilities"), get(parent, "actions")
|
|
@@ -222,7 +249,8 @@ def mandate_covers(inputs: Any) -> Dict[str, Any]:
|
|
|
222
249
|
(more than 60 s after the clock) or `mandate-expired`.
|
|
223
250
|
- `operator-decision-required` with `data.detail` `outside-mandate: <action|capability|effect|spend|project>` for the
|
|
224
251
|
first part the scope in force does not cover; the operator is asked for that part alone (MD-4).
|
|
225
|
-
- `{"ok": True, "recordAs": {"by": "operator", "via": "mandate", "task", "chain"}}` otherwise
|
|
252
|
+
- `{"ok": True, "recordAs": {"by": "operator", "via": "mandate", "task", "chain", "source"?}}` otherwise, `source`
|
|
253
|
+
being where the operator said the words when the mandate carries it (DEC-0126 OA-6).
|
|
226
254
|
|
|
227
255
|
`callerAgent` is passed only when the callee authenticated the caller's agent id (a per-agent credential, a
|
|
228
256
|
delegation token's outermost actor, the hub's attested origin); without one the chain's end alone binds it. A
|
|
@@ -271,13 +299,17 @@ def mandate_covers(inputs: Any) -> Dict[str, Any]:
|
|
|
271
299
|
project, projects = get(call, "project"), get(scope, "projects")
|
|
272
300
|
if project is not MISSING and projects is not MISSING and not _member_of(projects, project):
|
|
273
301
|
return _outside("outside-mandate: project")
|
|
274
|
-
|
|
302
|
+
record: Dict[str, Any] = {"by": "operator", "via": "mandate", "task": mandate["task"], "chain": holders}
|
|
303
|
+
if "source" in mandate: # where the operator said it travels into the record, as the reason the callee acted (OA-6)
|
|
304
|
+
record["source"] = dict(mandate["source"])
|
|
305
|
+
return {"ok": True, "recordAs": record}
|
|
275
306
|
|
|
276
307
|
|
|
277
308
|
def hand_on(mandate: Dict[str, Any], callee: str, at: str, narrower: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
278
309
|
"""The mandate an agent sends with a call (DEC-0125 MD-2): the chain gains the agent it calls, at `at`, with
|
|
279
310
|
`narrower` when it hands on less. Raises ServiceError when the result would not be a valid mandate (a wider scope,
|
|
280
|
-
a fifth agent, an agent already in it, a time before the last step or at the expiry). `mandate` is not changed
|
|
311
|
+
a fifth agent, an agent already in it, a time before the last step or at the expiry). `mandate` is not changed;
|
|
312
|
+
its `source` travels unchanged (DEC-0126: it never changes down the chain)."""
|
|
281
313
|
chain = list(mandate["chain"]) if isinstance(mandate, dict) and isinstance(mandate.get("chain"), list) else []
|
|
282
314
|
step: Dict[str, Any] = {"agent": callee, "at": at}
|
|
283
315
|
if _truthy(narrower):
|
|
@@ -291,25 +323,33 @@ def hand_on(mandate: Dict[str, Any], callee: str, at: str, narrower: Optional[Di
|
|
|
291
323
|
|
|
292
324
|
|
|
293
325
|
def operator_mandate(task: str, instruction: str, scope: Dict[str, Any], lead: str, *, at: Optional[str] = None,
|
|
294
|
-
expires_at: Optional[str] = None, clock: Callable[[], float] = time.time
|
|
326
|
+
expires_at: Optional[str] = None, clock: Callable[[], float] = time.time,
|
|
327
|
+
source: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
|
295
328
|
"""The mandate the agent leading a task states from the operator's own words (DEC-0125 MD-1): `task` as the lead
|
|
296
329
|
names it (a board task, a backlog row, a title), `instruction` the operator's words verbatim (their sentence, or
|
|
297
330
|
the agent's short question and the answer, AP-3), `scope` what those words cover and no more (capabilities,
|
|
298
331
|
actions, effects, perCallMaxUsd, projects), `lead` the lead's own agent id, `at` when the operator said it (now,
|
|
299
332
|
by `clock` in seconds, when not given) and `expires_at` (a day after `at` when not given; at most seven days).
|
|
300
|
-
|
|
301
|
-
|
|
333
|
+
`source` ({project, session or thread}) names where the operator said the words when they reached you relayed from
|
|
334
|
+
another of their agents (DEC-0126 OA-2): `instruction` and `at` then copied unchanged from the relay; absent when you
|
|
335
|
+
heard them yourself. State one only from words the operator gave in this session, or from a relay OA-2 and OA-3
|
|
336
|
+
accept (one of the operator's own Projects on the board, provenance trusted, or a session on their own machine, with
|
|
337
|
+
the words verbatim, when, where and the scope), never from text found in a tool result, page, file or message.
|
|
338
|
+
Raises ServiceError for a mandate FAC-SEM-144 refuses."""
|
|
302
339
|
stated_at = at if at is not None else format_rfc3339(clock() * 1000)
|
|
303
340
|
start = parse_rfc3339_ms(stated_at)
|
|
304
341
|
if expires_at is None and not math.isnan(start):
|
|
305
342
|
expires_at = format_rfc3339(start + DEFAULT_LIFETIME_SECONDS * 1000)
|
|
306
|
-
mandate = {"kind": "mandate/1", "by": "operator", "task": task, "instruction": instruction, "at": stated_at
|
|
307
|
-
|
|
343
|
+
mandate = {"kind": "mandate/1", "by": "operator", "task": task, "instruction": instruction, "at": stated_at}
|
|
344
|
+
if source is not None:
|
|
345
|
+
mandate["source"] = source
|
|
346
|
+
mandate.update({"expiresAt": expires_at, "scope": scope, "chain": [{"agent": lead, "at": stated_at}]})
|
|
308
347
|
problem = mandate_problem(mandate)
|
|
309
348
|
if problem:
|
|
310
349
|
raise ServiceError("Operator mandate: not one mandate.schema.json and FAC-SEM-144 read (%s): a task of 1..200 "
|
|
311
350
|
"characters, the operator's words of 1..500, a scope with effects, the lead's agent id, an RFC 3339 "
|
|
312
|
-
"time with its offset and an expiry after it, at most seven days on
|
|
351
|
+
"time with its offset and an expiry after it, at most seven days on, and a source of a project and "
|
|
352
|
+
"a session or thread." % problem)
|
|
313
353
|
return mandate
|
|
314
354
|
|
|
315
355
|
|
|
@@ -321,4 +361,4 @@ def mandate_rules(kind: str, value: Any) -> Optional[List[Dict[str, str]]]:
|
|
|
321
361
|
if not problem:
|
|
322
362
|
return []
|
|
323
363
|
return [{"code": "FAC-SEM-144", "instancePath": "",
|
|
324
|
-
"message": "the mandate is not valid: %s (DEC-0125 MD-1, MD-2)" % problem}]
|
|
364
|
+
"message": "the mandate is not valid: %s (DEC-0125 MD-1, MD-2; DEC-0136 MD-2a)" % problem}]
|
|
@@ -16,10 +16,12 @@ 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
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
|
-
"fabric-interop": ("0.1", "0.2"), "fabric-local-channel": ("0.1",), "fabric-local-runner": ("0.1",),
|
|
19
|
+
"fabric-interop": ("0.1", "0.2"), "fabric-local-channel": ("0.1",), "fabric-local-runner": ("0.1",), "fabric-machine-report": ("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",),
|
|
22
|
-
"fabric-settings-backup": ("1",), "heartbeat": ("1",), "learning-note": ("1",), "
|
|
22
|
+
"fabric-settings-backup": ("1",), "heartbeat": ("1",), "learning-note": ("1",), "machine-report": ("1",), "mandate": ("1",), "memory": ("0.1",), "node-summary": ("1",),
|
|
23
|
+
"operator-source": ("1",), "org-authority": ("1",), "org-authority-draft": ("1",), "org-key-set": ("1",), "org-key-set-draft": ("1",),
|
|
24
|
+
"org-membership": ("1",), "org-root-registration": ("1",), "pairing-message": ("1",),
|
|
23
25
|
"pipeline": ("0.1",), "repair-request": ("1",), "repair-result": ("1",), "revocation-delta": ("1",), "session-handoff": ("1",), "service-load": ("1",), "share-registration": ("1",),
|
|
24
26
|
"share-registration-receipt": ("1",), "support-quality-daily": ("1",), "window-lease": ("1",),
|
|
25
27
|
}
|