@brainai/satp-client 0.1.0-rc.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/README.md +515 -0
- package/examples/integration-patterns.js +495 -0
- package/examples/runtime-policy-adapter.js +75 -0
- package/package.json +66 -0
- package/src/attestation-request.js +149 -0
- package/src/borsh-reader.d.ts +213 -0
- package/src/borsh-reader.js +587 -0
- package/src/constants.js +77 -0
- package/src/index.d.ts +420 -0
- package/src/index.js +987 -0
- package/src/pda.js +191 -0
- package/src/runtime-policy-adapter.js +206 -0
- package/src/schema.js +59 -0
- package/src/trust-packet.js +148 -0
- package/src/v3-pda.d.ts +72 -0
- package/src/v3-pda.js +281 -0
- package/src/v3-sdk.d.ts +508 -0
- package/src/v3-sdk.js +1800 -0
- package/src/wallet-control-challenge.js +351 -0
package/src/pda.js
ADDED
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
const { PublicKey } = require('@solana/web3.js');
|
|
2
|
+
const {
|
|
3
|
+
getProgramIds,
|
|
4
|
+
IDENTITY_SEED,
|
|
5
|
+
REPUTATION_AUTHORITY_SEED,
|
|
6
|
+
VALIDATION_AUTHORITY_SEED,
|
|
7
|
+
REVIEW_COUNTER_SEED,
|
|
8
|
+
MINT_TRACKER_SEED,
|
|
9
|
+
REVIEWS_AUTHORITY_SEED,
|
|
10
|
+
ATTESTATION_SEED,
|
|
11
|
+
REVIEW_SEED,
|
|
12
|
+
ESCROW_SEED,
|
|
13
|
+
} = require('./constants');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Derive the Identity PDA for a wallet.
|
|
17
|
+
* Seeds: ["identity", wallet_pubkey]
|
|
18
|
+
* @param {PublicKey|string} wallet
|
|
19
|
+
* @param {'mainnet'|'devnet'} network
|
|
20
|
+
*/
|
|
21
|
+
function getIdentityPDA(wallet, network = 'devnet') {
|
|
22
|
+
const walletKey = new PublicKey(wallet);
|
|
23
|
+
const programIds = getProgramIds(network);
|
|
24
|
+
return PublicKey.findProgramAddressSync(
|
|
25
|
+
[Buffer.from(IDENTITY_SEED), walletKey.toBuffer()],
|
|
26
|
+
programIds.IDENTITY
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Derive the Reputation Authority PDA (program signer for CPI).
|
|
32
|
+
* Seeds: ["reputation_authority"]
|
|
33
|
+
* @param {'mainnet'|'devnet'} network
|
|
34
|
+
*/
|
|
35
|
+
function getReputationAuthorityPDA(network = 'devnet') {
|
|
36
|
+
const programIds = getProgramIds(network);
|
|
37
|
+
return PublicKey.findProgramAddressSync(
|
|
38
|
+
[Buffer.from(REPUTATION_AUTHORITY_SEED)],
|
|
39
|
+
programIds.REPUTATION
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Derive the Validation Authority PDA (program signer for CPI).
|
|
45
|
+
* Seeds: ["validation_authority"]
|
|
46
|
+
* @param {'mainnet'|'devnet'} network
|
|
47
|
+
*/
|
|
48
|
+
function getValidationAuthorityPDA(network = 'devnet') {
|
|
49
|
+
const programIds = getProgramIds(network);
|
|
50
|
+
return PublicKey.findProgramAddressSync(
|
|
51
|
+
[Buffer.from(VALIDATION_AUTHORITY_SEED)],
|
|
52
|
+
programIds.VALIDATION
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Derive the Review Counter PDA for an agent.
|
|
58
|
+
* Seeds: ["review_counter", agent_id]
|
|
59
|
+
* @param {PublicKey|string} agentId
|
|
60
|
+
* @param {'mainnet'|'devnet'} network
|
|
61
|
+
*/
|
|
62
|
+
function getReviewCounterPDA(agentId, network = 'devnet') {
|
|
63
|
+
const agentKey = new PublicKey(agentId);
|
|
64
|
+
const programIds = getProgramIds(network);
|
|
65
|
+
return PublicKey.findProgramAddressSync(
|
|
66
|
+
[Buffer.from(REVIEW_COUNTER_SEED), agentKey.toBuffer()],
|
|
67
|
+
programIds.REVIEWS
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Derive the MintTracker PDA for an identity.
|
|
73
|
+
* Seeds: ["mint_tracker", identity_pda]
|
|
74
|
+
* @param {PublicKey|string} identityPDA
|
|
75
|
+
* @param {'mainnet'|'devnet'} network
|
|
76
|
+
*/
|
|
77
|
+
function getMintTrackerPDA(identityPDA, network = 'devnet') {
|
|
78
|
+
const identityKey = new PublicKey(identityPDA);
|
|
79
|
+
const programIds = getProgramIds(network);
|
|
80
|
+
return PublicKey.findProgramAddressSync(
|
|
81
|
+
[Buffer.from(MINT_TRACKER_SEED), identityKey.toBuffer()],
|
|
82
|
+
programIds.IDENTITY
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Derive the Reviews Authority PDA (program signer for CPI into Attestations).
|
|
88
|
+
* Seeds: ["reviews_authority"]
|
|
89
|
+
* @param {'mainnet'|'devnet'} network
|
|
90
|
+
*/
|
|
91
|
+
function getReviewsAuthorityPDA(network = 'devnet') {
|
|
92
|
+
const programIds = getProgramIds(network);
|
|
93
|
+
return PublicKey.findProgramAddressSync(
|
|
94
|
+
[Buffer.from(REVIEWS_AUTHORITY_SEED)],
|
|
95
|
+
programIds.REVIEWS
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Derive the Review PDA for a specific reviewer + agent pair.
|
|
101
|
+
* Seeds: ["review", agent_id, reviewer]
|
|
102
|
+
* @param {PublicKey|string} agentId
|
|
103
|
+
* @param {PublicKey|string} reviewer
|
|
104
|
+
* @param {'mainnet'|'devnet'} network
|
|
105
|
+
*/
|
|
106
|
+
function getReviewPDA(agentId, reviewer, network = 'devnet') {
|
|
107
|
+
const agentKey = new PublicKey(agentId);
|
|
108
|
+
const reviewerKey = new PublicKey(reviewer);
|
|
109
|
+
const programIds = getProgramIds(network);
|
|
110
|
+
return PublicKey.findProgramAddressSync(
|
|
111
|
+
[Buffer.from(REVIEW_SEED), agentKey.toBuffer(), reviewerKey.toBuffer()],
|
|
112
|
+
programIds.REVIEWS
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Derive the Review Attestation PDA (auto-created via CPI when a review is submitted).
|
|
118
|
+
* Seeds: ["attestation", agent_id, reviews_authority, "review", reviewer]
|
|
119
|
+
* @param {PublicKey|string} agentId
|
|
120
|
+
* @param {PublicKey} reviewsAuthority - Use getReviewsAuthorityPDA() to derive
|
|
121
|
+
* @param {PublicKey|string} reviewer
|
|
122
|
+
* @param {'mainnet'|'devnet'} network
|
|
123
|
+
*/
|
|
124
|
+
function getReviewAttestationPDA(agentId, reviewsAuthority, reviewer, network = 'devnet') {
|
|
125
|
+
const agentKey = new PublicKey(agentId);
|
|
126
|
+
const reviewerKey = new PublicKey(reviewer);
|
|
127
|
+
const programIds = getProgramIds(network);
|
|
128
|
+
return PublicKey.findProgramAddressSync(
|
|
129
|
+
[
|
|
130
|
+
Buffer.from(ATTESTATION_SEED),
|
|
131
|
+
agentKey.toBuffer(),
|
|
132
|
+
reviewsAuthority.toBuffer(),
|
|
133
|
+
Buffer.from(REVIEW_SEED),
|
|
134
|
+
reviewerKey.toBuffer(),
|
|
135
|
+
],
|
|
136
|
+
programIds.ATTESTATIONS
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Derive the Escrow PDA for a client + description_hash pair.
|
|
142
|
+
* Seeds: ["escrow", client_pubkey, description_hash]
|
|
143
|
+
* @param {PublicKey|string} client - Client wallet
|
|
144
|
+
* @param {Buffer|number[]} descriptionHash - 32-byte SHA256 hash of job description
|
|
145
|
+
* @param {'mainnet'|'devnet'} network
|
|
146
|
+
*/
|
|
147
|
+
function getEscrowPDA(client, descriptionHash, network = 'devnet') {
|
|
148
|
+
const clientKey = new PublicKey(client);
|
|
149
|
+
const hashBuf = Buffer.isBuffer(descriptionHash)
|
|
150
|
+
? descriptionHash
|
|
151
|
+
: Buffer.from(descriptionHash);
|
|
152
|
+
const programIds = getProgramIds(network);
|
|
153
|
+
if (!programIds.ESCROW) {
|
|
154
|
+
throw new Error('SATP v2 mainnet escrow program ID is not configured; pass devnet or wait for an approved mainnet escrow deployment');
|
|
155
|
+
}
|
|
156
|
+
return PublicKey.findProgramAddressSync(
|
|
157
|
+
[Buffer.from(ESCROW_SEED), clientKey.toBuffer(), hashBuf],
|
|
158
|
+
programIds.ESCROW
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Derive the Review V3 PDA (job-scoped reviews).
|
|
164
|
+
* Seeds: ["review", job_pubkey, reviewer_pubkey]
|
|
165
|
+
* @param {PublicKey|string} jobPDA - Job/Escrow account pubkey
|
|
166
|
+
* @param {PublicKey|string} reviewer - Reviewer wallet
|
|
167
|
+
* @param {'mainnet'|'devnet'} network
|
|
168
|
+
*/
|
|
169
|
+
function getReviewV3PDA(jobPDA, reviewer, network = 'devnet') {
|
|
170
|
+
const jobKey = new PublicKey(jobPDA);
|
|
171
|
+
const reviewerKey = new PublicKey(reviewer);
|
|
172
|
+
const programIds = getProgramIds(network);
|
|
173
|
+
// Reviews V3 uses the same REVIEWS program ID
|
|
174
|
+
return PublicKey.findProgramAddressSync(
|
|
175
|
+
[Buffer.from(REVIEW_SEED), jobKey.toBuffer(), reviewerKey.toBuffer()],
|
|
176
|
+
programIds.REVIEWS
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
module.exports = {
|
|
181
|
+
getIdentityPDA,
|
|
182
|
+
getReputationAuthorityPDA,
|
|
183
|
+
getValidationAuthorityPDA,
|
|
184
|
+
getReviewCounterPDA,
|
|
185
|
+
getMintTrackerPDA,
|
|
186
|
+
getReviewsAuthorityPDA,
|
|
187
|
+
getReviewPDA,
|
|
188
|
+
getReviewAttestationPDA,
|
|
189
|
+
getEscrowPDA,
|
|
190
|
+
getReviewV3PDA,
|
|
191
|
+
};
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const DECISIONS = Object.freeze({
|
|
4
|
+
ALLOW: 'allow',
|
|
5
|
+
DENY: 'deny',
|
|
6
|
+
DEGRADE: 'degrade',
|
|
7
|
+
NEEDS_APPROVAL: 'needs_approval',
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const REASON_CODES = Object.freeze({
|
|
11
|
+
ACTION_PAYMENT_NEEDS_APPROVAL: 'ACTION_PAYMENT_NEEDS_APPROVAL',
|
|
12
|
+
ACTION_PAYMENT_PREAPPROVED: 'ACTION_PAYMENT_PREAPPROVED',
|
|
13
|
+
EVIDENCE_FRESH: 'EVIDENCE_FRESH',
|
|
14
|
+
EVIDENCE_STALE_OR_MISSING: 'EVIDENCE_STALE_OR_MISSING',
|
|
15
|
+
IDENTITY_INACTIVE: 'IDENTITY_INACTIVE',
|
|
16
|
+
IDENTITY_UNVERIFIED: 'IDENTITY_UNVERIFIED',
|
|
17
|
+
INVALID_ACTION_COST_USD: 'INVALID_ACTION_COST_USD',
|
|
18
|
+
LOCAL_POLICY_ALLOW: 'LOCAL_POLICY_ALLOW',
|
|
19
|
+
MISSING_CAPABILITY: 'MISSING_CAPABILITY',
|
|
20
|
+
PROTECTED_TOOL_REQUIRES_APPROVAL: 'PROTECTED_TOOL_REQUIRES_APPROVAL',
|
|
21
|
+
TRUST_SCORE_BELOW_DENY_FLOOR: 'TRUST_SCORE_BELOW_DENY_FLOOR',
|
|
22
|
+
TRUST_SCORE_BELOW_MINIMUM: 'TRUST_SCORE_BELOW_MINIMUM',
|
|
23
|
+
TRUST_SCORE_OK: 'TRUST_SCORE_OK',
|
|
24
|
+
X402_LOOKUP_PAYMENT_PREAPPROVED: 'X402_LOOKUP_PAYMENT_PREAPPROVED',
|
|
25
|
+
X402_LOOKUP_REQUIRES_APPROVAL: 'X402_LOOKUP_REQUIRES_APPROVAL',
|
|
26
|
+
X402_PAYMENT_IS_NOT_ACTION_AUTHORIZATION: 'X402_PAYMENT_IS_NOT_ACTION_AUTHORIZATION',
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const DEFAULT_POLICY = Object.freeze({
|
|
30
|
+
minimumTrustScore: 70,
|
|
31
|
+
denyTrustScoreBelow: 25,
|
|
32
|
+
maxAutoSpendUsd: 0,
|
|
33
|
+
requireVerifiedIdentity: true,
|
|
34
|
+
staleEvidenceAfterMs: 7 * 24 * 60 * 60 * 1000,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
function evaluateRuntimePolicy(identityPayload, actionDescriptor, options = {}) {
|
|
38
|
+
const policy = { ...DEFAULT_POLICY, ...(options.policy || {}) };
|
|
39
|
+
const now = options.now ? new Date(options.now) : new Date();
|
|
40
|
+
const identity = normalizeIdentity(identityPayload);
|
|
41
|
+
const action = normalizeAction(actionDescriptor);
|
|
42
|
+
const reasonCodes = [];
|
|
43
|
+
const checks = {};
|
|
44
|
+
|
|
45
|
+
checks.identityActive = identity.active === true;
|
|
46
|
+
if (!checks.identityActive) {
|
|
47
|
+
reasonCodes.push(REASON_CODES.IDENTITY_INACTIVE);
|
|
48
|
+
return decision(DECISIONS.DENY, reasonCodes, checks, 'Identity is not active.');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
checks.identityVerified = !policy.requireVerifiedIdentity || identity.verified === true;
|
|
52
|
+
if (!checks.identityVerified) {
|
|
53
|
+
reasonCodes.push(REASON_CODES.IDENTITY_UNVERIFIED);
|
|
54
|
+
return decision(DECISIONS.DENY, reasonCodes, checks, 'Identity is not verified by local policy.');
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
checks.hasCapability = !action.requiresCapability || identity.capabilities.includes(action.requiresCapability);
|
|
58
|
+
if (!checks.hasCapability) {
|
|
59
|
+
reasonCodes.push(REASON_CODES.MISSING_CAPABILITY);
|
|
60
|
+
return decision(DECISIONS.DENY, reasonCodes, checks, 'Required capability is absent.');
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
checks.trustScore = identity.trustScore;
|
|
64
|
+
checks.minimumTrustScore = action.minimumTrustScore ?? policy.minimumTrustScore;
|
|
65
|
+
if (identity.trustScore < policy.denyTrustScoreBelow) {
|
|
66
|
+
reasonCodes.push(REASON_CODES.TRUST_SCORE_BELOW_DENY_FLOOR);
|
|
67
|
+
return decision(DECISIONS.DENY, reasonCodes, checks, 'Trust score is below the deny floor.');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (identity.trustScore < checks.minimumTrustScore) {
|
|
71
|
+
reasonCodes.push(REASON_CODES.TRUST_SCORE_BELOW_MINIMUM);
|
|
72
|
+
if (action.allowDegraded === true) {
|
|
73
|
+
return decision(DECISIONS.DEGRADE, reasonCodes, checks, 'Trust score permits degraded access only.');
|
|
74
|
+
}
|
|
75
|
+
return decision(DECISIONS.NEEDS_APPROVAL, reasonCodes, checks, 'Trust score needs an operator decision.');
|
|
76
|
+
}
|
|
77
|
+
reasonCodes.push(REASON_CODES.TRUST_SCORE_OK);
|
|
78
|
+
|
|
79
|
+
checks.evidenceFresh = isEvidenceFresh(identity, policy, now);
|
|
80
|
+
if (action.requiresFreshEvidence && !checks.evidenceFresh) {
|
|
81
|
+
reasonCodes.push(REASON_CODES.EVIDENCE_STALE_OR_MISSING);
|
|
82
|
+
return staleEvidenceDecision(action, options, reasonCodes, checks);
|
|
83
|
+
}
|
|
84
|
+
if (action.requiresFreshEvidence) reasonCodes.push(REASON_CODES.EVIDENCE_FRESH);
|
|
85
|
+
|
|
86
|
+
checks.actionCostUsd = action.costUsd;
|
|
87
|
+
checks.actionCostUsdValid = action.costUsdValid;
|
|
88
|
+
if (!action.costUsdValid) {
|
|
89
|
+
reasonCodes.push(REASON_CODES.INVALID_ACTION_COST_USD);
|
|
90
|
+
return decision(DECISIONS.DENY, reasonCodes, checks, 'Action costUsd must be a finite non-negative number.');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
checks.maxAutoSpendUsd = policy.maxAutoSpendUsd;
|
|
94
|
+
if (checks.actionCostUsd > checks.maxAutoSpendUsd && options.actionPaymentPreapproved !== true) {
|
|
95
|
+
reasonCodes.push(REASON_CODES.ACTION_PAYMENT_NEEDS_APPROVAL);
|
|
96
|
+
return decision(DECISIONS.NEEDS_APPROVAL, reasonCodes, checks, 'Paid action exceeds local auto-spend policy.');
|
|
97
|
+
}
|
|
98
|
+
if (checks.actionCostUsd > 0) {
|
|
99
|
+
reasonCodes.push(REASON_CODES.ACTION_PAYMENT_PREAPPROVED);
|
|
100
|
+
reasonCodes.push(REASON_CODES.X402_PAYMENT_IS_NOT_ACTION_AUTHORIZATION);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (action.protectedTool && action.operatorApprovalRequired && options.operatorApproved !== true) {
|
|
104
|
+
reasonCodes.push(REASON_CODES.PROTECTED_TOOL_REQUIRES_APPROVAL);
|
|
105
|
+
return decision(DECISIONS.NEEDS_APPROVAL, reasonCodes, checks, 'Protected tool requires operator approval.');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
reasonCodes.push(REASON_CODES.LOCAL_POLICY_ALLOW);
|
|
109
|
+
return decision(DECISIONS.ALLOW, reasonCodes, checks, 'Local runtime policy allows the action.');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function staleEvidenceDecision(action, options, reasonCodes, checks) {
|
|
113
|
+
const lookup = action.evidenceLookup || null;
|
|
114
|
+
if (!lookup || lookup.type !== 'x402') {
|
|
115
|
+
return decision(DECISIONS.DEGRADE, reasonCodes, checks, 'Evidence is stale or missing; no paid lookup path is configured.');
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
checks.evidenceLookup = {
|
|
119
|
+
type: lookup.type,
|
|
120
|
+
endpoint: lookup.endpoint || null,
|
|
121
|
+
maxCostUsd: lookup.maxCostUsd ?? null,
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
if (options.evidenceLookupPaymentPreapproved !== true) {
|
|
125
|
+
reasonCodes.push(REASON_CODES.X402_LOOKUP_REQUIRES_APPROVAL);
|
|
126
|
+
reasonCodes.push(REASON_CODES.X402_PAYMENT_IS_NOT_ACTION_AUTHORIZATION);
|
|
127
|
+
return decision(DECISIONS.NEEDS_APPROVAL, reasonCodes, checks, 'Paid x402 evidence lookup requires approval before use.');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
reasonCodes.push(REASON_CODES.X402_LOOKUP_PAYMENT_PREAPPROVED);
|
|
131
|
+
reasonCodes.push(REASON_CODES.X402_PAYMENT_IS_NOT_ACTION_AUTHORIZATION);
|
|
132
|
+
return decision(DECISIONS.DEGRADE, reasonCodes, checks, 'Paid lookup may refresh evidence, but does not authorize the agent action.');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function decision(value, reasonCodes, checks, message) {
|
|
136
|
+
return {
|
|
137
|
+
decision: value,
|
|
138
|
+
reasonCodes: Array.from(new Set(reasonCodes)),
|
|
139
|
+
message,
|
|
140
|
+
checks,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function normalizeIdentity(identity = {}) {
|
|
145
|
+
return {
|
|
146
|
+
agentId: identity.agentId || identity.profileId || null,
|
|
147
|
+
active: identity.active !== false,
|
|
148
|
+
verified: identity.verified === true || identity.satpVerified === true,
|
|
149
|
+
trustScore: clampScore(identity.trustScore ?? identity.agentFolioTrustScore ?? 0),
|
|
150
|
+
capabilities: Array.isArray(identity.capabilities) ? identity.capabilities.slice() : [],
|
|
151
|
+
evidenceUpdatedAt: identity.evidenceUpdatedAt || identity.lastEvidenceAt || null,
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function normalizeAction(action = {}) {
|
|
156
|
+
const parsedCost = parseActionCostUsd(action);
|
|
157
|
+
|
|
158
|
+
return {
|
|
159
|
+
type: action.type || 'generic',
|
|
160
|
+
resource: action.resource || null,
|
|
161
|
+
operation: action.operation || null,
|
|
162
|
+
requiresCapability: action.requiresCapability || null,
|
|
163
|
+
minimumTrustScore: Number.isFinite(action.minimumTrustScore) ? action.minimumTrustScore : null,
|
|
164
|
+
allowDegraded: action.allowDegraded === true,
|
|
165
|
+
requiresFreshEvidence: action.requiresFreshEvidence === true,
|
|
166
|
+
evidenceLookup: action.evidenceLookup || null,
|
|
167
|
+
protectedTool: action.protectedTool === true || action.type === 'mcp_protected_tool',
|
|
168
|
+
operatorApprovalRequired: action.operatorApprovalRequired === true,
|
|
169
|
+
costUsd: parsedCost.value,
|
|
170
|
+
costUsdValid: parsedCost.valid,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function parseActionCostUsd(action) {
|
|
175
|
+
if (!Object.prototype.hasOwnProperty.call(action, 'costUsd') || action.costUsd == null) {
|
|
176
|
+
return { value: 0, valid: true };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
if (typeof action.costUsd !== 'number' || !Number.isFinite(action.costUsd) || action.costUsd < 0) {
|
|
180
|
+
return { value: null, valid: false };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return { value: action.costUsd, valid: true };
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function clampScore(value) {
|
|
187
|
+
const score = Number(value);
|
|
188
|
+
if (!Number.isFinite(score)) return 0;
|
|
189
|
+
return Math.max(0, Math.min(100, score));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function isEvidenceFresh(identity, policy, now) {
|
|
193
|
+
if (!identity.evidenceUpdatedAt) return false;
|
|
194
|
+
const updatedAt = new Date(identity.evidenceUpdatedAt);
|
|
195
|
+
if (Number.isNaN(updatedAt.getTime())) return false;
|
|
196
|
+
const ageMs = now.getTime() - updatedAt.getTime();
|
|
197
|
+
if (ageMs < 0) return false;
|
|
198
|
+
return ageMs <= policy.staleEvidenceAfterMs;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
module.exports = {
|
|
202
|
+
DECISIONS,
|
|
203
|
+
DEFAULT_POLICY,
|
|
204
|
+
REASON_CODES,
|
|
205
|
+
evaluateRuntimePolicy,
|
|
206
|
+
};
|
package/src/schema.js
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const borsh = require('borsh');
|
|
2
|
+
|
|
3
|
+
// ---- Identity Account Schema ----
|
|
4
|
+
class IdentityAccount {
|
|
5
|
+
constructor(fields) {
|
|
6
|
+
this.discriminator = fields.discriminator; // 8 bytes (Anchor)
|
|
7
|
+
this.owner = fields.owner; // 32 bytes pubkey
|
|
8
|
+
this.agentName = fields.agentName; // string
|
|
9
|
+
this.metadata = fields.metadata; // string (JSON)
|
|
10
|
+
this.createdAt = fields.createdAt; // i64 timestamp
|
|
11
|
+
this.updatedAt = fields.updatedAt; // i64 timestamp
|
|
12
|
+
this.bump = fields.bump; // u8
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const IDENTITY_SCHEMA = new Map([
|
|
17
|
+
[IdentityAccount, {
|
|
18
|
+
kind: 'struct',
|
|
19
|
+
fields: [
|
|
20
|
+
['discriminator', [8]],
|
|
21
|
+
['owner', [32]],
|
|
22
|
+
['agentName', 'string'],
|
|
23
|
+
['metadata', 'string'],
|
|
24
|
+
['createdAt', 'u64'],
|
|
25
|
+
['updatedAt', 'u64'],
|
|
26
|
+
['bump', 'u8'],
|
|
27
|
+
],
|
|
28
|
+
}],
|
|
29
|
+
]);
|
|
30
|
+
|
|
31
|
+
// ---- Reputation Account Schema ----
|
|
32
|
+
class ReputationAccount {
|
|
33
|
+
constructor(fields) {
|
|
34
|
+
this.discriminator = fields.discriminator;
|
|
35
|
+
this.owner = fields.owner;
|
|
36
|
+
this.score = fields.score; // u64
|
|
37
|
+
this.endorsements = fields.endorsements; // u32 count
|
|
38
|
+
this.lastEndorser = fields.lastEndorser; // 32 bytes pubkey
|
|
39
|
+
this.updatedAt = fields.updatedAt;
|
|
40
|
+
this.bump = fields.bump;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const REPUTATION_SCHEMA = new Map([
|
|
45
|
+
[ReputationAccount, {
|
|
46
|
+
kind: 'struct',
|
|
47
|
+
fields: [
|
|
48
|
+
['discriminator', [8]],
|
|
49
|
+
['owner', [32]],
|
|
50
|
+
['score', 'u64'],
|
|
51
|
+
['endorsements', 'u32'],
|
|
52
|
+
['lastEndorser', [32]],
|
|
53
|
+
['updatedAt', 'u64'],
|
|
54
|
+
['bump', 'u8'],
|
|
55
|
+
],
|
|
56
|
+
}],
|
|
57
|
+
]);
|
|
58
|
+
|
|
59
|
+
module.exports = { IdentityAccount, IDENTITY_SCHEMA, ReputationAccount, REPUTATION_SCHEMA };
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { prepareIdentityAttestationRequest } = require('./attestation-request');
|
|
4
|
+
|
|
5
|
+
const TRUST_PACKET_SCHEMA_VERSION = 'satp.trustPacket.v1';
|
|
6
|
+
|
|
7
|
+
function canonicalStringify(value) {
|
|
8
|
+
if (Array.isArray(value)) {
|
|
9
|
+
return '[' + value.map(canonicalStringify).join(',') + ']';
|
|
10
|
+
}
|
|
11
|
+
if (value && typeof value === 'object') {
|
|
12
|
+
return '{' + Object.keys(value).sort().map((key) => JSON.stringify(key) + ':' + canonicalStringify(value[key])).join(',') + '}';
|
|
13
|
+
}
|
|
14
|
+
return JSON.stringify(value);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function sameJsonValue(left, right) {
|
|
18
|
+
return canonicalStringify(left) === canonicalStringify(right);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Build a deterministic, read-only SATP trust packet for consumer preflight.
|
|
23
|
+
*
|
|
24
|
+
* The packet wraps the unsigned identity-attestation request with the derived
|
|
25
|
+
* program IDs, Genesis PDA, attestation PDA, request hash, and explicit flags
|
|
26
|
+
* proving that no signer, transaction, instruction, RPC write, or payment is
|
|
27
|
+
* required to consume it.
|
|
28
|
+
*/
|
|
29
|
+
function buildSatpTrustPacket(opts = {}) {
|
|
30
|
+
const request = prepareIdentityAttestationRequest(opts);
|
|
31
|
+
return {
|
|
32
|
+
schemaVersion: TRUST_PACKET_SCHEMA_VERSION,
|
|
33
|
+
packetType: 'satp-trust-packet',
|
|
34
|
+
mode: 'offline-readonly-trust-packet',
|
|
35
|
+
network: request.network,
|
|
36
|
+
subjectWallet: request.subjectWallet,
|
|
37
|
+
agentId: request.agentId,
|
|
38
|
+
claimType: request.claimType,
|
|
39
|
+
attestationType: request.attestationType,
|
|
40
|
+
metadataHash: request.metadataHash,
|
|
41
|
+
attester: request.attester,
|
|
42
|
+
expiresAt: request.expiresAt,
|
|
43
|
+
programs: { ...request.programs },
|
|
44
|
+
pda: {
|
|
45
|
+
genesis: request.genesisPda,
|
|
46
|
+
genesisBump: request.genesisBump,
|
|
47
|
+
attestation: request.attestationPda,
|
|
48
|
+
attestationBump: request.attestationBump,
|
|
49
|
+
},
|
|
50
|
+
requestHash: request.requestHash,
|
|
51
|
+
flags: {
|
|
52
|
+
signingRequired: false,
|
|
53
|
+
transactionRequired: false,
|
|
54
|
+
writesRequired: false,
|
|
55
|
+
livePaymentRequired: false,
|
|
56
|
+
unsigned: true,
|
|
57
|
+
noSign: true,
|
|
58
|
+
noTransaction: true,
|
|
59
|
+
},
|
|
60
|
+
instructions: [],
|
|
61
|
+
signers: [],
|
|
62
|
+
transaction: null,
|
|
63
|
+
request,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function validateSatpTrustPacket(packet) {
|
|
68
|
+
const errors = [];
|
|
69
|
+
if (!packet || typeof packet !== 'object') {
|
|
70
|
+
return { ok: false, errors: ['packet must be an object'] };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (packet.schemaVersion !== TRUST_PACKET_SCHEMA_VERSION) {
|
|
74
|
+
errors.push('schemaVersion must be ' + TRUST_PACKET_SCHEMA_VERSION);
|
|
75
|
+
}
|
|
76
|
+
if (packet.packetType !== 'satp-trust-packet') {
|
|
77
|
+
errors.push('packetType must be satp-trust-packet');
|
|
78
|
+
}
|
|
79
|
+
if (packet.mode !== 'offline-readonly-trust-packet') {
|
|
80
|
+
errors.push('mode must be offline-readonly-trust-packet');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const expectedFlags = {
|
|
84
|
+
signingRequired: false,
|
|
85
|
+
transactionRequired: false,
|
|
86
|
+
writesRequired: false,
|
|
87
|
+
livePaymentRequired: false,
|
|
88
|
+
unsigned: true,
|
|
89
|
+
noSign: true,
|
|
90
|
+
noTransaction: true,
|
|
91
|
+
};
|
|
92
|
+
if (!sameJsonValue(packet.flags, expectedFlags)) {
|
|
93
|
+
errors.push('flags must be read-only, unsigned, and no-transaction');
|
|
94
|
+
}
|
|
95
|
+
if (!Array.isArray(packet.instructions) || packet.instructions.length !== 0) {
|
|
96
|
+
errors.push('instructions must be an empty array');
|
|
97
|
+
}
|
|
98
|
+
if (!Array.isArray(packet.signers) || packet.signers.length !== 0) {
|
|
99
|
+
errors.push('signers must be an empty array');
|
|
100
|
+
}
|
|
101
|
+
if (packet.transaction !== null) {
|
|
102
|
+
errors.push('transaction must be null');
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let expected;
|
|
106
|
+
try {
|
|
107
|
+
expected = buildSatpTrustPacket({
|
|
108
|
+
subjectWallet: packet.subjectWallet,
|
|
109
|
+
agentId: packet.agentId,
|
|
110
|
+
claimType: packet.claimType || packet.attestationType,
|
|
111
|
+
metadataHash: packet.metadataHash,
|
|
112
|
+
attester: packet.attester,
|
|
113
|
+
network: packet.network,
|
|
114
|
+
expiresAt: packet.expiresAt,
|
|
115
|
+
});
|
|
116
|
+
} catch (err) {
|
|
117
|
+
errors.push('packet cannot be re-derived: ' + err.message);
|
|
118
|
+
return { ok: false, errors };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const fields = [
|
|
122
|
+
'network',
|
|
123
|
+
'subjectWallet',
|
|
124
|
+
'agentId',
|
|
125
|
+
'claimType',
|
|
126
|
+
'attestationType',
|
|
127
|
+
'metadataHash',
|
|
128
|
+
'attester',
|
|
129
|
+
'expiresAt',
|
|
130
|
+
'programs',
|
|
131
|
+
'pda',
|
|
132
|
+
'requestHash',
|
|
133
|
+
'request',
|
|
134
|
+
];
|
|
135
|
+
for (const field of fields) {
|
|
136
|
+
if (!sameJsonValue(packet[field], expected[field])) {
|
|
137
|
+
errors.push(field + ' does not match derived trust packet');
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return { ok: errors.length === 0, errors };
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
module.exports = {
|
|
145
|
+
TRUST_PACKET_SCHEMA_VERSION,
|
|
146
|
+
buildSatpTrustPacket,
|
|
147
|
+
validateSatpTrustPacket,
|
|
148
|
+
};
|
package/src/v3-pda.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { PublicKey } from '@solana/web3.js';
|
|
2
|
+
|
|
3
|
+
export type Network = 'mainnet' | 'devnet';
|
|
4
|
+
|
|
5
|
+
export interface V3ProgramIds {
|
|
6
|
+
IDENTITY: PublicKey;
|
|
7
|
+
REVIEWS: PublicKey;
|
|
8
|
+
REPUTATION: PublicKey;
|
|
9
|
+
ATTESTATIONS: PublicKey;
|
|
10
|
+
VALIDATION: PublicKey;
|
|
11
|
+
ESCROW: PublicKey;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Get all V3 program IDs for a network. */
|
|
15
|
+
export function getV3ProgramIds(network?: Network): V3ProgramIds;
|
|
16
|
+
|
|
17
|
+
/** SHA-256 hash of agent ID string. Returns 32-byte Buffer. */
|
|
18
|
+
export function hashAgentId(agentId: string): Buffer;
|
|
19
|
+
|
|
20
|
+
/** SHA-256 hash of lowercased name string. Returns 32-byte Buffer. */
|
|
21
|
+
export function hashName(name: string): Buffer;
|
|
22
|
+
|
|
23
|
+
/** Derive Genesis PDA from agent ID hash. */
|
|
24
|
+
export function getGenesisPDA(agentIdHash: string | Buffer, network?: Network): [PublicKey, number];
|
|
25
|
+
|
|
26
|
+
/** Derive Reputation V3 Authority PDA. */
|
|
27
|
+
export function getV3ReputationAuthorityPDA(network?: Network): [PublicKey, number];
|
|
28
|
+
|
|
29
|
+
/** Derive Validation V3 Authority PDA. */
|
|
30
|
+
export function getV3ValidationAuthorityPDA(network?: Network): [PublicKey, number];
|
|
31
|
+
|
|
32
|
+
/** Derive MintTracker PDA from Genesis PDA. */
|
|
33
|
+
export function getV3MintTrackerPDA(genesisPDA: PublicKey | string, network?: Network): [PublicKey, number];
|
|
34
|
+
|
|
35
|
+
/** Derive NameRegistry PDA from name hash. */
|
|
36
|
+
export function getNameRegistryPDA(nameHash: string | Buffer, network?: Network): [PublicKey, number];
|
|
37
|
+
|
|
38
|
+
/** Derive LinkedWallet PDA from Genesis PDA and wallet. */
|
|
39
|
+
export function getLinkedWalletPDA(
|
|
40
|
+
genesisPDA: PublicKey | string,
|
|
41
|
+
wallet: PublicKey | string,
|
|
42
|
+
network?: Network
|
|
43
|
+
): [PublicKey, number];
|
|
44
|
+
|
|
45
|
+
/** Derive Review V3 PDA from job account and reviewer. */
|
|
46
|
+
export function getV3ReviewPDA(
|
|
47
|
+
jobPDA: PublicKey | string,
|
|
48
|
+
reviewer: PublicKey | string,
|
|
49
|
+
network?: Network
|
|
50
|
+
): [PublicKey, number];
|
|
51
|
+
|
|
52
|
+
/** Derive Attestation PDA from agent ID hash, attester, and type. */
|
|
53
|
+
export function getV3AttestationPDA(
|
|
54
|
+
agentIdHash: string | Buffer,
|
|
55
|
+
attester: PublicKey | string,
|
|
56
|
+
attestationType: string,
|
|
57
|
+
network?: Network
|
|
58
|
+
): [PublicKey, number];
|
|
59
|
+
|
|
60
|
+
/** Derive Review Counter PDA from agent ID. */
|
|
61
|
+
export function getV3ReviewCounterPDA(
|
|
62
|
+
agentIdOrHash: string | Buffer,
|
|
63
|
+
network?: Network
|
|
64
|
+
): [PublicKey, number];
|
|
65
|
+
|
|
66
|
+
/** Derive Escrow V3 PDA from client, description hash, and nonce. */
|
|
67
|
+
export function getV3EscrowPDA(
|
|
68
|
+
client: PublicKey | string,
|
|
69
|
+
descriptionHash: Buffer,
|
|
70
|
+
nonce: number | bigint,
|
|
71
|
+
network?: Network
|
|
72
|
+
): [PublicKey, number];
|