@svsprotocol/solana 0.1.0
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/LICENSE +158 -0
- package/README.md +365 -0
- package/dist/action-production-proof-evidence.js +553 -0
- package/dist/adapter-catalog.d.ts +29 -0
- package/dist/adapter-catalog.js +146 -0
- package/dist/adapter-core.d.ts +48 -0
- package/dist/adapter-core.js +249 -0
- package/dist/approval-signature.js +197 -0
- package/dist/base58.js +69 -0
- package/dist/bot-auth.js +50 -0
- package/dist/bot-certification-evidence.js +342 -0
- package/dist/bot-first-action-runbook.js +299 -0
- package/dist/bot-integration-contract.js +41 -0
- package/dist/certified-submit-status.js +176 -0
- package/dist/common.d.ts +1135 -0
- package/dist/elizaos.d.ts +43 -0
- package/dist/elizaos.js +227 -0
- package/dist/goat.d.ts +47 -0
- package/dist/goat.js +261 -0
- package/dist/index.d.ts +330 -0
- package/dist/index.js +128 -0
- package/dist/protocol.d.ts +205 -0
- package/dist/protocol.js +900 -0
- package/dist/receipt.js +51 -0
- package/dist/signed-proof-read-protection.js +495 -0
- package/dist/solana-agent-kit.d.ts +35 -0
- package/dist/solana-agent-kit.js +151 -0
- package/dist/svs-client.js +1232 -0
- package/dist/vercel-ai.d.ts +47 -0
- package/dist/vercel-ai.js +266 -0
- package/dist/verified-agent-adoption-kit.js +471 -0
- package/dist/verified-agent-profile.js +329 -0
- package/dist/verified-agent-registry-consumer.js +421 -0
- package/dist/verified-agent-registry.d.ts +36 -0
- package/dist/verified-agent-registry.js +826 -0
- package/dist/verified-agent-trust-score.js +335 -0
- package/dist/webhooks.js +834 -0
- package/package.json +72 -0
package/dist/bot-auth.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { createHmac, timingSafeEqual } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_BOT_REQUEST_TIMESTAMP_MAX_AGE_MS = 5 * 60 * 1000;
|
|
4
|
+
export const BOT_REQUEST_SIGNATURE_VERSION = "v1";
|
|
5
|
+
|
|
6
|
+
export function createBotRequestSignature({ body, timestamp, secret } = {}) {
|
|
7
|
+
if (!secret) {
|
|
8
|
+
throw new Error("secret is required.");
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
if (!timestamp) {
|
|
12
|
+
throw new Error("timestamp is required.");
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const rawBody = body === undefined || body === null ? "" : String(body);
|
|
16
|
+
const digest = createHmac("sha256", secret)
|
|
17
|
+
.update(`${timestamp}.${rawBody}`)
|
|
18
|
+
.digest("hex");
|
|
19
|
+
|
|
20
|
+
return `${BOT_REQUEST_SIGNATURE_VERSION}=${digest}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function verifyBotRequestSignature({
|
|
24
|
+
body,
|
|
25
|
+
timestamp,
|
|
26
|
+
signature,
|
|
27
|
+
secret,
|
|
28
|
+
now = new Date(),
|
|
29
|
+
maxAgeMs = DEFAULT_BOT_REQUEST_TIMESTAMP_MAX_AGE_MS
|
|
30
|
+
} = {}) {
|
|
31
|
+
if (!signature || !timestamp || !secret) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const requestTime = new Date(timestamp);
|
|
36
|
+
if (Number.isNaN(requestTime.getTime())) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (Math.abs(now.getTime() - requestTime.getTime()) > maxAgeMs) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const expected = createBotRequestSignature({ body, timestamp, secret });
|
|
45
|
+
const expectedBuffer = Buffer.from(expected);
|
|
46
|
+
const actualBuffer = Buffer.from(signature);
|
|
47
|
+
|
|
48
|
+
return expectedBuffer.length === actualBuffer.length &&
|
|
49
|
+
timingSafeEqual(expectedBuffer, actualBuffer);
|
|
50
|
+
}
|
|
@@ -0,0 +1,342 @@
|
|
|
1
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { hashObject } from "./receipt.js";
|
|
4
|
+
|
|
5
|
+
export const BOT_CERTIFICATION_EVIDENCE_VERSION = "svs.bot-certification-evidence.v1";
|
|
6
|
+
export const BOT_CERTIFICATION_EVIDENCE_VERIFICATION_VERSION = "svs.bot-certification-evidence-verification.v1";
|
|
7
|
+
export const DEFAULT_BOT_CERTIFICATION_EVIDENCE_PATH = "./data/security/bot-certification-evidence.json";
|
|
8
|
+
|
|
9
|
+
export function createBotCertificationEvidenceFromResult({
|
|
10
|
+
result,
|
|
11
|
+
generatedAt = new Date()
|
|
12
|
+
} = {}) {
|
|
13
|
+
const certification = result?.certification ?? {};
|
|
14
|
+
const unsigned = {
|
|
15
|
+
version: BOT_CERTIFICATION_EVIDENCE_VERSION,
|
|
16
|
+
generatedAt: generatedAt.toISOString(),
|
|
17
|
+
source: {
|
|
18
|
+
resultVersion: result?.version ?? null,
|
|
19
|
+
serverUrl: result?.serverUrl ?? null
|
|
20
|
+
},
|
|
21
|
+
reportSafety: {
|
|
22
|
+
secretsIncluded: false,
|
|
23
|
+
redactedFields: [
|
|
24
|
+
"SVS_BOT_API_KEY",
|
|
25
|
+
"SVS_BOT_REQUEST_SIGNING_SECRET",
|
|
26
|
+
"requestSigningSecret",
|
|
27
|
+
"apiKey",
|
|
28
|
+
"webhookSecret",
|
|
29
|
+
"svs-request-signature"
|
|
30
|
+
]
|
|
31
|
+
},
|
|
32
|
+
certificationCheck: {
|
|
33
|
+
ok: result?.ok === true,
|
|
34
|
+
status: result?.status ?? null,
|
|
35
|
+
checkedAt: result?.checkedAt ?? null,
|
|
36
|
+
botId: result?.botId ?? certification?.botId ?? null,
|
|
37
|
+
integrationContract: summarizeIntegrationContractBinding(result?.integrationContract ?? result?.contract),
|
|
38
|
+
nextAction: result?.nextAction ?? certification?.nextAction ?? null
|
|
39
|
+
},
|
|
40
|
+
certification: {
|
|
41
|
+
version: certification?.version ?? null,
|
|
42
|
+
ok: certification?.ok === true,
|
|
43
|
+
status: certification?.status ?? null,
|
|
44
|
+
botId: certification?.botId ?? result?.botId ?? null,
|
|
45
|
+
certificationHash: certification?.certification?.certificationHash ??
|
|
46
|
+
certification?.certificationHash ??
|
|
47
|
+
null,
|
|
48
|
+
recordId: certification?.certification?.recordId ??
|
|
49
|
+
certification?.recordId ??
|
|
50
|
+
null,
|
|
51
|
+
found: certification?.certification?.found === true ||
|
|
52
|
+
certification?.found === true,
|
|
53
|
+
botMatches: certification?.certification?.botMatches === true ||
|
|
54
|
+
certification?.botMatches === true,
|
|
55
|
+
quality: summarizeBotIntegrationQuality(certification?.quality ?? result?.quality ?? certification?.certification?.quality)
|
|
56
|
+
},
|
|
57
|
+
qualityTarget: summarizeBotIntegrationQualityTarget(certification?.qualityTarget ?? result?.qualityTarget),
|
|
58
|
+
proofs: {
|
|
59
|
+
verificationSetHash: certification?.proofs?.verificationSetHash ??
|
|
60
|
+
certification?.evidence?.verificationSetHash ??
|
|
61
|
+
null,
|
|
62
|
+
botIntegrationReportHash: certification?.proofs?.botIntegrationReportHash ??
|
|
63
|
+
certification?.evidence?.botIntegrationReportHash ??
|
|
64
|
+
null,
|
|
65
|
+
integrationContractHash: result?.integrationContract?.certifiedContractHash ??
|
|
66
|
+
certification?.proofs?.integrationContractHash ??
|
|
67
|
+
certification?.evidence?.integrationContractHash ??
|
|
68
|
+
null,
|
|
69
|
+
currentIntegrationContractHash: result?.integrationContract?.currentContractHash ??
|
|
70
|
+
certification?.proofs?.currentIntegrationContractHash ??
|
|
71
|
+
result?.proofs?.currentIntegrationContractHash ??
|
|
72
|
+
null,
|
|
73
|
+
integrationContractCurrent: result?.integrationContract
|
|
74
|
+
? result.integrationContract.ok === true
|
|
75
|
+
: certification?.proofs?.integrationContractCurrent === true ||
|
|
76
|
+
result?.proofs?.integrationContractCurrent === true,
|
|
77
|
+
actionRecordVerificationOk: certification?.proofs?.actionRecordVerificationOk === true ||
|
|
78
|
+
certification?.evidence?.actionRecordVerificationOk === true,
|
|
79
|
+
receiptRegistrySubmissionVerificationOk: certification?.proofs?.receiptRegistrySubmissionVerificationOk === true ||
|
|
80
|
+
certification?.evidence?.receiptRegistrySubmissionVerificationOk === true,
|
|
81
|
+
portableSetVerified: certification?.proofs?.portableSetVerified === true ||
|
|
82
|
+
certification?.evidence?.portableSetVerified === true,
|
|
83
|
+
secretsIncluded: certification?.proofs?.secretsIncluded === true ||
|
|
84
|
+
certification?.evidence?.secretsIncluded === true
|
|
85
|
+
},
|
|
86
|
+
freshness: {
|
|
87
|
+
fresh: certification?.freshness?.fresh === true,
|
|
88
|
+
stale: certification?.freshness?.stale === true,
|
|
89
|
+
ageMs: Number.isFinite(certification?.freshness?.ageMs) ? certification.freshness.ageMs : null,
|
|
90
|
+
staleAfterMs: Number.isFinite(certification?.freshness?.staleAfterMs)
|
|
91
|
+
? certification.freshness.staleAfterMs
|
|
92
|
+
: null
|
|
93
|
+
},
|
|
94
|
+
nextAction: result?.nextAction ?? certification?.nextAction ?? null
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
...unsigned,
|
|
99
|
+
evidenceHash: hashBotCertificationEvidence(unsigned)
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export async function persistBotCertificationEvidence({
|
|
104
|
+
result,
|
|
105
|
+
outputPath = DEFAULT_BOT_CERTIFICATION_EVIDENCE_PATH,
|
|
106
|
+
generatedAt = new Date()
|
|
107
|
+
} = {}) {
|
|
108
|
+
const evidence = createBotCertificationEvidenceFromResult({ result, generatedAt });
|
|
109
|
+
const text = `${JSON.stringify(evidence, null, 2)}\n`;
|
|
110
|
+
|
|
111
|
+
await mkdir(dirname(outputPath), { recursive: true });
|
|
112
|
+
await writeFile(outputPath, text);
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
path: outputPath,
|
|
116
|
+
bytes: Buffer.byteLength(text, "utf8"),
|
|
117
|
+
evidence
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function verifyBotCertificationEvidence(evidence, {
|
|
122
|
+
requireOk = true,
|
|
123
|
+
staleAfterMs = null,
|
|
124
|
+
now = new Date()
|
|
125
|
+
} = {}) {
|
|
126
|
+
const freshness = checkBotCertificationEvidenceFreshness({
|
|
127
|
+
generatedAt: evidence?.generatedAt,
|
|
128
|
+
staleAfterMs,
|
|
129
|
+
now
|
|
130
|
+
});
|
|
131
|
+
const recomputedHash = hashBotCertificationEvidence(evidence);
|
|
132
|
+
const checks = [
|
|
133
|
+
check(
|
|
134
|
+
"Bot certification evidence version is supported",
|
|
135
|
+
evidence?.version === BOT_CERTIFICATION_EVIDENCE_VERSION,
|
|
136
|
+
evidence?.version ?? "missing"
|
|
137
|
+
),
|
|
138
|
+
check(
|
|
139
|
+
"Bot certification evidence hash is valid",
|
|
140
|
+
evidence?.evidenceHash === recomputedHash,
|
|
141
|
+
`declared=${evidence?.evidenceHash ?? "missing"} recomputed=${recomputedHash ?? "missing"}`
|
|
142
|
+
),
|
|
143
|
+
check(
|
|
144
|
+
"Bot certification evidence excludes secrets",
|
|
145
|
+
evidence?.reportSafety?.secretsIncluded === false && evidence?.proofs?.secretsIncluded !== true,
|
|
146
|
+
`reportSafety=${evidence?.reportSafety?.secretsIncluded ?? "missing"} proofs=${evidence?.proofs?.secretsIncluded ?? "missing"}`
|
|
147
|
+
),
|
|
148
|
+
check(
|
|
149
|
+
"Bot certification command passed",
|
|
150
|
+
!requireOk || evidence?.certificationCheck?.ok === true,
|
|
151
|
+
`status=${evidence?.certificationCheck?.status ?? "missing"}`
|
|
152
|
+
),
|
|
153
|
+
check(
|
|
154
|
+
"Bot integration certification is ready",
|
|
155
|
+
!requireOk ||
|
|
156
|
+
evidence?.certification?.ok === true &&
|
|
157
|
+
["ready", "certified"].includes(evidence?.certification?.status),
|
|
158
|
+
`status=${evidence?.certification?.status ?? "missing"}`
|
|
159
|
+
),
|
|
160
|
+
check(
|
|
161
|
+
"Bot certification references a matching certified bot",
|
|
162
|
+
!requireOk || evidence?.certification?.found === true && evidence?.certification?.botMatches === true,
|
|
163
|
+
`found=${evidence?.certification?.found ?? "missing"} botMatches=${evidence?.certification?.botMatches ?? "missing"}`
|
|
164
|
+
),
|
|
165
|
+
check(
|
|
166
|
+
"Bot certification action record proof passed",
|
|
167
|
+
!requireOk || evidence?.proofs?.actionRecordVerificationOk === true,
|
|
168
|
+
`ok=${evidence?.proofs?.actionRecordVerificationOk ?? "missing"}`
|
|
169
|
+
),
|
|
170
|
+
check(
|
|
171
|
+
"Bot certification custom registry proof passed",
|
|
172
|
+
!requireOk || evidence?.proofs?.receiptRegistrySubmissionVerificationOk === true,
|
|
173
|
+
`ok=${evidence?.proofs?.receiptRegistrySubmissionVerificationOk ?? "missing"}`
|
|
174
|
+
),
|
|
175
|
+
check(
|
|
176
|
+
"Bot certification portable set proof passed",
|
|
177
|
+
!requireOk || evidence?.proofs?.portableSetVerified === true,
|
|
178
|
+
`ok=${evidence?.proofs?.portableSetVerified ?? "missing"}`
|
|
179
|
+
),
|
|
180
|
+
check(
|
|
181
|
+
"Bot certification integration contract binding passed",
|
|
182
|
+
!requireOk ||
|
|
183
|
+
!evidence?.certificationCheck?.integrationContract ||
|
|
184
|
+
evidence.certificationCheck.integrationContract.ok === true,
|
|
185
|
+
`status=${evidence?.certificationCheck?.integrationContract?.status ?? "missing"} local=${evidence?.certificationCheck?.integrationContract?.localContractHash ?? "missing"} certified=${evidence?.certificationCheck?.integrationContract?.certifiedContractHash ?? "missing"}`
|
|
186
|
+
),
|
|
187
|
+
check(
|
|
188
|
+
"Bot certification quality target passed",
|
|
189
|
+
!requireOk || evidence?.qualityTarget?.ready === true &&
|
|
190
|
+
evidence.qualityTarget.score === evidence.qualityTarget.targetScore &&
|
|
191
|
+
evidence.qualityTarget.status === "excellent",
|
|
192
|
+
`ready=${evidence?.qualityTarget?.ready ?? "missing"} score=${evidence?.qualityTarget?.score ?? "missing"} target=${evidence?.qualityTarget?.targetScore ?? "missing"} status=${evidence?.qualityTarget?.status ?? "missing"}`
|
|
193
|
+
)
|
|
194
|
+
];
|
|
195
|
+
|
|
196
|
+
if (staleAfterMs !== null && staleAfterMs !== undefined) {
|
|
197
|
+
checks.push(check(
|
|
198
|
+
"Bot certification evidence is fresh",
|
|
199
|
+
!freshness.stale,
|
|
200
|
+
freshness.ageMs === null
|
|
201
|
+
? `generatedAt=${evidence?.generatedAt ?? "missing"}`
|
|
202
|
+
: `ageMs=${freshness.ageMs} staleAfterMs=${staleAfterMs}`
|
|
203
|
+
));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const failedChecks = checks.filter((item) => !item.ok);
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
version: BOT_CERTIFICATION_EVIDENCE_VERIFICATION_VERSION,
|
|
210
|
+
ok: failedChecks.length === 0,
|
|
211
|
+
status: failedChecks.length === 0 ? "verified" : freshness.stale ? "stale" : "failed",
|
|
212
|
+
checkedAt: now.toISOString(),
|
|
213
|
+
found: Boolean(evidence),
|
|
214
|
+
generatedAt: evidence?.generatedAt ?? null,
|
|
215
|
+
stale: freshness.stale,
|
|
216
|
+
ageMs: freshness.ageMs,
|
|
217
|
+
staleAfterMs,
|
|
218
|
+
evidenceHash: evidence?.evidenceHash ?? null,
|
|
219
|
+
computedEvidenceHash: recomputedHash,
|
|
220
|
+
botId: evidence?.certificationCheck?.botId ?? evidence?.certification?.botId ?? null,
|
|
221
|
+
certificationHash: evidence?.certification?.certificationHash ?? null,
|
|
222
|
+
recordId: evidence?.certification?.recordId ?? null,
|
|
223
|
+
qualityTarget: evidence?.qualityTarget ?? null,
|
|
224
|
+
failedCheckCount: failedChecks.length,
|
|
225
|
+
failedChecks,
|
|
226
|
+
checks
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function summarizeBotIntegrationQuality(quality) {
|
|
231
|
+
if (!quality || typeof quality !== "object") {
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
version: quality.version ?? null,
|
|
237
|
+
score: Number.isInteger(quality.score) ? quality.score : null,
|
|
238
|
+
maxScore: Number.isInteger(quality.maxScore) ? quality.maxScore : null,
|
|
239
|
+
status: quality.status ?? null,
|
|
240
|
+
passedFactorCount: Number.isInteger(quality.passedFactorCount) ? quality.passedFactorCount : null,
|
|
241
|
+
failedFactorCount: Number.isInteger(quality.failedFactorCount) ? quality.failedFactorCount : null,
|
|
242
|
+
missingFactorCodes: Array.isArray(quality.factors)
|
|
243
|
+
? quality.factors.filter((factor) => factor.ok !== true).map((factor) => factor.code ?? "unknown")
|
|
244
|
+
: [],
|
|
245
|
+
nextAction: quality.nextAction
|
|
246
|
+
? {
|
|
247
|
+
code: quality.nextAction.code ?? null,
|
|
248
|
+
message: quality.nextAction.message ?? null
|
|
249
|
+
}
|
|
250
|
+
: null
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function summarizeBotIntegrationQualityTarget(target) {
|
|
255
|
+
if (!target || typeof target !== "object") {
|
|
256
|
+
return null;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return {
|
|
260
|
+
version: target.version ?? null,
|
|
261
|
+
ready: target.ready === true,
|
|
262
|
+
score: Number.isInteger(target.score) ? target.score : null,
|
|
263
|
+
maxScore: Number.isInteger(target.maxScore) ? target.maxScore : null,
|
|
264
|
+
targetScore: Number.isInteger(target.targetScore) ? target.targetScore : null,
|
|
265
|
+
status: target.status ?? null,
|
|
266
|
+
missingFactorCount: Number.isInteger(target.missingFactorCount) ? target.missingFactorCount : null,
|
|
267
|
+
missingFactorCodes: Array.isArray(target.missingFactorCodes) ? target.missingFactorCodes : [],
|
|
268
|
+
nextAction: target.nextAction
|
|
269
|
+
? {
|
|
270
|
+
code: target.nextAction.code ?? null,
|
|
271
|
+
message: target.nextAction.message ?? null
|
|
272
|
+
}
|
|
273
|
+
: null
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function summarizeIntegrationContractBinding(binding) {
|
|
278
|
+
if (!binding || typeof binding !== "object") {
|
|
279
|
+
return null;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return {
|
|
283
|
+
version: binding.version ?? null,
|
|
284
|
+
required: binding.required === true,
|
|
285
|
+
ok: binding.ok === true || binding.status === "current",
|
|
286
|
+
status: binding.status ?? null,
|
|
287
|
+
localContractHash: binding.localContractHash ?? binding.currentHash ?? null,
|
|
288
|
+
dashboardContractHash: binding.dashboardContractHash ?? binding.currentHash ?? null,
|
|
289
|
+
certifiedContractHash: binding.certifiedContractHash ?? binding.certifiedHash ?? null,
|
|
290
|
+
currentContractHash: binding.currentContractHash ?? binding.currentHash ?? null,
|
|
291
|
+
drifted: binding.drifted === true,
|
|
292
|
+
contractVersion: binding.contractVersion ?? null
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function hashBotCertificationEvidence(evidence) {
|
|
297
|
+
if (!evidence || typeof evidence !== "object") {
|
|
298
|
+
return null;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
const { evidenceHash: _evidenceHash, ...unsigned } = evidence;
|
|
302
|
+
|
|
303
|
+
return hashObject(unsigned);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
export function checkBotCertificationEvidenceFreshness({
|
|
307
|
+
generatedAt,
|
|
308
|
+
staleAfterMs,
|
|
309
|
+
now = new Date()
|
|
310
|
+
} = {}) {
|
|
311
|
+
if (staleAfterMs === null || staleAfterMs === undefined) {
|
|
312
|
+
return {
|
|
313
|
+
stale: false,
|
|
314
|
+
ageMs: null
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const generatedTime = Date.parse(generatedAt);
|
|
319
|
+
const nowTime = now instanceof Date ? now.getTime() : Date.parse(now);
|
|
320
|
+
|
|
321
|
+
if (!Number.isFinite(generatedTime) || !Number.isFinite(nowTime)) {
|
|
322
|
+
return {
|
|
323
|
+
stale: true,
|
|
324
|
+
ageMs: null
|
|
325
|
+
};
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const ageMs = Math.max(0, nowTime - generatedTime);
|
|
329
|
+
|
|
330
|
+
return {
|
|
331
|
+
stale: ageMs > staleAfterMs,
|
|
332
|
+
ageMs
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function check(name, ok, detail) {
|
|
337
|
+
return {
|
|
338
|
+
name,
|
|
339
|
+
ok: Boolean(ok),
|
|
340
|
+
detail
|
|
341
|
+
};
|
|
342
|
+
}
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import {
|
|
3
|
+
BOT_INTEGRATION_CONTRACT_VERSION,
|
|
4
|
+
BOT_INTEGRATION_QUICKSTART_VERSION,
|
|
5
|
+
hashBotIntegrationContract
|
|
6
|
+
} from "./bot-integration-contract.js";
|
|
7
|
+
|
|
8
|
+
export const BOT_FIRST_ACTION_RUNBOOK_VERSION = "svs.bot-first-action-runbook.v1";
|
|
9
|
+
export const BOT_FIRST_ACTION_STEP_VERSION = "svs.bot-first-action-runbook-step.v1";
|
|
10
|
+
|
|
11
|
+
const DEFAULT_ENV_PATH = "./examples/external-bots/signed-devnet-memo-bot.local.env";
|
|
12
|
+
|
|
13
|
+
export function createBotFirstActionRunbook(contract, {
|
|
14
|
+
serverUrl = null,
|
|
15
|
+
envPath = null,
|
|
16
|
+
generatedAt = new Date().toISOString()
|
|
17
|
+
} = {}) {
|
|
18
|
+
const quickstart = validateQuickstart(contract);
|
|
19
|
+
const contractHash = hashBotIntegrationContract(contract);
|
|
20
|
+
const commands = quickstart.commands ?? {};
|
|
21
|
+
const resolvedEnvPath = envPath ?? quickstart.envFile?.localPath ?? DEFAULT_ENV_PATH;
|
|
22
|
+
const resolvedServerUrl = normalizeBaseUrl(
|
|
23
|
+
serverUrl ?? quickstart.envFile?.defaults?.SVS_SERVER_URL ?? "http://127.0.0.1:4173"
|
|
24
|
+
);
|
|
25
|
+
const commandSet = createCommandSet({
|
|
26
|
+
commands,
|
|
27
|
+
resolvedServerUrl,
|
|
28
|
+
resolvedEnvPath,
|
|
29
|
+
quickstartEnvPath: quickstart.envFile?.localPath ?? null,
|
|
30
|
+
quickstartServerUrl: quickstart.envFile?.defaults?.SVS_SERVER_URL ?? null
|
|
31
|
+
});
|
|
32
|
+
const runbook = {
|
|
33
|
+
version: BOT_FIRST_ACTION_RUNBOOK_VERSION,
|
|
34
|
+
generatedAt,
|
|
35
|
+
objective: "Move a new bot from local env setup to one human-approved, production-verifiable SVS action with the fewest manual handoffs.",
|
|
36
|
+
audience: [
|
|
37
|
+
"bot-builders",
|
|
38
|
+
"operators"
|
|
39
|
+
],
|
|
40
|
+
serverUrl: resolvedServerUrl,
|
|
41
|
+
envPath: resolvedEnvPath,
|
|
42
|
+
contract: {
|
|
43
|
+
version: contract.version,
|
|
44
|
+
quickstartVersion: quickstart.version,
|
|
45
|
+
hashAlgorithm: "sha256",
|
|
46
|
+
hash: contractHash,
|
|
47
|
+
source: quickstart.endpoints?.integrationContract ?? `${resolvedServerUrl}/api/bots/integration-contract`
|
|
48
|
+
},
|
|
49
|
+
noSecretGuarantees: [
|
|
50
|
+
"Does not include API keys.",
|
|
51
|
+
"Does not include request-signing secrets.",
|
|
52
|
+
"Does not include wallet private keys.",
|
|
53
|
+
"Only points to env fields and commands that the operator or bot runtime already controls."
|
|
54
|
+
],
|
|
55
|
+
commandSet,
|
|
56
|
+
orderedSteps: createOrderedSteps(commandSet),
|
|
57
|
+
fallbackPath: {
|
|
58
|
+
when: "The proof-wait submit exits before the operator finishes approval, broadcast, custom-registry registration, and proof export.",
|
|
59
|
+
commands: [
|
|
60
|
+
commandSet.certifiedSubmitStatus,
|
|
61
|
+
commandSet.certifiedSubmitPromote
|
|
62
|
+
].filter(Boolean),
|
|
63
|
+
operatorAction: "Approve, broadcast, verify action, register the custom receipt, then promote the certified-submit evidence."
|
|
64
|
+
},
|
|
65
|
+
successDefinition: {
|
|
66
|
+
botSide: "The proof-wait submit returns or later promotes a production-verified action with a fetched proof response.",
|
|
67
|
+
operatorSide: "Dashboard action lifecycle shows approved, broadcast, verified, custom registry registered, and exportable proof.",
|
|
68
|
+
auditorSide: "The resulting verification set includes action-production proof evidence and custom receipt-registry evidence."
|
|
69
|
+
},
|
|
70
|
+
nextAction: {
|
|
71
|
+
code: "run_first_action_path",
|
|
72
|
+
message: "Run the ordered steps; if the proof wait expires, use the fallback status/promote commands after the operator completes the dashboard path."
|
|
73
|
+
}
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
return {
|
|
77
|
+
...runbook,
|
|
78
|
+
runbookHashAlgorithm: "sha256",
|
|
79
|
+
runbookHash: hashRunbook(runbook)
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function renderBotFirstActionRunbookText(runbook) {
|
|
84
|
+
const lines = [
|
|
85
|
+
"SVS first verified action runbook",
|
|
86
|
+
`Server: ${runbook.serverUrl}`,
|
|
87
|
+
`Env: ${runbook.envPath}`,
|
|
88
|
+
`Contract: ${runbook.contract.hash}`,
|
|
89
|
+
"",
|
|
90
|
+
"Ordered path:"
|
|
91
|
+
];
|
|
92
|
+
|
|
93
|
+
for (const step of runbook.orderedSteps ?? []) {
|
|
94
|
+
lines.push(`${step.order}. [${step.actor}] ${step.title}`);
|
|
95
|
+
if (step.command) {
|
|
96
|
+
lines.push(` ${step.command}`);
|
|
97
|
+
}
|
|
98
|
+
if (step.dashboardAction) {
|
|
99
|
+
lines.push(` Dashboard: ${step.dashboardAction}`);
|
|
100
|
+
}
|
|
101
|
+
lines.push(` Success: ${step.successSignal}`);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (runbook.fallbackPath?.commands?.length) {
|
|
105
|
+
lines.push("");
|
|
106
|
+
lines.push("If proof wait expires:");
|
|
107
|
+
for (const command of runbook.fallbackPath.commands) {
|
|
108
|
+
lines.push(` ${command}`);
|
|
109
|
+
}
|
|
110
|
+
lines.push(` Dashboard: ${runbook.fallbackPath.operatorAction}`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
lines.push("");
|
|
114
|
+
lines.push(`Runbook hash: ${runbook.runbookHash}`);
|
|
115
|
+
return `${lines.join("\n")}\n`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function validateQuickstart(contract) {
|
|
119
|
+
if (contract?.version !== BOT_INTEGRATION_CONTRACT_VERSION) {
|
|
120
|
+
throw new Error(`Unsupported bot integration contract version: ${contract?.version ?? "missing"}.`);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (contract?.quickstart?.version !== BOT_INTEGRATION_QUICKSTART_VERSION) {
|
|
124
|
+
throw new Error(`Unsupported bot quickstart version: ${contract?.quickstart?.version ?? "missing"}.`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return contract.quickstart;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function createCommandSet({
|
|
131
|
+
commands,
|
|
132
|
+
resolvedServerUrl,
|
|
133
|
+
resolvedEnvPath,
|
|
134
|
+
quickstartEnvPath,
|
|
135
|
+
quickstartServerUrl
|
|
136
|
+
}) {
|
|
137
|
+
const envFlag = `--env ${resolvedEnvPath}`;
|
|
138
|
+
const normalizeCommand = (command) => rewriteCommand(command, {
|
|
139
|
+
resolvedEnvPath,
|
|
140
|
+
resolvedServerUrl,
|
|
141
|
+
quickstartEnvPath,
|
|
142
|
+
quickstartServerUrl
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
quickstart: normalizeCommand(commands.quickstart) ?? `npm run bot:quickstart -- --server ${resolvedServerUrl} --write-env ${resolvedEnvPath}`,
|
|
147
|
+
preflight: normalizeCommand(commands.preflight) ?? `npm run bot:preflight -- ${envFlag}`,
|
|
148
|
+
doctor: normalizeCommand(commands.doctor) ?? `npm run bot:doctor -- ${envFlag}`,
|
|
149
|
+
report: normalizeCommand(commands.report) ?? `npm run bot:report -- ${envFlag}`,
|
|
150
|
+
certification: normalizeCommand(commands.certification) ?? `npm run bot:certification -- ${envFlag} --write-evidence`,
|
|
151
|
+
certifiedSubmitReadiness: normalizeCommand(commands.certifiedSubmitReadiness) ?? `npm run bot:certified-submit:readiness -- ${envFlag} --write-receipt`,
|
|
152
|
+
certifiedSubmitAndWaitForProof: normalizeCommand(commands.certifiedSubmitAndWaitForProof) ??
|
|
153
|
+
`npm run bot:certified-submit -- ${envFlag} --wait-production-proof true --production-proof-wait-attempts 24 --production-proof-wait-interval-ms 10000 --production-proof-check-receipt-registry-chain true`,
|
|
154
|
+
certifiedSubmitStatus: normalizeCommand(commands.certifiedSubmitStatus) ?? "npm run bot:certified-submit:status",
|
|
155
|
+
certifiedSubmitPromote: normalizeCommand(commands.certifiedSubmitPromote) ?? "npm run bot:certified-submit:promote"
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function rewriteCommand(command, {
|
|
160
|
+
resolvedEnvPath,
|
|
161
|
+
resolvedServerUrl,
|
|
162
|
+
quickstartEnvPath,
|
|
163
|
+
quickstartServerUrl
|
|
164
|
+
}) {
|
|
165
|
+
if (!command) {
|
|
166
|
+
return null;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
let text = String(command);
|
|
170
|
+
|
|
171
|
+
for (const path of new Set([quickstartEnvPath, DEFAULT_ENV_PATH])) {
|
|
172
|
+
if (path && path !== resolvedEnvPath) {
|
|
173
|
+
text = text.split(path).join(resolvedEnvPath);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (quickstartServerUrl && quickstartServerUrl !== resolvedServerUrl) {
|
|
178
|
+
text = text.split(quickstartServerUrl).join(resolvedServerUrl);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return text;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function createOrderedSteps(commandSet) {
|
|
185
|
+
return [
|
|
186
|
+
{
|
|
187
|
+
version: BOT_FIRST_ACTION_STEP_VERSION,
|
|
188
|
+
order: 1,
|
|
189
|
+
actor: "operator",
|
|
190
|
+
title: "Generate the bot env handoff",
|
|
191
|
+
command: commandSet.quickstart,
|
|
192
|
+
successSignal: "A local env file exists with placeholders, current server URL, and pinned integration-contract hash."
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
version: BOT_FIRST_ACTION_STEP_VERSION,
|
|
196
|
+
order: 2,
|
|
197
|
+
actor: "operator",
|
|
198
|
+
title: "Fill credentials from the dashboard Bot onboarding panel",
|
|
199
|
+
dashboardAction: "Copy bot id, API key, request-signing secret, controller wallet, and policy into the env file.",
|
|
200
|
+
successSignal: "The bot runtime has the env values; no secret material is stored in this runbook."
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
version: BOT_FIRST_ACTION_STEP_VERSION,
|
|
204
|
+
order: 3,
|
|
205
|
+
actor: "bot",
|
|
206
|
+
title: "Run safe preflight",
|
|
207
|
+
command: commandSet.preflight,
|
|
208
|
+
successSignal: "Preflight exits 0 and reports the next action without queueing a transaction."
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
version: BOT_FIRST_ACTION_STEP_VERSION,
|
|
212
|
+
order: 4,
|
|
213
|
+
actor: "bot",
|
|
214
|
+
title: "Run doctor for compatibility and credential readiness",
|
|
215
|
+
command: commandSet.doctor,
|
|
216
|
+
successSignal: "Doctor exits 0 with the current quickstart version and no missing client capabilities."
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
version: BOT_FIRST_ACTION_STEP_VERSION,
|
|
220
|
+
order: 5,
|
|
221
|
+
actor: "bot",
|
|
222
|
+
title: "Generate a shareable integration report",
|
|
223
|
+
command: commandSet.report,
|
|
224
|
+
successSignal: "The report is non-secret and gives the operator one adoption score plus next action."
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
version: BOT_FIRST_ACTION_STEP_VERSION,
|
|
228
|
+
order: 6,
|
|
229
|
+
actor: "operator",
|
|
230
|
+
title: "Certify the bot integration path",
|
|
231
|
+
command: commandSet.certification,
|
|
232
|
+
successSignal: "Certification evidence is current, contract-bound, and qualityTarget is ready before production submit."
|
|
233
|
+
},
|
|
234
|
+
{
|
|
235
|
+
version: BOT_FIRST_ACTION_STEP_VERSION,
|
|
236
|
+
order: 7,
|
|
237
|
+
actor: "bot",
|
|
238
|
+
title: "Write the certified-submit readiness receipt",
|
|
239
|
+
command: commandSet.certifiedSubmitReadiness,
|
|
240
|
+
successSignal: "A no-secret readiness receipt proves the bot can submit through the certified path."
|
|
241
|
+
},
|
|
242
|
+
{
|
|
243
|
+
version: BOT_FIRST_ACTION_STEP_VERSION,
|
|
244
|
+
order: 8,
|
|
245
|
+
actor: "bot",
|
|
246
|
+
title: "Submit the first action and wait for production proof",
|
|
247
|
+
command: commandSet.certifiedSubmitAndWaitForProof,
|
|
248
|
+
successSignal: "The helper submits with HMAC request signing, polls signed proof reads, and fetches the action production proof when the operator path completes."
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
version: BOT_FIRST_ACTION_STEP_VERSION,
|
|
252
|
+
order: 9,
|
|
253
|
+
actor: "operator",
|
|
254
|
+
title: "Approve, broadcast, verify, and register the action",
|
|
255
|
+
dashboardAction: "Use the dashboard action queue to approve with wallet, broadcast, verify action, register the custom receipt, and export proof.",
|
|
256
|
+
successSignal: "The selected action has a custom registry PDA proof and exportable verification set."
|
|
257
|
+
}
|
|
258
|
+
];
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function hashRunbook(runbook) {
|
|
262
|
+
return createHash("sha256")
|
|
263
|
+
.update(stableStringify(normalizeRunbookForHash(runbook)))
|
|
264
|
+
.digest("hex");
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function normalizeRunbookForHash(value) {
|
|
268
|
+
if (Array.isArray(value)) {
|
|
269
|
+
return value.map(normalizeRunbookForHash);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (value && typeof value === "object") {
|
|
273
|
+
return Object.keys(value)
|
|
274
|
+
.filter((key) => !["generatedAt", "runbookHash", "runbookHashAlgorithm"].includes(key))
|
|
275
|
+
.sort()
|
|
276
|
+
.reduce((result, key) => {
|
|
277
|
+
result[key] = normalizeRunbookForHash(value[key]);
|
|
278
|
+
return result;
|
|
279
|
+
}, {});
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
return value;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
function stableStringify(value) {
|
|
286
|
+
if (Array.isArray(value)) {
|
|
287
|
+
return `[${value.map(stableStringify).join(",")}]`;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (value && typeof value === "object") {
|
|
291
|
+
return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableStringify(value[key])}`).join(",")}}`;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return JSON.stringify(value);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function normalizeBaseUrl(value) {
|
|
298
|
+
return String(value || "http://127.0.0.1:4173").replace(/\/+$/, "");
|
|
299
|
+
}
|