@uuaid/agent-commons 0.0.0 → 0.2.0-alpha.2

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 (69) hide show
  1. package/CONFORMANCE.md +26 -0
  2. package/LICENSE +201 -0
  3. package/NOTICE +25 -0
  4. package/PROVENANCE.json +29 -0
  5. package/README.md +313 -5
  6. package/bin/agent-commons.mjs +71 -0
  7. package/examples/Dockerfile +12 -0
  8. package/examples/agent.mjs +17 -0
  9. package/examples/global-policy.json +9 -0
  10. package/examples/run-cohort.mjs +68 -0
  11. package/package.json +28 -8
  12. package/profiles/agentnet.chat.json +11 -0
  13. package/profiles/local.json +11 -0
  14. package/profiles/zilligon.com.json +11 -0
  15. package/profiles/zilligons.com.json +11 -0
  16. package/src/carrier.mjs +79 -0
  17. package/src/channels.d.mts +75 -0
  18. package/src/channels.mjs +158 -0
  19. package/src/cohort.mjs +61 -0
  20. package/src/collaboration.mjs +875 -0
  21. package/src/config.mjs +39 -0
  22. package/src/continuity.d.mts +24 -0
  23. package/src/continuity.mjs +594 -0
  24. package/src/governance-scope.d.mts +46 -0
  25. package/src/governance-scope.mjs +139 -0
  26. package/src/governance.mjs +353 -0
  27. package/src/index.d.ts +109 -0
  28. package/src/index.mjs +15 -0
  29. package/src/loop.mjs +71 -0
  30. package/src/offline.d.mts +7 -0
  31. package/src/offline.mjs +29 -0
  32. package/src/pillar.mjs +59 -0
  33. package/src/profiles.d.mts +12 -0
  34. package/src/profiles.mjs +91 -0
  35. package/src/runtime.mjs +346 -0
  36. package/src/store.mjs +73 -0
  37. package/src/transport.d.mts +76 -0
  38. package/src/transport.mjs +267 -0
  39. package/src/trust.mjs +65 -0
  40. package/src/vendor/pillar/blob/blob.mjs +510 -0
  41. package/src/vendor/pillar/crypto/e2e.mjs +97 -0
  42. package/src/vendor/pillar/identity/jcs.mjs +35 -0
  43. package/src/vendor/pillar/identity/keychain.mjs +192 -0
  44. package/src/vendor/pillar/identity/tier.mjs +130 -0
  45. package/src/vendor/pillar/net/carrier-client.mjs +197 -0
  46. package/src/vendor/pillar/net/delegation.mjs +209 -0
  47. package/src/vendor/pillar/net/envelope.mjs +258 -0
  48. package/src/vendor/pillar/net/refusals.mjs +127 -0
  49. package/src/vendor/pillar/net/seed-tier.mjs +185 -0
  50. package/test/channels.test.mjs +268 -0
  51. package/test/cohort.test.mjs +36 -0
  52. package/test/collaboration.test.mjs +459 -0
  53. package/test/continuity.test.mjs +229 -0
  54. package/test/custody.negative.test.mjs +165 -0
  55. package/test/derivation-parity.test.mjs +19 -0
  56. package/test/fixtures/derivation-vectors.json +26 -0
  57. package/test/fixtures/ed25519-small-order.vectors.json +15 -0
  58. package/test/governance-expected-red.expected-red.mjs +241 -0
  59. package/test/governance-scope.test.mjs +142 -0
  60. package/test/governance-t1-t10.test.mjs +477 -0
  61. package/test/governance.test.mjs +514 -0
  62. package/test/hold-admission.test.mjs +1185 -0
  63. package/test/keychain-strict-ed25519.test.mjs +240 -0
  64. package/test/offline.test.mjs +152 -0
  65. package/test/runtime-channels.test.mjs +1140 -0
  66. package/test/runtime.test.mjs +201 -0
  67. package/test/security-runtime.test.mjs +104 -0
  68. package/test/strict-ed25519-consumers.test.mjs +234 -0
  69. package/test/transport.test.mjs +353 -0
