@passioncode-ai/passioncode 0.2.2 → 0.2.5

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.
Files changed (24) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +35 -0
  3. package/family.json +1 -1
  4. package/package.json +1 -1
  5. package/payload/.claude-plugin/marketplace.json +2 -2
  6. package/payload/manifest.json +8 -8
  7. package/payload/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
  8. package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +3 -3
  9. package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/adapt_project.py +2 -2
  10. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +4 -3
  11. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/estate-record.md +35 -6
  12. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +58 -16
  13. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +1 -1
  14. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-estate.mjs +129 -43
  15. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-service.mjs +2 -1
  16. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_estate.py +192 -50
  17. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_service.py +4 -3
  18. package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +3 -3
  19. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +1 -1
  20. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +4 -3
  21. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +1 -1
  22. package/payload/plugins/passioncode/.claude-plugin/plugin.json +1 -1
  23. package/payload/plugins/passioncode/hooks/repo-rules.js +9 -4
  24. package/payload/plugins/passioncode/skills/working-in-passioncode/SKILL.md +9 -0
@@ -151,6 +151,84 @@ def _list(value: Any) -> List[Dict[str, Any]]:
151
151
  return [item for item in value if isinstance(item, dict)] if isinstance(value, list) else []
152
152
 
153
153
 
154
+ def _indexed(value: Any) -> List[Tuple[Dict[str, Any], int]]:
155
+ """The object items of an array with their index in the array as written, so an instancePath names the item it
156
+ flags even when a non-object precedes it (the contract's ``indexed``)."""
157
+ return [(item, i) for i, item in enumerate(value) if isinstance(item, dict)] if isinstance(value, list) else []
158
+
159
+
160
+ class _Unshowable(Exception):
161
+ pass
162
+
163
+
164
+ def _strict_string(value: Any) -> str:
165
+ """String(value), raising where JavaScript's String() throws: an object whose own ``toString`` is not a function
166
+ (every own member of parsed JSON is data), directly or inside an array it joins."""
167
+ if isinstance(value, dict):
168
+ if "toString" in value:
169
+ raise _Unshowable()
170
+ return "[object Object]"
171
+ if isinstance(value, list):
172
+ return ",".join("" if item is None else _strict_string(item) for item in value)
173
+ return _js_string(value)
174
+
175
+
176
+ def _json_stringify(value: Any) -> str:
177
+ """JSON.stringify for a parsed JSON value: members in their order, numbers as JavaScript writes them."""
178
+ if isinstance(value, dict):
179
+ return "{" + ",".join(_delegation._string(k) + ":" + _json_stringify(v) for k, v in value.items()) + "}"
180
+ if isinstance(value, list):
181
+ return "[" + ",".join(_json_stringify(v) for v in value) + "]"
182
+ if value is None:
183
+ return "null"
184
+ if isinstance(value, bool):
185
+ return "true" if value else "false"
186
+ if _number(value):
187
+ text = _js_string(value)
188
+ return "null" if text in ("NaN", "Infinity", "-Infinity") else text
189
+ return _delegation._string(value)
190
+
191
+
192
+ def _show(value: Any) -> str:
193
+ """The contract's ``show``: String(value), or the value's JSON where String() would throw."""
194
+ try:
195
+ return _strict_string(value)
196
+ except _Unshowable:
197
+ return _json_stringify(value)
198
+
199
+
200
+ def _key_of(*parts: Any) -> str:
201
+ """The contract's ``keyOf``: a collision-free key over JSON values, JSON.stringify of the parts (an absent value
202
+ is null inside an array, as JavaScript writes it)."""
203
+ return _json_stringify([None if part is _MISSING else part for part in parts])
204
+
205
+
206
+ #: DEC-0082: the personal context, and an organization's context ``org:<orgId>`` (the contract's ``context-route.ts``).
207
+ PERSONAL_CONTEXT = "personal"
208
+ ORG_CONTEXT_PREFIX = "org:"
209
+ _ORG_ID = re.compile(r"[A-Za-z0-9][A-Za-z0-9_-]{0,127}")
210
+
211
+
212
+ def parse_context(value: Any) -> Optional[Dict[str, str]]:
213
+ """The context ``value`` names, or None when it names none: ``personal`` exactly, or ``org:`` and an organization id
214
+ (``[A-Za-z0-9][A-Za-z0-9_-]{0,127}``). Compared as written: ``Personal``, ``ORG:x`` or a trailing newline name no
215
+ context. Never raises."""
216
+ if not isinstance(value, str):
217
+ return None
218
+ if value == PERSONAL_CONTEXT:
219
+ return {"kind": "personal"}
220
+ if not value.startswith(ORG_CONTEXT_PREFIX):
221
+ return None
222
+ org = value[len(ORG_CONTEXT_PREFIX):]
223
+ return {"kind": "org", "org": org} if _ORG_ID.fullmatch(org) else None
224
+
225
+
226
+ def _context_of(entry: Any) -> Any:
227
+ """A placement's or a secret placement's context: its ``context``, ``personal`` when absent (DEC-0082)."""
228
+ context = _get(entry, "context")
229
+ return PERSONAL_CONTEXT if context is _MISSING else context
230
+
231
+
154
232
  def _b64url_bytes(text: str) -> bytes:
155
233
  """Buffer.from(text, "base64url") for text of the base64url alphabet: a dangling sixth bit group is ignored."""
156
234
  return _delegation._b64decode(text)
@@ -514,7 +592,7 @@ def _signing_credential(credential: Any, path: str, flag) -> None:
514
592
 
515
593
 
516
594
  def _revocation_key(entry: Dict[str, Any]) -> str:
517
- return "%s %s" % (_s(entry, "kind"), _s(entry, "ref"))
595
+ return "%s %s" % (_show(_get(entry, "kind")), _show(_get(entry, "ref")))
518
596
 
519
597
 
520
598
  def _lt(a: float, b: float) -> bool:
@@ -610,11 +688,13 @@ def verify_estate_record(record: Any, *, at: Any, parent: Any = _MISSING, author
610
688
  return {"ok": True, "digest": digest, "revision": record["revision"], "signedBy": credential["credentialId"], "signCount": assertion["signCount"]}
611
689
 
612
690
 
613
- def placement_trust(record: Dict[str, Any], machine_id: str, agent_ref: str) -> Optional[Dict[str, Any]]:
614
- """What a package verifier (DEC-0051) trusts for a package placed on a machine: the publisher keys the placement
615
- names, from the record's `publisherKeys`. None when the record places no such agent on that machine."""
691
+ def placement_trust(record: Dict[str, Any], machine_id: str, agent_ref: str, context: str = PERSONAL_CONTEXT) -> Optional[Dict[str, Any]]:
692
+ """What a package verifier (DEC-0051) trusts for a package placed on a machine in a context (DEC-0082; ``personal``
693
+ when not given, so a caller written before contexts is unchanged): the publisher keys the placement names, from the
694
+ record's `publisherKeys`. None when the record places no such agent on that machine in that context."""
616
695
  machine = next((m for m in _list(_get(record, "machines")) if _same(_get(m, "id"), machine_id)), None)
617
- placement = next((p for p in _list(_get(machine, "packages")) if _same(_get(p, "agentRef"), agent_ref)), None)
696
+ placement = next((p for p in _list(_get(machine, "packages"))
697
+ if _same(_get(p, "agentRef"), agent_ref) and _same(_context_of(p), context)), None)
618
698
  if placement is None:
619
699
  return None
620
700
  return {"publisher": [k for k in _list(_get(record, "publisherKeys")) if _same(_get(k, "kid"), _get(placement, "publisherKid"))]}
@@ -720,7 +800,7 @@ def _revision_chain(value: Dict[str, Any]) -> List[Dict[str, str]]:
720
800
  flag("/next/estate", "a revision continues its parent's estate")
721
801
  expected = _js_to_number(_get(parent, "revision")) + 1
722
802
  if not _same(_get(nxt, "revision"), expected):
723
- flag("/next/revision", "the revision after %s is %s: a revision never skips or repeats one" % (_s(parent, "revision"), _js_string(expected)))
803
+ flag("/next/revision", "the revision after %s is %s: a revision never skips or repeats one" % (_show(_get(parent, "revision")), _js_string(expected)))
724
804
  try:
725
805
  parent_digest: Any = estate_record_digest(parent)
726
806
  except TypeError:
@@ -730,15 +810,15 @@ def _revision_chain(value: Dict[str, Any]) -> List[Dict[str, str]]:
730
810
  if _lt(estate_time(_get(nxt, "issuedAt")), estate_time(_get(parent, "issuedAt"))):
731
811
  flag("/next/issuedAt", "a revision is never issued before its parent")
732
812
  kept = {_revocation_key(e) for e in _list(_get(nxt, "revocations"))}
733
- for index, entry in enumerate(_list(_get(parent, "revocations"))):
813
+ for entry, index in _indexed(_get(parent, "revocations")):
734
814
  if _revocation_key(entry) not in kept:
735
- flag("/parent/revocations/%d" % index, "the %s revocation of %s is dropped: a revision keeps every revocation before it" % (_s(entry, "kind"), _s(entry, "ref")))
815
+ flag("/parent/revocations/%d" % index, "the %s revocation of %s is dropped: a revision keeps every revocation before it" % (_show(_get(entry, "kind")), _show(_get(entry, "ref"))))
736
816
  return findings
737
817
 
738
818
 
739
819
  def _valid_device_key(key: Dict[str, Any]) -> bool:
740
820
  public = _get(key, "publicKey")
741
- raw = _node_base64("" if public is _MISSING or public is None else _js_string(public))
821
+ raw = _node_base64("" if public is _MISSING or public is None else _show(public))
742
822
  if _get(key, "alg") == "ed25519":
743
823
  return len(raw) == 32 and not _delegation.is_small_order_key(raw)
744
824
  return is_valid_p256_key(raw)
@@ -748,10 +828,10 @@ def _keys(value: Dict[str, Any], flag) -> None:
748
828
  ids: List[Any] = []
749
829
  hub_kids: List[Any] = []
750
830
  hub_keys: List[Any] = []
751
- for m, machine in enumerate(_list(_get(value, "machines"))):
831
+ for machine, m in _indexed(_get(value, "machines")):
752
832
  at = "/machines/%d" % m
753
833
  if _contains(ids, _get(machine, "id")):
754
- flag("%s/id" % at, "machine %s is listed twice" % _s(machine, "id"))
834
+ flag("%s/id" % at, "machine %s is listed twice" % _show(_get(machine, "id")))
755
835
  ids.append(_get(machine, "id"))
756
836
  device_keys = _list(_get(machine, "deviceKeys"))
757
837
  if len([k for k in device_keys if _get(k, "use") == "sig"]) != 1:
@@ -759,8 +839,8 @@ def _keys(value: Dict[str, Any], flag) -> None:
759
839
  if len([k for k in device_keys if _get(k, "use") == "kem"]) > 1:
760
840
  flag("%s/deviceKeys" % at, "a machine has at most one key-agreement device key, <id>#kem")
761
841
  seen: List[Any] = []
762
- for k, key in enumerate(device_keys):
763
- if not _same(_get(key, "kid"), "%s#%s" % (_s(machine, "id"), _s(key, "use"))):
842
+ for key, k in _indexed(_get(machine, "deviceKeys")):
843
+ if not _same(_get(key, "kid"), "%s#%s" % (_show(_get(machine, "id")), _show(_get(key, "use")))):
764
844
  flag("%s/deviceKeys/%d/kid" % (at, k), "a device key is listed as <machine id>#sig or <machine id>#kem, by its role")
765
845
  if not _valid_device_key(key):
766
846
  flag("%s/deviceKeys/%d/publicKey" % (at, k), "an Ed25519 key is never of small order (DEC-0045) and a P-256 key is a valid point (SEC 1 §3.2.2)")
@@ -770,7 +850,7 @@ def _keys(value: Dict[str, Any], flag) -> None:
770
850
  hub = _get(machine, "hubKey")
771
851
  if isinstance(hub, dict):
772
852
  if _contains(hub_kids, _get(hub, "kid")):
773
- flag("%s/hubKey/kid" % at, "hub key %s is certified for two machines: one kid per machine" % _s(hub, "kid"))
853
+ flag("%s/hubKey/kid" % at, "hub key %s is certified for two machines: one kid per machine" % _show(_get(hub, "kid")))
774
854
  hub_kids.append(_get(hub, "kid"))
775
855
  if _contains(hub_keys, _get(hub, "x")):
776
856
  flag("%s/hubKey/x" % at, "one hub public key is certified for two machines under two kids: one key per machine")
@@ -780,49 +860,55 @@ def _keys(value: Dict[str, Any], flag) -> None:
780
860
  if _delegation.is_small_order_key(raw):
781
861
  flag("%s/hubKey/x" % at, "a hub key of small order verifies forged tokens and is never certified (DEC-0045)")
782
862
  credentials: List[Any] = []
783
- for index, authority in enumerate(_list(_get(value, "authorities"))):
863
+ for authority, index in _indexed(_get(value, "authorities")):
784
864
  if _contains(credentials, _get(authority, "credentialId")):
785
865
  flag("/authorities/%d/credentialId" % index, "a passkey is listed once")
786
866
  credentials.append(_get(authority, "credentialId"))
787
- if "problem" in credential_key(_get(authority, "publicKey"), _s(authority, "alg")):
867
+ if "problem" in credential_key(_get(authority, "publicKey"), _show(_get(authority, "alg"))):
788
868
  flag("/authorities/%d/publicKey" % index, "a passkey's public key is a valid P-256 point for ES256 or an Ed25519 key of full order for EdDSA")
789
869
 
790
870
 
791
871
  def _placements(value: Dict[str, Any], flag) -> None:
792
872
  kids: List[Any] = []
793
- for index, key in enumerate(_list(_get(value, "publisherKeys"))):
873
+ for key, index in _indexed(_get(value, "publisherKeys")):
794
874
  if _contains(kids, _get(key, "kid")):
795
- flag("/publisherKeys/%d/kid" % index, "publisher key %s is pinned twice" % _s(key, "kid"))
875
+ flag("/publisherKeys/%d/kid" % index, "publisher key %s is pinned twice" % _show(_get(key, "kid")))
796
876
  kids.append(_get(key, "kid"))
797
- for m, machine in enumerate(_list(_get(value, "machines"))):
798
- agents: List[Any] = []
799
- for p, placement in enumerate(_list(_get(machine, "packages"))):
877
+ for machine, m in _indexed(_get(value, "machines")):
878
+ agents: List[str] = []
879
+ for placement, p in _indexed(_get(machine, "packages")):
800
880
  at = "/machines/%d/packages/%d" % (m, p)
801
- if _contains(agents, _get(placement, "agentRef")):
802
- flag("%s/agentRef" % at, "%s is placed twice on one machine" % _s(placement, "agentRef"))
803
- agents.append(_get(placement, "agentRef"))
881
+ key = _key_of(_get(placement, "agentRef"), _context_of(placement))
882
+ if key in agents:
883
+ flag("%s/agentRef" % at, "%s is placed twice on one machine in the context %s"
884
+ % (_show(_get(placement, "agentRef")), _show(_context_of(placement))))
885
+ agents.append(key)
804
886
  if not _contains(kids, _get(placement, "publisherKid")):
805
- flag("%s/publisherKid" % at, "publisher key %s is not in publisherKeys, so the machine pins no key to verify the package under" % _s(placement, "publisherKid"))
887
+ flag("%s/publisherKid" % at, "publisher key %s is not in publisherKeys, so the machine pins no key to verify the package under" % _show(_get(placement, "publisherKid")))
806
888
 
807
889
 
808
890
  def _secret_references(value: Dict[str, Any], flag) -> None:
809
891
  machine_ids = [_get(m, "id") for m in _list(_get(value, "machines"))]
810
- for m, machine in enumerate(_list(_get(value, "machines"))):
811
- placed = [_get(p, "agentRef") for p in _list(_get(machine, "packages"))]
892
+ for machine, m in _indexed(_get(value, "machines")):
893
+ placed = [_key_of(_get(p, "agentRef"), _context_of(p)) for p in _list(_get(machine, "packages"))]
812
894
  slots: List[str] = []
813
- for s, secret in enumerate(_list(_get(machine, "secrets"))):
895
+ for secret, s in _indexed(_get(machine, "secrets")):
814
896
  at = "/machines/%d/secrets/%d" % (m, s)
815
- for field in ("slot", "from", "agentRef"):
897
+ # The message never repeats the value, and no message of this rule repeats a secret placement's context.
898
+ for field in ("slot", "from", "agentRef", "context"):
816
899
  text = _get(secret, field)
817
900
  if isinstance(text, str) and _secrets.secret_value(text):
818
901
  flag("%s/%s" % (at, field), "a secret placement names a slot, never a value: this looks like a credential")
819
- if not _contains(placed, _get(secret, "agentRef")):
820
- flag("%s/agentRef" % at, "a secret goes only where a placed package declares it (v4 Path 2): this agent is not placed on this machine")
902
+ if _key_of(_get(secret, "agentRef"), _context_of(secret)) not in placed:
903
+ flag("%s/agentRef" % at, "a secret goes only where a placed package declares it (v4 Path 2): this agent is not placed on this machine in the secret's context")
821
904
  if not _contains(machine_ids, _get(secret, "from")):
822
905
  flag("%s/from" % at, "the slot is filled from a machine of this estate")
823
- key = "%s %s" % (_s(secret, "agentRef"), _s(secret, "slot"))
906
+ elif not _same(_context_of(secret), PERSONAL_CONTEXT) and not _same(_get(secret, "from"), _get(machine, "id")):
907
+ # An envelope names no context (DEC-0082): an organization's instance fills its slot on its own machine.
908
+ flag("%s/from" % at, "an organization's instance fills its slot from its own machine's vault, never by an envelope from another machine")
909
+ key = _key_of(_get(secret, "agentRef"), _get(secret, "slot"), _context_of(secret))
824
910
  if key in slots:
825
- flag("%s/slot" % at, "slot %s is placed twice for one agent on one machine" % _s(secret, "slot"))
911
+ flag("%s/slot" % at, "slot %s is placed twice for one agent in one context on one machine" % _show(_get(secret, "slot")))
826
912
  slots.append(key)
827
913
 
828
914
 
@@ -834,12 +920,12 @@ def _grant_key(ident: Any) -> Any:
834
920
 
835
921
  def _grants_and_revocations(value: Dict[str, Any], flag) -> None:
836
922
  grant_ids: List[Any] = []
837
- for index, grant in enumerate(_list(_get(value, "shareGrants"))):
923
+ for grant, index in _indexed(_get(value, "shareGrants")):
838
924
  if _get(grant, "principalKind") != "external":
839
925
  flag("/shareGrants/%d/principalKind" % index, "a share grant is to a principal outside the estate: principalKind external")
840
926
  ident = _get(_get(grant, "meta"), "id") if isinstance(_get(grant, "meta"), dict) else _MISSING
841
927
  if _contains(grant_ids, _grant_key(ident)):
842
- flag("/shareGrants/%d/meta/id" % index, "grant %s is listed twice" % _js_string(ident))
928
+ flag("/shareGrants/%d/meta/id" % index, "grant %s is listed twice" % _show(ident))
843
929
  grant_ids.append(_grant_key(ident))
844
930
  machines = _list(_get(value, "machines"))
845
931
  listed = {
@@ -852,14 +938,60 @@ def _grants_and_revocations(value: Dict[str, Any], flag) -> None:
852
938
  "grant": grant_ids,
853
939
  }
854
940
  seen: List[str] = []
855
- for index, revocation in enumerate(_list(_get(value, "revocations"))):
941
+ for revocation, index in _indexed(_get(value, "revocations")):
856
942
  ref = _grant_key(_get(revocation, "ref")) if _get(revocation, "kind") == "grant" else _get(revocation, "ref")
857
- key = "%s %s" % (_s(revocation, "kind"), _js_string(ref))
943
+ kind = _show(_get(revocation, "kind"))
944
+ key = "%s %s" % (kind, _show(ref))
858
945
  if key in seen:
859
- flag("/revocations/%d" % index, "the %s %s is revoked twice" % (_s(revocation, "kind"), _s(revocation, "ref")))
946
+ flag("/revocations/%d" % index, "the %s %s is revoked twice" % (kind, _show(_get(revocation, "ref"))))
860
947
  seen.append(key)
861
- if _contains(listed.get(_s(revocation, "kind"), []), ref):
862
- flag("/revocations/%d/ref" % index, "the record still lists the %s it revokes: revocation outranks addition" % _s(revocation, "kind"))
948
+ if _contains(listed.get(kind, []), ref):
949
+ flag("/revocations/%d/ref" % index, "the record still lists the %s it revokes: revocation outranks addition" % kind)
950
+
951
+
952
+ def grant_context_findings(grant: Any, at: str) -> List[Dict[str, str]]:
953
+ """FAC-SEM-110 (DEC-0082): a grant to a principal outside the estate (``principalKind: external``, a share grant) is
954
+ used in that principal's organization, so its ``context`` is never ``personal``; absent means not stated."""
955
+ if not isinstance(grant, dict) or _get(grant, "principalKind") != "external" or _get(grant, "context") != PERSONAL_CONTEXT:
956
+ return []
957
+ return [{"code": "FAC-SEM-110", "instancePath": "%s/context" % at,
958
+ "message": "a grant to a principal outside the estate is used in that principal's organization, never in the personal context: "
959
+ "name the organization's org:<orgId>, or leave context out"}]
960
+
961
+
962
+ def _placement_grant_problems(record: Dict[str, Any], placement: Dict[str, Any]) -> List[Tuple[str, str]]:
963
+ """FAC-SEM-111 (DEC-0082) for one placement: an instance in an organization's context names, in ``grant``, a share
964
+ grant of the same record (matched by the normal form of its id, DEC-0076) whose ``context`` is the placement's, as
965
+ written, and that the record does not revoke; a personal instance names none. Any other context needs a grant, and
966
+ only an ``org:<orgId>`` context can match one, so a malformed context fails closed."""
967
+ import fabric_identifiers # noqa: E402 - beside this file
968
+ context = _context_of(placement)
969
+ grant = _get(placement, "grant")
970
+ if _same(context, PERSONAL_CONTEXT):
971
+ return [] if grant is _MISSING else [("/grant", "a personal instance runs under no share grant: only a placement in an organization's context names one")]
972
+ if not isinstance(grant, str):
973
+ return [("/grant", "an instance placed in the context %s names the share grant it runs under, by its id" % _show(context))]
974
+ ident = fabric_identifiers.normal_identifier(grant)
975
+ grants = [g for g in _list(_get(record, "shareGrants")) if isinstance(_get(g, "meta"), dict)
976
+ and isinstance(_get(g["meta"], "id"), str) and fabric_identifiers.normal_identifier(g["meta"]["id"]) == ident]
977
+ problems: List[Tuple[str, str]] = []
978
+ if not grants:
979
+ problems.append(("/grant", "the record has no share grant %s" % grant))
980
+ elif (parse_context(context) or {}).get("kind") != "org":
981
+ problems.append(("/context", "%s is not an organization's context id (org:<orgId>), so no share grant matches it" % _show(context)))
982
+ elif not any(_same(_get(g, "context"), context) for g in grants):
983
+ problems.append(("/grant", "the share grant %s is not for the context %s: its context must be the placement's, as written" % (grant, _show(context))))
984
+ if any(_get(e, "kind") == "grant" and isinstance(_get(e, "ref"), str) and fabric_identifiers.normal_identifier(e["ref"]) == ident
985
+ for e in _list(_get(record, "revocations"))):
986
+ problems.append(("/grant", "the record revokes the share grant %s, so no instance runs under it" % grant))
987
+ return problems
988
+
989
+
990
+ def _placement_grants(value: Dict[str, Any], flag) -> None:
991
+ for machine, m in _indexed(_get(value, "machines")):
992
+ for placement, p in _indexed(_get(machine, "packages")):
993
+ for suffix, message in _placement_grant_problems(value, placement):
994
+ flag("/machines/%d/packages/%d%s" % (m, p, suffix), message)
863
995
 
864
996
 
865
997
  def _record_findings(value: Dict[str, Any]) -> List[Dict[str, str]]:
@@ -869,7 +1001,10 @@ def _record_findings(value: Dict[str, Any]) -> List[Dict[str, str]]:
869
1001
  _placements(value, _finding(findings, "FAC-SEM-074"))
870
1002
  _secret_references(value, _finding(findings, "FAC-SEM-075"))
871
1003
  _grants_and_revocations(value, _finding(findings, "FAC-SEM-076"))
872
- for index, authority in enumerate(_list(_get(value, "authorities"))): # FAC-SEM-092 (DEC-0063)
1004
+ for grant, index in _indexed(_get(value, "shareGrants")): # FAC-SEM-110 (DEC-0082)
1005
+ findings.extend(grant_context_findings(grant, "/shareGrants/%d" % index))
1006
+ _placement_grants(value, _finding(findings, "FAC-SEM-111"))
1007
+ for authority, index in _indexed(_get(value, "authorities")): # FAC-SEM-092 (DEC-0063)
873
1008
  _signing_credential(authority, "/authorities/%d" % index, _finding(findings, "FAC-SEM-092"))
874
1009
  return findings
875
1010
 
@@ -884,29 +1019,36 @@ def _installation(value: Dict[str, Any]) -> List[Dict[str, str]]:
884
1019
  record = value["record"] if isinstance(_get(value, "record"), dict) else {}
885
1020
  pkg = value["package"] if isinstance(_get(value, "package"), dict) else {}
886
1021
  machines = _list(_get(record, "machines"))
887
- machine_index = next((i for i, m in enumerate(machines) if _same(_get(m, "id"), _get(value, "machine"))), -1)
1022
+ machine_index = next((i for m, i in _indexed(_get(record, "machines")) if _same(_get(m, "id"), _get(value, "machine"))), -1)
888
1023
  flag074 = _finding(findings, "FAC-SEM-074")
889
1024
  if machine_index < 0:
890
- flag074("/machine", "machine %s is not in the estate record" % _s(value, "machine"))
1025
+ flag074("/machine", "machine %s is not in the estate record" % _show(_get(value, "machine")))
891
1026
  return findings
892
- machine = machines[machine_index]
1027
+ machine = record["machines"][machine_index]
893
1028
  try:
894
1029
  digest: Any = _package_digest(pkg)
895
1030
  except TypeError:
896
1031
  digest = _MISSING
897
1032
  if digest is _MISSING or not _same(digest, _get(pkg, "digest")):
898
1033
  flag074("/package/digest", "the package is not the one its digest names: it changed after it was packed")
899
- placement = next((p for p in _list(_get(machine, "packages")) if _same(_get(p, "agentRef"), _get(pkg, "agentRef"))), None)
1034
+ context = _context_of(value)
1035
+ placement, placement_index = next(((p, i) for p, i in _indexed(_get(machine, "packages"))
1036
+ if _same(_get(p, "agentRef"), _get(pkg, "agentRef")) and _same(_context_of(p), context)), (None, -1))
900
1037
  if placement is None:
901
- flag074("/package/agentRef", "the record places no %s on machine %s" % (_s(pkg, "agentRef"), _s(value, "machine")))
1038
+ flag074("/package/agentRef", "the record places no %s on machine %s in the context %s"
1039
+ % (_show(_get(pkg, "agentRef")), _show(_get(value, "machine")), _show(context)))
902
1040
  elif not _same(_get(placement, "digest"), digest):
903
- flag074("/package", "the record places %s of this agent on this machine, not this package" % _s(placement, "digest"))
1041
+ flag074("/package", "the record places %s of this agent on this machine in this context, not this package" % _show(_get(placement, "digest")))
904
1042
  declared = [_get(slot, "name") for slot in _list(_get(pkg, "secrets"))]
905
1043
  flag075 = _finding(findings, "FAC-SEM-075")
906
- for index, secret in enumerate(_list(_get(machine, "secrets"))):
907
- if _same(_get(secret, "agentRef"), _get(pkg, "agentRef")) and not _contains(declared, _get(secret, "slot")):
1044
+ for secret, index in _indexed(_get(machine, "secrets")):
1045
+ if _same(_get(secret, "agentRef"), _get(pkg, "agentRef")) and _same(_context_of(secret), context) and not _contains(declared, _get(secret, "slot")):
908
1046
  flag075("/record/machines/%d/secrets/%d/slot" % (machine_index, index),
909
- "the package declares no slot %s: a secret goes only where a placed package declares it" % _s(secret, "slot"))
1047
+ "the package declares no slot %s: a secret goes only where a placed package declares it" % _show(_get(secret, "slot")))
1048
+ if placement is not None:
1049
+ flag111 = _finding(findings, "FAC-SEM-111")
1050
+ for suffix, message in _placement_grant_problems(record, placement):
1051
+ flag111("/record/machines/%d/packages/%d%s" % (machine_index, placement_index, suffix), message)
910
1052
  return findings
