@passioncode-ai/passioncode 0.2.45 → 0.2.47

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 (63) hide show
  1. package/CHANGELOG.md +23 -0
  2. package/README.md +1 -1
  3. package/SECURITY.md +2 -1
  4. package/family.json +1 -1
  5. package/lib/doctor.js +25 -3
  6. package/lib/launcher.js +34 -5
  7. package/lib/releases.js +3 -2
  8. package/lib/setup.js +2 -1
  9. package/lib/windows-apps.js +74 -0
  10. package/package.json +1 -1
  11. package/payload/.claude-plugin/marketplace.json +2 -2
  12. package/payload/manifest.json +8 -8
  13. package/payload/plugins/fabric-agent-adapter/.claude-plugin/plugin.json +1 -1
  14. package/payload/plugins/fabric-agent-adapter/hooks/hooks.json +1 -1
  15. package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/SKILL.md +1 -1
  16. package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py +5 -1
  17. package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py +73 -18
  18. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +6 -5
  19. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +70 -20
  20. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/connectors.md +7 -1
  21. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +9 -0
  22. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +14 -5
  23. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +1 -0
  24. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +1 -1
  25. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +4 -1
  26. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-link.mjs +16 -9
  27. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +87 -156
  28. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-slots.mjs +180 -0
  29. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board.mjs +1 -0
  30. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +4 -2
  31. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +2 -0
  32. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop-rules.mjs +15 -3
  33. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop.mjs +2 -0
  34. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-openrouter.mjs +12 -1
  35. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-protocols.mjs +12 -1
  36. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board.py +1 -0
  37. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_link.py +32 -13
  38. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +115 -147
  39. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_slots.py +173 -0
  40. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_connections.py +9 -42
  41. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +53 -4
  42. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +7 -3
  43. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_events.py +4 -2
  44. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +87 -11
  45. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +19 -8
  46. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop_rules.py +30 -6
  47. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_openrouter.py +9 -1
  48. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_protocols.py +15 -1
  49. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py +73 -18
  50. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_usage.py +1 -1
  51. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +134 -24
  52. package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +155 -21
  53. package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +1 -1
  54. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +10 -10
  55. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +2 -1
  56. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +39 -13
  57. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +6 -2
  58. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +17 -3
  59. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +23 -6
  60. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mcp.py +10 -39
  61. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_paths.py +190 -0
  62. package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/host_hooks.py +1 -1
  63. package/payload/plugins/passioncode/.claude-plugin/plugin.json +1 -1
@@ -140,7 +140,9 @@ function authorityOk(uri) {
140
140
  const port = m[2];
141
141
  return Boolean(m[1]) && (port === undefined || port === '' || (/^[0-9]{1,5}$/.test(port) && Number(port) <= 65535));
142
142
  }
