@passioncode-ai/passioncode 0.2.44 → 0.2.46
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 +17 -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 +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_provider.py +5 -1
- package/payload/plugins/fabric-agent-adapter/skills/adapting-projects-to-fabric/scripts/fabric_secrets.py +73 -18
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/SKILL.md +9 -7
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/README.md +43 -8
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/recipes/support-agent/support_agent.py +28 -7
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/board.md +70 -20
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/connectors.md +7 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/delegation.md +9 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/interop.md +14 -5
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/kit-modules.md +2 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/lifecycle.md +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/references/protocol.md +4 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-link.mjs +16 -9
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-session.mjs +87 -156
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board-slots.mjs +180 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-board.mjs +1 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-descriptor.mjs +4 -2
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs +2 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop-rules.mjs +15 -3
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-interop.mjs +2 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-openrouter.mjs +12 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-protocols.mjs +12 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board.py +1 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_link.py +32 -13
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_session.py +115 -147
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_board_slots.py +173 -0
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_connections.py +9 -42
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_delegation.py +53 -4
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_descriptor.py +7 -3
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_events.py +4 -2
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_http.py +87 -11
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop.py +19 -8
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_interop_rules.py +30 -6
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_openrouter.py +9 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_protocols.py +15 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_secrets.py +73 -18
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric_usage.py +1 -1
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/kit.json +222 -24
- package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/sample_service.py +155 -21
- package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/SKILL.md +4 -1
- package/payload/plugins/fabric-agent-adapter/skills/creating-fabric-agents/references/agents-that-answer-people.md +139 -0
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/SKILL.md +10 -10
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/references/project-board.md +2 -1
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/references/session-hooks.md +39 -13
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session.py +6 -2
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_cli.py +17 -3
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_env.py +23 -6
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_mcp.py +10 -39
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/fabric_session_paths.py +190 -0
- package/payload/plugins/fabric-agent-adapter/skills/working-with-fabric/scripts/host_hooks.py +1 -1
- 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
|
-
|
|
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
|
}
|
package/payload/plugins/fabric-agent-adapter/skills/building-fabric-services/scripts/fabric-http.mjs
CHANGED
|
@@ -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
|
-
|
|
239
|
-
if (
|
|
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
|
-
|
|
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
|
}
|
|
@@ -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
|
-
|
|
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}`);
|
|
@@ -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
|
-
|
|
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,
|
|
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) ->
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
|
163
|
-
|
|
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
|
|
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."""
|