911
1053
 
912
1054
 
@@ -49,9 +49,10 @@ DASHBOARD_EXTENSION_KEY = "https://fabric.passioncode.ai/agent-contract/extensio
49
49
  DASHBOARD_KIT = "fabric-dashboard-kit@1.1.0"
50
50
  REFERENCE_THEMES = ("graphite", "fabric", "paper", "forest", "ocean", "ember", "contrast")
51
51
  EXIT_ALREADY_RUNNING = 75
52
- # Lifecycle contract (fabric-workspace knowledge/lifecycle.md). A launchd-supervised copy that finds
53
- # the lock held backs off in-process for this long before its exit 75 (LC-03, F8): under KeepAlive
54
- # with ThrottleInterval 10 that is a few starts an hour instead of one every 10 s.
52
+ # Lifecycle rules: references/lifecycle.md, the rule for this kit's users, which implements the public
53
+ # contract's service specification (Lifecycle). A launchd-supervised copy that finds the lock held backs
54
+ # off in-process for this long before its exit 75 (LC-03, F8): under KeepAlive with ThrottleInterval 10
55
+ # that is a few starts an hour instead of one every 10 s.
55
56
  SUPERVISOR_ENV = "FABRIC_SERVICE_SUPERVISOR"
56
57
  LOCK_WAIT_SUPERVISED_SECONDS = 300.0