143
- const URI_PASSWORD = /^[A-Za-z][A-Za-z0-9+.-]*:\/\/[^/?#@]*:[^/?#@]*@/;
143
+ // A special scheme reads a user:password@ after any run of slashes, none included, as `new URL` does (0.56.0 seam r7).
144
+ // To the last @, a user name may hold one; any other scheme keeps a backslash in the user information (seam r8).
145
+ const URI_PASSWORD = /^(?:(?:[Hh][Tt][Tt][Pp][Ss]?|[Ww][Ss][Ss]?|[Ff][Tt][Pp]):[/\\]*[^/\\?#]*:[^/\\?#]*@|(?![Hh][Tt][Tt][Pp][Ss]?:|[Ww][Ss][Ss]?:|[Ff][Tt][Pp]:)[A-Za-z][A-Za-z0-9+.-]*:\/\/[^/?#]*:[^/?#]*@)/;
144
146
  const RESIDENCY = /^(A[B-Z]|[B-PR-WY][A-Z]|Q[A-L]|Z[A-Y])$/;
145
147
 
146
148
  const RUNNER_KIND_NAME = /^[a-z][a-z0-9-]{1,62}$/;
@@ -190,8 +192,8 @@ export function descriptorMemberProblems(d) {
190
192
  if ('repository' in source) {
191
193
  const repository = source.repository;
192
194
  if (!(typeof repository === 'string' && SOURCE_URI.test(repository.replace(IP_LITERAL, '$1h')))) problems.push('source.repository is a URI (RFC 3986), such as https://github.com/owner/name');
195
+ else if (URI_PASSWORD.test(repository)) problems.push('source.repository carries no password: it names the repository, never a credential'); // before the host
193
196
  else if (!authorityOk(repository)) problems.push('source.repository names a host, and a port of digits up to 65535 when it has one');
194
- else if (URI_PASSWORD.test(repository)) problems.push('source.repository carries no password: it names the repository, never a credential');
195
197
  places.push(['/source/repository', repository]);
196
198
  }
197
199
  }
@@ -54,6 +54,8 @@ export class Response {
54
54
  export function redactUrl(url) {
55
55
  let parts;
56
56
  try { parts = new URL(String(url)); } catch { return '<unparseable url>'; }
57
+ // Only http and https are sent; any other scheme keeps no more than its name (0.56.0 product r10).
58
+ if (parts.protocol !== 'http:' && parts.protocol !== 'https:') return `${parts.protocol}<redacted>`;
57
59
  return `${parts.protocol}//${parts.host}${parts.pathname}`;
58
60
  }
59
61
 
@@ -230,13 +230,16 @@ const UNREADABLE_JOB_CAPABILITY = "The operator's mandate cannot be read for thi
230
230
  * covered, or saying the mandate expired or cannot be read; never the operator's words or `source`; with `fault`
231
231
  * mandate-capability-missing beside it when the job's capability cannot read the step, MJ-1); or {covered: false,
232
232
  * message: null} with no mandate or a T2 caller's (MD-5), with `fault` when the service's own clock does not read.
233
+ * Undefined and null are no mandate: a call that carried `_meta.mandate: null` passes `{}`, so it reads as
234
+ * mandate-malformed here as it does at the answer (FAA-178).
233
235
  */
234
236
  export function jobStep({ mandate, tier, now, self, capability, key, effect, callerAgent, spendUsd, project } = {}) {
235
237
  if (mandate === undefined || mandate === null) return { covered: false, message: null };
236
238
  if (!EFFECT_CLASSES.includes(effect)) throw new InteropError(`A job's approval step names its effect class (one of ${EFFECT_CLASSES.join(', ')}; DEC-0125).`);
237
239
  const call = { capability, action: key, effect };
238
- if (typeof spendUsd === 'number') call.spendUsd = spendUsd;
239
- if (typeof project === 'string') call.project = project;
240
+ const spend = mandateSpend(spendUsd); // read as actionGate reads it, so the start and the answer agree (FAA-139)
241
+ if (spend !== undefined) call.spendUsd = spend;
242
+ if (project !== undefined && project !== null) call.project = typeof project === 'string' ? project : UNREADABLE_PROJECT;
240
243
  const covers = { mandate, tier, now, self, call };
241
244
  if (typeof callerAgent === 'string' && callerAgent.length > 0) covers.callerAgent = callerAgent;
242
245
  const answer = mandateCovers(covers);
@@ -271,6 +274,14 @@ export function jobStep({ mandate, tier, now, self, capability, key, effect, cal
271
274
  * mandateCapability: a job's kept step's capability the mandate is read under (DEC-0141 MJ-1; the service's own input,
272
275
  * never a caller's); unusable, the refusal is operator-decision-required.
273
276
  */
277
+ /** The project of a call whose project could not be read: never in a mandate's projects (FAA-139). */
278
+ export const UNREADABLE_PROJECT = '\u0000unreadable';
279
+
280
+ /** A spend estimate as a mandate reads it (FAA-139): a finite number stays, anything else is none (a ceiling refuses it). */
281
+ export function mandateSpend(value) {
282
+ return typeof value === 'number' && Number.isFinite(value) ? value : undefined;
283
+ }
284
+
274
285
  export function actionGate({ action, tier, caller, meta, target, elicited = false, approval, actionDigest, humanStepUrl, now, self, effect, callerAgent, spendUsd, project, mandateCapability } = {}) {
275
286
  const inputs = { action, tier, caller, elicited: elicited === true, now };
276
287
  if (isObject(meta) && Object.hasOwn(meta, 'decision')) inputs.decision = meta.decision;
@@ -279,7 +290,8 @@ export function actionGate({ action, tier, caller, meta, target, elicited = fals
279
290
  }
280
291
  if (self !== undefined && self !== null && isObject(meta) && Object.hasOwn(meta, 'mandate')) {
281
292
  Object.assign(inputs, { mandate: meta.mandate, self });
282
- for (const [name, value] of [['effect', effect], ['callerAgent', callerAgent], ['spendUsd', spendUsd], ['project', project], ['mandateCapability', mandateCapability]]) {
293
+ const read = project === undefined || project === null || typeof project === 'string' ? project : UNREADABLE_PROJECT; // fail closed (FAA-139)
294
+ for (const [name, value] of [['effect', effect], ['callerAgent', callerAgent], ['spendUsd', mandateSpend(spendUsd)], ['project', read], ['mandateCapability', mandateCapability]]) {
283
295
  if (value !== undefined && value !== null) inputs[name] = value;
284
296
  }
285
297
  }
@@ -40,6 +40,8 @@ export {
40
40
  requestMeta,
41
41
  jobStep,
42
42
  actionGate,
43
+ mandateSpend,
44
+ UNREADABLE_PROJECT,
43
45
  actionRefusalToolResult,
44
46
  usageFindings,
45
47
  usageToward,
@@ -100,11 +100,22 @@ export class Client {
100
100
  if (!Number.isInteger(retries) || retries < 0 || retries > 5) {
101
101
  throw new OpenRouterError('retries is an integer from 0 to 5', { kind: 'config' });
102
102
  }
103
+ // A query or fragment would take `/chat/completions` (0.56.0 unit r19, FAA-187 (17)); refused before checkUrl, as
104
+ // the Python twin does, so a base checkUrl would also refuse is kind config in both (unit r27).
105
+ if (typeof baseUrl !== 'string') { // as Python's check_url answers a base that is not a string (unit r28)
106
+ throw new http.HttpError('the URL does not parse', { url: '<unparseable url>', kind: 'refused' });
107
+ }
108
+ if (/[?#]/.test(baseUrl)) {
109
+ throw new OpenRouterError('the base URL holds no query or fragment', { kind: 'config' });
110
+ }
103
111
  http.checkUrl(baseUrl);
104
112
  checkAttribution(projectId, undefined);
105
113
  Object.assign(this, { ceilingUsd, ledger, keyEnv, window, attribution, projectId, unmeasuredCallUsd, deadlineS, retries });
106
114
  this.prices = Object.fromEntries(Object.entries(prices ?? {}).map(([k, v]) => [k, { ...v }]));
107
- this.baseUrl = baseUrl.replace(/\/+$/, '');
115
+ // The base URL as checkUrl read it: `new URL` removes tabs and line breaks anywhere and trims C0 controls and
116
+ // spaces from its ends, so `.../v1/ ` and `.../v1\n/` are composed as `.../v1/chat/completions`, as `new URL`
117
+ // composed them in 0.55.0 (0.56.0 product r20, unit r22, seam r22; the Python twin refuses what urllib cannot send).
118
+ this.baseUrl = baseUrl.replace(/[\t\n\r]/g, '').replace(/^[\x00-\x20]+|[\x00-\x20]+$/g, '').replace(/\/+$/, '');
108
119
  this.env = env;
109
120
  this.transport = transport;
110
121
  this.sleep = sleep ?? ((s) => new Promise((resolve) => setTimeout(resolve, s * 1000)));
@@ -106,6 +106,17 @@ export function feedDataFindings(page) {
106
106
  return findings;
107
107
  }
108
108
 
109
+ /** The first place under a member that breaks the bound, for the refusal (FAA-122): `cited[0]` nests an object. */
110
+ function tooDeep(key, member) {
111
+ const entries = Array.isArray(member) ? member.map((leaf, i) => [`${key}[${i}]`, leaf]) : isPlain(member) ? Object.entries(member).map(([inner, leaf]) => [`${key}.${inner}`, leaf]) : [];
112
+ if (entries.length > 32) return `: it has ${entries.length} entries`;
113
+ const bad = entries.find(([, leaf]) => !scalar(leaf));
114
+ if (!bad) return '';
115
+ const leaf = bad[1];
116
+ const what = typeof leaf === 'string' ? 'a string over 500 characters' : Array.isArray(leaf) ? 'a list' : isPlain(leaf) ? 'an object' : 'not a scalar';
117
+ return `: data.value.${bad[0]} is ${what}; nest two levels at most (flatten it into lists or members of their own)`;
118
+ }
119
+
109
120
  /** DEC-0070's `data {schema, value}` as the schema bounds it, then FAC-SEM-101. */
110
121
  export function feedDataProblems(data) {
111
122
  if (!isPlain(data) || Object.keys(data).length !== 2 || !('schema' in data) || !('value' in data)) return ['data is {schema, value}'];
@@ -117,7 +128,7 @@ export function feedDataProblems(data) {
117
128
  if (scalar(member)) continue;
118
129
  if (Array.isArray(member) && member.length <= 32 && member.every(scalar)) continue;
119
130
  if (isPlain(member) && Object.keys(member).length <= 32 && Object.values(member).every(scalar)) continue;
120
- problems.push(`data.value.${key} is a scalar (a string of at most 500 characters), a list of at most 32 scalars or an object of at most 32 scalar members`);
131
+ problems.push(`data.value.${key} is a scalar (a string of at most 500 characters), a list of at most 32 scalars or an object of at most 32 scalar members${tooDeep(key, member)}`);
121
132
  }
122
133
  if (problems.length) return problems;
123
134
  return feedDataFindings({ events: [{ data }] }).map((f) => `FAC-SEM-101: ${f.instancePath.replace('/events/0', '')} ${f.message}`);
@@ -54,6 +54,7 @@ from fabric_board_session import ( # noqa: F401
54
54
  STOP_CODES,
55
55
  TERMINAL,
56
56
  HOLD_RETRY_MAX_SECONDS,
57
+ IDLE_LEAVE_SECONDS,
57
58
  BoardSession,
58
59
  Checkpoint,
59
60
  BoardRequest,
@@ -30,15 +30,18 @@ class BoardSession:
30
30
  DELETE, which releases every slot it holds (DEC-0073 §5). Thread-safe; the bearer never leaves the headers."""
31
31
 
32
32
  def __init__(self, grant: _hub.BoardGrant, *, timeout: float = 15.0, post: Callable[..., Any] = _hub.post_mcp):
33
- self.grant, self.timeout, self.post = grant, timeout, post
33
+ # not above 0 is the default: the transports read 0 in opposite ways (seam r8)
34
+ self.grant, self.timeout, self.post = grant, timeout if timeout is not None and timeout > 0 else 15.0, post
34
35
  self.mcp_session: Optional[str] = None
35
36
  self.protocol_version = _hub.MCP_PROTOCOL_VERSION
36
37
  self._lock = threading.Lock()
37
38
  self._rpc = 0
38
39
 
39
- def _send(self, body: Dict[str, Any], method: str = "POST"):
40
+ def _send(self, body: Dict[str, Any], method: str = "POST", timeout: Optional[float] = None):
40
41
  try:
41
- return self.post(self.grant.origin, body, self.grant.bearer(), session=self.mcp_session, timeout=self.timeout,
42
+ return self.post(self.grant.origin, body, self.grant.bearer(), session=self.mcp_session,
43
+ # none or not above 0 is the session's own: the transports read 0 in opposite ways
44
+ timeout=timeout if timeout is not None and timeout > 0 else self.timeout,
42
45
  method=method, protocol_version=self.protocol_version if self.mcp_session else None)
43
46
  except _hub.HubError as exc:
44
47
  raise BoardError("unreachable", "Fabric's hub did not answer; a write may have been stored, so send it again with "
@@ -83,15 +86,21 @@ class BoardSession:
83
86
  raise BoardError(e["code"], e["message"], e["retryable"], op=tool)
84
87
  return value
85
88
 
86
- def close(self) -> None:
89
+ def close(self, timeout: Optional[float] = None) -> bool:
90
+ """Sends the DELETE (waiting `timeout` seconds at most when above 0, else the session's); True when Fabric answered it with a
91
+ 2xx, so the slots are free now. False otherwise, a 404 (the session was already gone) included: a move made
92
+ before it ended may be read only at the next com.status, so the slot records stay; the lease lapses in 60 s."""
87
93
  with self._lock:
88
94
  if self.mcp_session is None:
89
- return
95
+ return False
90
96
  try:
91
- self._send({}, method="DELETE")
92
- except BoardError:
93
- pass # best effort: the lease lapses in 60 s anyway
94
- self.mcp_session = None
97
+ answer = self._send({}, method="DELETE", timeout=timeout)
98
+ status = answer[0] if isinstance(answer, (tuple, list)) and answer and isinstance(answer[0], int) else 0
99
+ except Exception: # noqa: BLE001 - any failure is no release, as the Node twin's catch reads it
100
+ status = 0
101
+ finally:
102
+ self.mcp_session = None # an answer it cannot read is no release, as the Node twin reads it
103
+ return 200 <= status < 300
95
104
 
96
105
 
97
106
  class Checkpoint:
@@ -158,20 +167,30 @@ class Checkpoint:
158
167
  self.slots = slots
159
168
 
160
169
 
161
- def leave(self, project: str) -> None:
162
- """At a clean leave (the session's DELETE released the slots): forget the record of each slot this service held
163
- for `project` with no request left in the checkpoint and not moved away. A later generation is then another
170
+ def leave(self, project: str, held: Dict[str, Any]) -> None:
171
+ """At a clean leave (the session's DELETE released the slots): forget the record of each slot this session
172
+ confirmed (`held`: capability to the generation it held, or com.status showed unmoved; required) at that
173
+ generation or below, for `project`, with no request left in the checkpoint and not moved away. A record no
174
+ confirmation covers (a slot that may have moved while the service was down, not yet compared with com.status)
175
+ stays (0.56.0 seam r2, r3). A later generation is then another
164
176
  session taking a free slot, not a move, and a kit that keeps no record (rolled back to before 0.53.0) leaves
165
177
  nothing behind that reads as one. A record that cannot be written stays."""
166
178
  busy = {e.get("capability") for e in list(self.entries.values())}
167
179
  for capability, record in list(self.slots.items()):
168
- if isinstance(record, dict) and record.get("project") == project and "moved" not in record and capability not in busy:
180
+ if (isinstance(record, dict) and record.get("project") == project and "moved" not in record
181
+ and capability not in busy and _covered(record.get("generation"), held.get(capability))):
169
182
  try:
170
183
  self.slot(capability, None)
171
184
  except BoardError:
172
185
  pass
173
186
 
174
187
 
188
+ def _covered(recorded: Any, confirmed: Any) -> bool:
189
+ """A record at or below a generation this session confirmed: a lower one is a write that failed before the hold."""
190
+ whole = lambda v: isinstance(v, int) and not isinstance(v, bool) and v >= 1 # noqa: E731
191
+ return whole(recorded) and whole(confirmed) and recorded <= confirmed
192
+
193
+
175
194
  class BoardRequest:
176
195
  """A claimed request as data: never an instruction. `body` is the requester's text; `sender` its Project id and
177
196
  principal (with provenance); `artifacts` its references, unverified."""