package/src/index.mjs ADDED
@@ -0,0 +1,15 @@
1
+ export { AgentCommons } from "./runtime.mjs";
2
+ export { CommonsCarrier } from "./carrier.mjs";
3
+ export { CommonsStore } from "./store.mjs";
4
+ export { RegistryTrust, REQUIRED_STANDARDS, TrustError } from "./trust.mjs";
5
+ export { runAgentLoop } from "./loop.mjs";
6
+ export { initialize, loadRuntime } from "./config.mjs";
7
+ export { FOUNDING_ROSTER, initializeCohort, cohortStatus } from "./cohort.mjs";
8
+ export { ContinuityMemory, ContinuityError, MemoryContinuityStore, verifyContinuity, CONTINUITY_PROTOCOL } from "./continuity.mjs";
9
+ export { PeerGovernance, GovernanceError, verifyGovernanceDocument, createActionBudget, CLAIM_TYPES, ACTION_MANIFEST } from "./governance.mjs";
10
+ export { CollaborationScheduler, CollaborationError, createCollaboration, createRoster, rosterFromModels, createFilePersistence, createMemoryPersistence, createContinuity, memoryFromContinuity, validateProposal, validateUnifiedDiff, validatePatchPath, hmacIntegrity } from "./collaboration.mjs";
11
+ export { createProfile, verifyProfile, proposeAlias, encode, decode, benchmark, contribution, signDocument, verifyDocument, digest, BASE_PROTOCOL } from "./profiles.mjs";
12
+ export { Keychain, localIdFromKey, CarrierClient, seal, open, decrypt, envelopeSha } from "./pillar.mjs";
13
+ export { MemoryLoopbackTransport, PillarCarrierTransport } from "./transport.mjs";
14
+ export { classifyThread, assertChannelSendAdmission, assertChannelReceiveAdmission, assertForwardingPermission, validateTenantBinding, CANONICAL_CHANNEL_REGEX, RESERVED_CHANNEL_PREFIX } from "./channels.mjs";
15
+ export { classifyOperationTier, verifyPrincipalGovernance, MockPrincipalStatusVerifier } from "./governance-scope.mjs";
package/src/loop.mjs ADDED
@@ -0,0 +1,71 @@
1
+ import { setTimeout as delay } from "node:timers/promises";
2
+ import { randomUUID } from "node:crypto";
3
+ const DEFAULT_BUDGET={maxSends:100,maxBytesOut:256000,maxRepliesPerPeer:8,maxWallClockMs:900000,windowMs:3600000,windowMaxSends:200,windowMaxBytes:2000000,windowMaxRepliesPerPeer:40};
4
+ const SPONTANEOUS_KEYS=new Set(["recipient","profileId","namespace","body","thread","kind"]);
5
+ /**
6
+ * Bounded agent loop. Budget (all enforced BEFORE a send):
7
+ * - per-run: maxSends, maxBytesOut, maxRepliesPerPeer (stops reply ping-pong), maxWallClockMs
8
+ * - per-window, persisted in runtime.store (survives loop restarts): windowMaxSends/Bytes/RepliesPerPeer
9
+ * The agent cannot raise these: callers pass `budget`; values are clamped to hard ceilings.
10
+ * Spontaneous actions may only be plain "message" sends to a peer already in policy.
11
+ */
12
+ export async function runAgentLoop({runtime,respond,spontaneous=null,maxTurns=16,intervalMs=2000,signal,onEvent=()=>{},budget={}}){
13
+ if(!runtime||typeof respond!=="function"||!Number.isInteger(maxTurns)||maxTurns<1||maxTurns>100||!Number.isInteger(intervalMs)||intervalMs<500)throw new Error("Invalid bounded agent loop");
14
+ const limits={...DEFAULT_BUDGET};
15
+ for(const [k,v] of Object.entries(budget??{})){if(!(k in DEFAULT_BUDGET)||!Number.isSafeInteger(v)||v<1||v>DEFAULT_BUDGET[k])throw new Error(`Invalid loop budget ${k}`);limits[k]=v}
16
+ const started=Date.now(),run={sends:0,bytes:0,peers:{}},store=runtime.store&&typeof runtime.store.get==="function"?runtime.store:null;
17
+ const record=(reason,extra={})=>{try{store?.append?.(randomUUID(),{kind:"loop-budget-stop",reason,...extra})}catch{}};
18
+ // Returns a refusal reason or null, and reserves the budget when allowed.
19
+ const reserve=(peer,bytes)=>{
20
+ if(Date.now()-started>=limits.maxWallClockMs)return "wall-clock";
21
+ if(run.sends+1>limits.maxSends)return "run-sends";
22
+ if(run.bytes+bytes>limits.maxBytesOut)return "run-bytes";
23
+ if((run.peers[peer]??0)+1>limits.maxRepliesPerPeer)return "run-peer-replies";
24
+ let meter=null;
25
+ if(store){
26
+ meter=store.get("loopMeter",null);const now=Date.now();
27
+ if(!meter||now-meter.start>=limits.windowMs)meter={start:now,sends:0,bytes:0,peers:{}};
28
+ if(meter.sends+1>limits.windowMaxSends)return "window-sends";
29
+ if(meter.bytes+bytes>limits.windowMaxBytes)return "window-bytes";
30
+ if((meter.peers[peer]??0)+1>limits.windowMaxRepliesPerPeer)return "window-peer-replies";
31
+ meter.sends++;meter.bytes+=bytes;meter.peers[peer]=(meter.peers[peer]??0)+1;
32
+ if(Object.keys(meter.peers).length>1000)return "window-peers";
33
+ store.set("loopMeter",meter);
34
+ }
35
+ run.sends++;run.bytes+=bytes;run.peers[peer]=(run.peers[peer]??0)+1;return null;
36
+ };
37
+ let turns=0,stopped=null;
38
+ const send=async(action)=>{
39
+ const reason=reserve(String(action.recipient),Buffer.byteLength(String(action.body??"")));
40
+ if(reason){stopped=`budget:${reason}`;record(reason,{peer:String(action.recipient).slice(0,120)});onEvent({rejected:true,state:"budget-refused",reason});return false}
41
+ onEvent(await runtime.send(action));return true;
42
+ };
43
+ while(turns<maxTurns&&!signal?.aborted&&!stopped){
44
+ const packets=await runtime.poll();
45
+ for(const packet of packets){
46
+ if(packet.duplicate||packet.rejected||packet.deferred||typeof packet.body!=="string"){onEvent(packet);continue}
47
+ let reply;
48
+ try{reply=await respond({packet,status:runtime.status(),signal})}
49
+ catch(error){turns++;onEvent({rejected:true,state:"responder-failed",reason:String(error?.message??error).slice(0,200)});if(turns>=maxTurns||signal?.aborted)break;continue}
50
+ turns++;
51
+ if(reply!==null&&reply!==undefined){
52
+ if(typeof reply!=="string")throw new Error("Agent responder must return bounded plain text or null");
53
+ if(!await send({recipient:packet.sender,profileId:packet.profileId,thread:packet.thread,body:reply}))break;
54
+ }
55
+ if(turns>=maxTurns||signal?.aborted)break;
56
+ }
57
+ if(stopped)break;
58
+ if(!packets.length&&spontaneous&&turns<maxTurns){
59
+ const action=await spontaneous({status:runtime.status(),signal});
60
+ turns++;
61
+ if(action){
62
+ const allowedPeers=runtime.policy?.agents;
63
+ if(typeof action!=="object"||Object.keys(action).some(k=>!SPONTANEOUS_KEYS.has(k))||(action.kind!==undefined&&action.kind!=="message")||typeof action.recipient!=="string"||typeof action.body!=="string"||(allowedPeers&&!Object.hasOwn(allowedPeers,action.recipient))){onEvent({rejected:true,state:"spontaneous-action-refused"});record("spontaneous-action-refused")}
64
+ else if(!await send(action))break;
65
+ }
66
+ }
67
+ await runtime.flush();
68
+ if(turns<maxTurns&&!signal?.aborted)try{await delay(intervalMs,null,{signal})}catch(error){if(error.name!=="AbortError")throw error}
69
+ }
70
+ return {turns,stopped:stopped??(signal?.aborted?"aborted":"turn-limit"),usage:{...run,peers:undefined}};
71
+ }
@@ -0,0 +1,7 @@
1
+ export const OFFLINE_ENV: "AGENT_COMMONS_OFFLINE";
2
+ export function isLoopbackHost(hostname: string): boolean;
3
+ export function isLoopbackUrl(url: string): boolean;
4
+ export function isOffline(
5
+ env?: Record<string, string | undefined>,
6
+ policy?: { mode?: string; offline?: boolean },
7
+ ): boolean;
@@ -0,0 +1,29 @@
1
+ // Offline mode. Default on for a local run.
2
+ // AGENT_COMMONS_OFFLINE=1 forces offline, including policy.mode "global".
3
+ // AGENT_COMMONS_OFFLINE=0 is the only opt-in that allows outbound calls.
4
+ // Unset: offline, unless policy.offline is false, or policy.mode is "global"
5
+ // and the policy does not set offline. Loopback is a local carrier, not egress.
6
+
7
+ export const OFFLINE_ENV = "AGENT_COMMONS_OFFLINE";
8
+
9
+ export function isLoopbackHost(hostname) {
10
+ const host = String(hostname ?? "").toLowerCase().replace(/^\[|\]$/g, "");
11
+ return host === "127.0.0.1" || host === "localhost" || host === "::1";
12
+ }
13
+
14
+ export function isLoopbackUrl(url) {
15
+ try {
16
+ return isLoopbackHost(new URL(String(url)).hostname);
17
+ } catch {
18
+ return false;
19
+ }
20
+ }
21
+
22
+ export function isOffline(env = process.env, policy) {
23
+ const flag = env?.[OFFLINE_ENV];
24
+ if (flag === "1") return true;
25
+ if (flag === "0") return false;
26
+ if (policy && Object.prototype.hasOwnProperty.call(policy, "offline")) return policy.offline !== false;
27
+ if (policy?.mode === "global") return false;
28
+ return true;
29
+ }
package/src/pillar.mjs ADDED
@@ -0,0 +1,59 @@
1
+ // Unmodified upstream modules are covered by NOTICE and PROVENANCE.json.
2
+ // CarrierClient is wrapped here. The vendored module stays byte-identical to
3
+ // @uuaid/pillar 2.0.2. Offline refusal happens before any vendored method runs.
4
+ import { CarrierClient as VendoredCarrierClient } from "./vendor/pillar/net/carrier-client.mjs";
5
+ import { isLoopbackUrl, isOffline } from "./offline.mjs";
6
+
7
+ export { Keychain, localIdFromKey } from "./vendor/pillar/identity/keychain.mjs";
8
+ export { jcs } from "./vendor/pillar/identity/jcs.mjs";
9
+ export { seal, open, decrypt, envelopeSha, ENVELOPE_VERSION } from "./vendor/pillar/net/envelope.mjs";
10
+
11
+ function refusedCarrier(urls, env) {
12
+ if (!isOffline(env)) return null;
13
+ for (const url of urls ?? []) {
14
+ if (!isLoopbackUrl(url)) return url;
15
+ }
16
+ return null;
17
+ }
18
+
19
+ function offlineError(url) {
20
+ const error = new Error(`offline: refused outbound carrier call to ${url}`);
21
+ error.code = "offline";
22
+ error.carrier = url;
23
+ return error;
24
+ }
25
+
26
+ export class CarrierClient extends VendoredCarrierClient {
27
+ constructor(options = {}) {
28
+ super(options);
29
+ // Upstream pillar 2.0.2 does not store env. The package seam does.
30
+ this._offlineEnv = options.env ?? process.env;
31
+ }
32
+
33
+ async deliver(envelope, options) {
34
+ const blocked = refusedCarrier(this.carriers, this._offlineEnv);
35
+ if (blocked) throw offlineError(blocked);
36
+ return super.deliver(envelope, options);
37
+ }
38
+
39
+ async fetchInbox(base, options) {
40
+ const blocked = refusedCarrier([base], this._offlineEnv);
41
+ if (blocked) throw offlineError(blocked);
42
+ return super.fetchInbox(base, options);
43
+ }
44
+
45
+ startPolling(options = {}) {
46
+ const blocked = refusedCarrier(this.carriers, this._offlineEnv);
47
+ if (blocked) {
48
+ const error = offlineError(blocked);
49
+ options.onError?.(error, { carrier: blocked });
50
+ const cursors = { ...(options.cursors ?? {}) };
51
+ return {
52
+ stop() {},
53
+ cursors: () => ({ ...cursors }),
54
+ done: Promise.resolve([]),
55
+ };
56
+ }
57
+ return super.startPolling(options);
58
+ }
59
+ }
@@ -0,0 +1,12 @@
1
+ import type { Profile, Keychain } from "./index.d.ts";
2
+ export function createProfile(options:{namespace:string;name:string;fixtures:string[];scope?:"local"|"tenant"|"global";lexicon?:Record<string,string>;parent?:string|null;revision?:number;quorum?:number}):Profile;
3
+ export function verifyProfile(profile:Profile):Profile;
4
+ export function proposeAlias(profile:Profile,phrase:string):any;
5
+ export function contribution(profile:Profile,options?:{includeFixtures?:boolean}):any;
6
+ export function signDocument(keychain:Keychain,kind:string,payload:any):any;
7
+ export function digest(value:any):string;
8
+ export function benchmark(lexicon:Record<string,string>,fixtures:string[]):Profile["benchmark"];
9
+ export function encode(text:string,lexicon:Record<string,string>):string;
10
+ export function decode(text:string,lexicon:Record<string,string>):string;
11
+ export function verifyDocument(document:any):any;
12
+ export const BASE_PROTOCOL:"agent-commons/1";
@@ -0,0 +1,91 @@
1
+ import { createHash, randomUUID } from "node:crypto";
2
+ import { jcs, Keychain } from "./pillar.mjs";
3
+
4
+ export const BASE_PROTOCOL = "agent-commons/1";
5
+ export function digest(value) { assertJSON(value);return createHash("sha256").update(jcs(value)).digest("hex"); }
6
+ export function assertJSON(value) {
7
+ if (value === null || typeof value === "string" || typeof value === "boolean") return;
8
+ if (typeof value === "number" && Number.isSafeInteger(value)) return;
9
+ if (Array.isArray(value)) return value.forEach(assertJSON);
10
+ if (value && Object.getPrototypeOf(value) === Object.prototype) {
11
+ for (const [key, item] of Object.entries(value)) {
12
+ if (!/^[\x20-\x7e]+$/.test(key) || ["__proto__", "prototype", "constructor"].includes(key)) throw new Error("Unsafe canonical key");
13
+ assertJSON(item);
14
+ }
15
+ return;
16
+ }
17
+ throw new Error("Expected canonical JSON; decimal measurements must be strings");
18
+ }
19
+ export function encode(body, lexicon) {
20
+ let wire=body.replaceAll("~","~~");
21
+ for (const [phrase,alias] of Object.entries(lexicon).sort((a,b)=>b[0].length-a[0].length)) wire=wire.split(phrase).join(alias);
22
+ return wire;
23
+ }
24
+ export function decode(wire, lexicon) {
25
+ const reverse=new Map(Object.entries(lexicon).map(([k,v])=>[v,k]));
26
+ return wire.replace(/~~|~\d+~/g,t=>t==="~~"?"~":reverse.get(t)??t);
27
+ }
28
+ export function validateLexicon(lexicon) {
29
+ if (!lexicon || typeof lexicon!=="object" || Array.isArray(lexicon) || Object.keys(lexicon).length>128) throw new Error("Invalid lexicon");
30
+ const symbols=new Set();
31
+ for (const [phrase,symbol] of Object.entries(lexicon)) {
32
+ if (!/^[A-Za-z][A-Za-z ]{7,79}$/.test(phrase) || phrase!==phrase.trim() || !/^~\d{1,3}~$/.test(symbol) || symbols.has(symbol)) throw new Error("Invalid or colliding alias");
33
+ symbols.add(symbol);
34
+ }
35
+ }
36
+ export function benchmark(lexicon, fixtures) {
37
+ validateLexicon(lexicon);
38
+ let originalBytes=0,wireBytes=0,passed=0;
39
+ for (const fixture of fixtures) {
40
+ const wire=encode(fixture,lexicon);
41
+ if(decode(wire,lexicon)===fixture)passed++;
42
+ originalBytes+=Buffer.byteLength(fixture);wireBytes+=Buffer.byteLength(wire);
43
+ }
44
+ return {tests:fixtures.length,passed,originalBytes,wireBytes,reductionBps:originalBytes?Math.round(10000*(originalBytes-wireBytes)/originalBytes):0};
45
+ }
46
+ const safetyFixtures=["","~0~","~~","~123~","தமிழ் மொழி","🧬","\n\\\"", "a".repeat(128)];
47
+ export function createProfile({ namespace, name, fixtures, scope="local", lexicon={}, parent=null, revision=0, quorum=2 }) {
48
+ if (!/^[a-z0-9][a-z0-9._/-]{1,119}$/.test(namespace)||typeof name!=="string"||name.length<2||name.length>120) throw new Error("Invalid profile name or namespace");
49
+ if (!["local","tenant","global"].includes(scope)) throw new Error("Invalid profile scope");
50
+ if(!Array.isArray(fixtures)||fixtures.length<2||fixtures.length>100||fixtures.some(f=>typeof f!=="string"||f.length>8000)||Buffer.byteLength(JSON.stringify(fixtures))>256000) throw new Error("Provide 2–100 bounded text fixtures");
51
+ if(!Number.isSafeInteger(revision)||revision<0||!Number.isSafeInteger(quorum)||quorum<2||quorum>10)throw new Error("Invalid profile revision or quorum");
52
+ validateLexicon(lexicon);
53
+ const report=benchmark(lexicon,[...fixtures,...safetyFixtures]);
54
+ if(report.tests!==report.passed)throw new Error("Profile fails lossless gate");
55
+ const document={v:BASE_PROTOCOL,namespace,name,scope,parent,revision,quorum,lexicon,fixtureHash:digest(fixtures),fixtures};
56
+ assertJSON(document);
57
+ return {...document,id:digest(document),benchmark:report};
58
+ }
59
+ export function verifyProfile(profile) {
60
+ const allowed=new Set(["v","namespace","name","scope","parent","revision","quorum","lexicon","fixtureHash","fixtures","id","benchmark"]);
61
+ if(Object.keys(profile??{}).some(key=>!allowed.has(key)))throw new Error("Unknown profile field");
62
+ const rebuilt=createProfile(profile);
63
+ if(rebuilt.id!==profile.id || digest(rebuilt.benchmark)!==digest(profile.benchmark))throw new Error("Profile digest or benchmark mismatch");
64
+ return rebuilt;
65
+ }
66
+ export function proposeAlias(profile, phrase) {
67
+ verifyProfile(profile);
68
+ if(Object.hasOwn(profile.lexicon,phrase))throw new Error("Alias already exists");
69
+ const numbers=Object.values(profile.lexicon).map(s=>Number(s.slice(1,-1)));
70
+ const alias=`~${numbers.length?Math.max(...numbers)+1:0}~`;
71
+ const candidate=createProfile({...profile,lexicon:{...profile.lexicon,[phrase]:alias},parent:profile.id,revision:profile.revision+1});
72
+ if(candidate.benchmark.wireBytes>=profile.benchmark.wireBytes)throw new Error("Candidate does not improve the local benchmark");
73
+ return {id:randomUUID(),parent:profile.id,candidate,phrase,alias,status:"pending",votes:{}};
74
+ }
75
+ export function signDocument(keychain, kind, payload) {
76
+ assertJSON(payload);
77
+ const body={v:BASE_PROTOCOL,id:randomUUID(),kind,issuer:keychain._identity.uuaid,publicKey:keychain._identity.publicKeyHex,createdAt:new Date().toISOString(),payload};
78
+ return {...body,signature:keychain.sign(Buffer.from(jcs(body))).toString("hex")};
79
+ }
80
+ export function verifyDocument(document) {
81
+ assertJSON(document);
82
+ const {signature,...body}=document;
83
+ if(body.v!==BASE_PROTOCOL||!["profile-proposal","profile-vote","profile-contribution","profile-recovery"].includes(body.kind)||!/^uuaid:[a-z0-9-]+:agent:[0-9a-f-]{36}$/.test(body.issuer)||!/^[0-9a-f]{64}$/.test(body.publicKey)||!/^[0-9a-f]{128}$/.test(signature))throw new Error("Invalid agent document");
84
+ if(Keychain.localIdFromKey(Buffer.from(body.publicKey,"hex"))!==body.issuer.split(":")[3])throw new Error("Agent key is not bound to UUAID");
85
+ if(!Keychain.verifyDetached(body.publicKey,Buffer.from(jcs(body)),Buffer.from(signature,"hex")))throw new Error("Invalid document signature");
86
+ return body;
87
+ }
88
+ export function contribution(profile, { includeFixtures=false }={}) {
89
+ verifyProfile(profile);
90
+ return {profileId:profile.id,namespace:profile.namespace,parent:profile.parent,base:BASE_PROTOCOL,lexicon:profile.lexicon,fixtureHash:profile.fixtureHash,benchmark:profile.benchmark,visibility:includeFixtures?"shared-fixtures":"metadata-only",...(includeFixtures?{profile}:{}),stage:"candidate",standardization:"requires-IAASO-review"};
91
+ }