57
58
  LOCK_BACKOFF_FIRST_SECONDS = 0.5
@@ -5,9 +5,9 @@ license: AGPL-3.0-only OR LicenseRef-PassionCode-Commercial
5
5
  compatibility: Requires filesystem access and Python 3.9+. Exact schema checks additionally need git, Node.js, pnpm, and the pinned fabric-agent-contract checkout (a public repository). Works without those tools in an explicitly degraded structural-check mode. Ships in one plugin with adapting-projects-to-fabric, whose scripts it reuses.
6
6
  metadata:
7
7
  author: PassionCode.ai
8
- version: "0.32.0"
8
+ version: "0.33.0"
9
9
  contract-version: "0.1.0"
10
- contract-commit: "1636fa97262f4a26e2086572d540c2ee701e0010"
10
+ contract-commit: "82e163665e156a08aa3b92e4711015582c06162c"
11
11
  ---
12
12
 
13
13
  # Creating Fabric-compatible agents
@@ -123,7 +123,7 @@ python3 <plugin-dir>/skills/adapting-projects-to-fabric/scripts/adapt_project.py
123
123
  The effect class is declared, never defaulted: ask the author what the agent does outside itself when the request
124
124
  does not say. Cost presets are only for a run whose cost varies; their amounts come from the author or measured runs.
125
125
 
126
- Pin exactly contract `0.1.0` at commit `1636fa97262f4a26e2086572d540c2ee701e0010` and
126
+ Pin exactly contract `0.1.0` at commit `82e163665e156a08aa3b92e4711015582c06162c` and
127
127
  read the pinned guide before implementing protocol details. If this skill is installed
128
128
  without its sibling, the scaffolder is absent: create the bundle by hand from the pinned
129
129
  contract's `docs/guides/connecting-compatible-agents.md` and mark the structural check
@@ -19,7 +19,7 @@ compatibility: >-
19
19
  this plugin; Codex and Kimi Code get the session-start preamble hook on the person's consent.
20
20
  metadata:
21
21
  author: PassionCode.ai
22
- version: "0.32.0"
22
+ version: "0.33.0"
23
23
  ---
24
24
 
25
25
  # Working with Fabric
@@ -1,9 +1,10 @@
1
1
  # The project board (`fabric-project-comms/0.1`, the `com.*` tools)
2
2
 
3
3
  Read this when the session starts in a project a Fabric hub serves, when the person asks about board mail, when you
4
- need something from another Project, or when you wait for an answer. The rule itself has one home: PassionCode's
5
- knowledge base, [agents-first.md, "The project board"](https://github.com/passioncode-ai/fabric-workspace/blob/main/knowledge/agents-first.md#the-project-board).
6
- This page is how a coding agent with this plugin follows it; where the two differ, that page wins.
4
+ need something from another Project, or when you wait for an answer. This page is the rule a coding agent with this
5
+ plugin follows. Inside PassionCode.ai it implements the knowledge base's
6
+ [agents-first.md, "The project board"](https://github.com/passioncode-ai/fabric-workspace/blob/main/knowledge/agents-first.md#the-project-board)
7
+ (private to the organization), and is kept in step with it; nothing behind that link is needed to follow this page.
7
8
 
8
9
  ## Contents
9
10
 
@@ -60,7 +60,7 @@ WATCH_MAX_BACKOFF_S = 900
60
60
  SESSION_ID = re.compile(r"[A-Za-z0-9._:-]{1,128}")
61
61
  LOOPBACK_ORIGIN = re.compile(r"http://127\.0\.0\.1:(\d{1,5})")
62
62
  LOCAL_ORIGIN = re.compile(r'"origin": "http://(?:127\.0\.0\.1|localhost)')
63
- VERSION = "0.32.0"
63
+ VERSION = "0.33.0"
64
64
  HOSTS = {"claude": "Claude Code", "codex": "Codex", "kimi": "Kimi Code"}
65
65
  # A session Fabric launched. FABRIC_LAUNCHED_SESSION=1 is the marker Fabric is asked to set (need D6); the others are
66
66
  # what Fabric already gives a launched session (fabric sessionBundle.ts): only their NAMES are looked at, never a value
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json",
3
3
  "name": "passioncode",
4
4
  "displayName": "PassionCode.ai",
5
- "version": "0.2.2",
5
+ "version": "0.2.5",
6
6
  "description": "Set up and check PassionCode.ai on a computer with one command (setting-up-passioncode), the organisation's working rules for every contributor's agent (working-in-passioncode), and a once-a-day check at session start that keeps the PassionCode.ai skill set current.",
7
7
  "author": {
8
8
  "name": "PassionCode.ai",
@@ -5,13 +5,18 @@
5
5
  * wiki.passioncode.ai/knowledge) and the repository's AGENTS.md before its first edit, and after
6
6
  * the work updates the knowledge base page that owns any cross-repository fact it changed. A rule
7
7
  * an agent has to remember is a rule it forgets in the one repository where it mattered, so the
8
- * session start says it, in one line, and only where it applies. Anything unexpected (no git, no
9
- * origin, a slow git) prints nothing: a reminder must never cost a session.
8
+ * session start says it, in one line, and only where it applies. The same line names the two Fabric
9
+ * principles that govern how the work moves — 10, a free agent takes the next task, and 11, a project
10
+ * moves in releases and every run leaves a lesson — by their anchors in principles.md, so the
11
+ * knowledge base's rules §4 reaches every session without each AGENTS.md restating it. Anything
12
+ * unexpected (no git, no origin, a slow git) prints nothing: a reminder must never cost a session.
10
13
  */
11
14
  const { execFileSync } = require('child_process');
12
15
 
13
16
  const KNOWLEDGE = 'fabric-workspace/knowledge/';
14
17
  const KNOWLEDGE_WEB = 'https://wiki.passioncode.ai/knowledge';
18
+ const PRINCIPLE_10 = `${KNOWLEDGE}principles.md#10-a-free-agent-takes-the-next-task`;
19
+ const PRINCIPLE_11 = `${KNOWLEDGE}principles.md#11-a-project-moves-in-releases-and-every-run-leaves-a-lesson-the-next-one-reads`;
15
20
 
16
21
  /** The GitHub organization an origin URL belongs to, when it is passioncode-ai; else null. */
17
22
  function orgOf(url) {
@@ -32,7 +37,7 @@ function originOf(dir) {
32
37
  /** The one line to print at session start in `dir`, or null. */
33
38
  function rulesLine(dir) {
34
39
  if (!orgOf(originOf(dir))) return null;
35
- return `[passioncode] passioncode-ai repository: read the knowledge base (${KNOWLEDGE} or ${KNOWLEDGE_WEB}), then this repository's AGENTS.md, before the first edit; after the work, update the knowledge base page that owns any cross-repository fact you changed; edit each canonical task source declared in docs/backlog-sources.json and publish the derived workspace backlog.`;
40
+ return `[passioncode] passioncode-ai repository: read the knowledge base (${KNOWLEDGE} or ${KNOWLEDGE_WEB}), then this repository's AGENTS.md, before the first edit; work by Fabric principle 10, a free agent takes the next task (a wait goes on a watcher, stop only at a human gate; ${PRINCIPLE_10}), and principle 11, a project moves in releases and every run leaves a lesson the next one reads (${PRINCIPLE_11}); after the work, update the knowledge base page that owns any cross-repository fact you changed; edit each canonical task source declared in docs/backlog-sources.json and publish the derived workspace backlog.`;
36
41
  }
37
42
 
38
- module.exports = { KNOWLEDGE, KNOWLEDGE_WEB, orgOf, rulesLine };
43
+ module.exports = { KNOWLEDGE, KNOWLEDGE_WEB, PRINCIPLE_10, PRINCIPLE_11, orgOf, rulesLine };
@@ -124,6 +124,15 @@ Also, before the first edit:
124
124
  - **Receipts.** A claim that will be read as true carries `file:line`, a command and
125
125
  its output, or a test name. Documentation changes in the same change as the
126
126
  behaviour it describes.
127
+ - **A free agent takes the next task** (Fabric [principle 10](https://github.com/passioncode-ai/fabric-workspace/blob/main/knowledge/principles.md#10-a-free-agent-takes-the-next-task)).
128
+ A finished unit is followed by the next task from the board or the handoff, in the same turn.
129
+ Every wait (a review, CI, a merge, a peer, a scheduled time) goes on a watcher that acts when it
130
+ clears, and one that can outlive the session goes on a durable trigger. Stop only at a human gate:
131
+ a sign-in, a consent, a payment, a secret a person types, spend above the limit, a recorded decision.
132
+ - **A project moves in releases; every run leaves a lesson** (Fabric [principle 11](https://github.com/passioncode-ai/fabric-workspace/blob/main/knowledge/principles.md#11-a-project-moves-in-releases-and-every-run-leaves-a-lesson-the-next-one-reads)).
133
+ A finding gets an id in the owning register and is fixed in the next release with every other
134
+ open item; only the five-entry hotfix lane ships alone. Questions go out in rounds. Read the
135
+ retrospectives before work and write the lesson after it.
127
136
 
128
137
  agent-sync missing: `npx @ssheleg/agent-sync install`. If it cannot be installed, do
129
138
  not edit a guarded file — say which file and why you stopped.