@reclaimprotocol/agent 0.1.39 → 0.1.40
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/lib/mcp/api/operations.gen.d.ts +7 -0
- package/lib/mcp/api/operations.gen.js +49 -3
- package/lib/mcp/instructions.js +10 -8
- package/lib/mcp/tools/agent/example-app.d.ts +5 -1
- package/lib/mcp/tools/agent/example-app.js +87 -24
- package/lib/mcp/tools/authenticate/eth-key.d.ts +15 -0
- package/lib/mcp/tools/authenticate/eth-key.js +72 -6
- package/lib/mcp/tools/authenticate/issue-org-keypair.d.ts +3 -2
- package/lib/mcp/tools/authenticate/issue-org-keypair.js +83 -18
- package/lib/mcp/tools/authenticate/issue-org-token.js +9 -2
- package/package.json +2 -2
- package/skill/SKILL.builder.md +30 -11
- package/skill/SKILL.old.md +23 -5
- package/skill/builder-mode.md +29 -164
- package/skill/old-mode.md +40 -95
- package/skill/start-integration.md +12 -9
- package/skill/verification-client-integration.md +39 -9
- package/src/mcp/api/operations.gen.ts +51 -3
- package/src/mcp/instructions.ts +10 -8
- package/src/mcp/tools/agent/example-app.ts +97 -30
- package/src/mcp/tools/authenticate/eth-key.ts +114 -6
- package/src/mcp/tools/authenticate/issue-org-keypair.ts +97 -19
- package/src/mcp/tools/authenticate/issue-org-token.ts +13 -2
|
@@ -1,16 +1,24 @@
|
|
|
1
|
+
import { conflict } from '@hapi/boom'
|
|
1
2
|
import { deriveFromPrivateKey, generateKeypair } from '@reclaimprotocol/client'
|
|
2
|
-
import type
|
|
3
|
+
import { isProblemError, type ReclaimClient } from '@reclaimprotocol/client/api'
|
|
3
4
|
import assert from 'node:assert'
|
|
4
5
|
import { isAbsolute, relative, resolve } from 'node:path'
|
|
5
6
|
import { defineTool, type RegisteredTool } from '../../server.ts'
|
|
6
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
backupEnvVar,
|
|
9
|
+
normalizeEthHex,
|
|
10
|
+
readDotenvKey,
|
|
11
|
+
removeEnvVar,
|
|
12
|
+
upsertEnvVar,
|
|
13
|
+
} from './eth-key.ts'
|
|
7
14
|
|
|
8
15
|
/**
|
|
9
16
|
* Creates the organization's eth key pair for session binding and encrypted
|
|
10
17
|
* results: writes the private key to the app's server env file and registers
|
|
11
18
|
* only the public key with Builder, so the private key never passes through
|
|
12
19
|
* the agent's transcript. Reuses a key the env file already holds, so a
|
|
13
|
-
* repeat call (for example, to turn on encryption) keeps the same key.
|
|
20
|
+
* repeat call (for example, to turn on encryption) keeps the same key. With
|
|
21
|
+
* `replace`, always generates a fresh key and rotates the org's key to it.
|
|
14
22
|
*/
|
|
15
23
|
export function issueOrgKeypairTool(client: ReclaimClient): RegisteredTool {
|
|
16
24
|
return defineTool<{
|
|
@@ -19,6 +27,7 @@ export function issueOrgKeypairTool(client: ReclaimClient): RegisteredTool {
|
|
|
19
27
|
envFile?: string
|
|
20
28
|
envVar?: string
|
|
21
29
|
encryptResults?: boolean
|
|
30
|
+
replace?: boolean
|
|
22
31
|
}>(
|
|
23
32
|
{
|
|
24
33
|
name: 'issue_org_keypair',
|
|
@@ -30,8 +39,12 @@ export function issueOrgKeypairTool(client: ReclaimClient): RegisteredTool {
|
|
|
30
39
|
+ 'private key to a server env file in the project (reusing a key '
|
|
31
40
|
+ 'already there) and registers only the public key. Returns the '
|
|
32
41
|
+ 'address, file, and variable; never the key. Returns 409 when the '
|
|
33
|
-
+ 'org already has a different key
|
|
34
|
-
+ '
|
|
42
|
+
+ 'org already has a different key. Set `replace: true` only when '
|
|
43
|
+
+ 'the developer asked to rotate or replace the key: it generates '
|
|
44
|
+
+ 'a new key and swaps it in one step, and apps holding the old '
|
|
45
|
+
+ 'key can no longer decrypt new results. The old key is first '
|
|
46
|
+
+ 'appended to `<envFile>.backup` (gitignored); tell the developer '
|
|
47
|
+
+ 'its path.',
|
|
35
48
|
inputSchema: {
|
|
36
49
|
type: 'object',
|
|
37
50
|
required: ['orgId', 'projectDir'],
|
|
@@ -55,11 +68,18 @@ export function issueOrgKeypairTool(client: ReclaimClient): RegisteredTool {
|
|
|
55
68
|
encryptResults: {
|
|
56
69
|
type: 'boolean',
|
|
57
70
|
description:
|
|
58
|
-
'Encrypt delivered results to this key. Default false
|
|
71
|
+
'Encrypt delivered results to this key. Default false, or the '
|
|
72
|
+
+ 'org\'s current setting with `replace`.',
|
|
73
|
+
},
|
|
74
|
+
replace: {
|
|
75
|
+
type: 'boolean',
|
|
76
|
+
description:
|
|
77
|
+
'Rotate: generate a new key and replace the org\'s current '
|
|
78
|
+
+ 'one. Default false.',
|
|
59
79
|
},
|
|
60
80
|
},
|
|
61
81
|
},
|
|
62
|
-
annotations: { openWorldHint: true, destructiveHint:
|
|
82
|
+
annotations: { openWorldHint: true, destructiveHint: true },
|
|
63
83
|
},
|
|
64
84
|
async(args) => {
|
|
65
85
|
assert(
|
|
@@ -75,24 +95,60 @@ export function issueOrgKeypairTool(client: ReclaimClient): RegisteredTool {
|
|
|
75
95
|
new Error('`envFile` must be inside `projectDir`.'),
|
|
76
96
|
)
|
|
77
97
|
|
|
78
|
-
const
|
|
98
|
+
const replace = args.replace ?? false
|
|
99
|
+
// Rotation always generates a fresh key: the env file's key may or
|
|
100
|
+
// may not be the one the org holds, and reuse would be a no-op.
|
|
101
|
+
const existing = replace ? undefined : readDotenvKey(envPath, envVar)
|
|
79
102
|
const keypair = existing
|
|
80
103
|
? deriveFromPrivateKey(normalizeEthHex(existing))
|
|
81
104
|
: generateKeypair()
|
|
82
|
-
//
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
105
|
+
// A rotation keeps the org's current setting unless the caller sets it.
|
|
106
|
+
const canEncryptResult = args.encryptResults
|
|
107
|
+
?? (replace && await currentCanEncryptResult())
|
|
108
|
+
// Back up and save the key before the org switches to it, so a
|
|
109
|
+
// failed write can't leave the org on a key no app holds. A failed
|
|
110
|
+
// registration (for example, a 409) puts the env file back.
|
|
111
|
+
const old = readDotenvKey(envPath, envVar)
|
|
112
|
+
const backupFile = old && !existing
|
|
113
|
+
? backupEnvVar(
|
|
114
|
+
args.projectDir, envFile, envVar, 'replaced by issue_org_keypair',
|
|
115
|
+
)
|
|
116
|
+
: undefined
|
|
93
117
|
const path = upsertEnvVar(
|
|
94
118
|
args.projectDir, envFile, envVar, keypair.privateKey,
|
|
95
119
|
)
|
|
120
|
+
try {
|
|
121
|
+
await client.call('SetOrgKeypair', {
|
|
122
|
+
params: { orgId: args.orgId },
|
|
123
|
+
body: {
|
|
124
|
+
publicKey: keypair.publicKey,
|
|
125
|
+
canEncryptResult,
|
|
126
|
+
replace,
|
|
127
|
+
},
|
|
128
|
+
})
|
|
129
|
+
} catch(err) {
|
|
130
|
+
if(old) {
|
|
131
|
+
upsertEnvVar(args.projectDir, envFile, envVar, old)
|
|
132
|
+
} else {
|
|
133
|
+
removeEnvVar(args.projectDir, envFile, envVar)
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if(!replace && isProblemError(err) && err.output.statusCode === 409) {
|
|
137
|
+
throw conflict(
|
|
138
|
+
'The org already has a different key. Ask the developer to '
|
|
139
|
+
+ 'choose: (a) reuse it: they paste its private key into '
|
|
140
|
+
+ `\`${envFile}\` as \`${envVar}\` themselves, never into the `
|
|
141
|
+
+ 'chat; (b) rotate it: call `issue_org_keypair` again with '
|
|
142
|
+
+ '`replace: true`, after which apps holding the old key can no '
|
|
143
|
+
+ 'longer decrypt new results; (c) turn off result encryption: '
|
|
144
|
+
+ 'call `update_org_keypair` with `canEncryptResult: false`, '
|
|
145
|
+
+ 'which keeps the key.',
|
|
146
|
+
)
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
throw err
|
|
150
|
+
}
|
|
151
|
+
|
|
96
152
|
return {
|
|
97
153
|
orgId: args.orgId,
|
|
98
154
|
address: keypair.address,
|
|
@@ -100,6 +156,28 @@ export function issueOrgKeypairTool(client: ReclaimClient): RegisteredTool {
|
|
|
100
156
|
envFile: path,
|
|
101
157
|
envVar,
|
|
102
158
|
reused: Boolean(existing),
|
|
159
|
+
backupFile: backupFile ?? null,
|
|
160
|
+
...(replace && !backupFile && !old
|
|
161
|
+
? {
|
|
162
|
+
note: 'Nothing was backed up: the old private key is not in '
|
|
163
|
+
+ 'this file. Another app may hold it.',
|
|
164
|
+
}
|
|
165
|
+
: {}),
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async function currentCanEncryptResult() {
|
|
169
|
+
try {
|
|
170
|
+
const { data } = await client.call('GetOrgKeypair', {
|
|
171
|
+
params: { orgId: args.orgId },
|
|
172
|
+
})
|
|
173
|
+
return data.canEncryptResult
|
|
174
|
+
} catch(err) {
|
|
175
|
+
if(isProblemError(err) && err.output.statusCode === 404) {
|
|
176
|
+
return false
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
throw err
|
|
180
|
+
}
|
|
103
181
|
}
|
|
104
182
|
},
|
|
105
183
|
)
|
|
@@ -2,7 +2,7 @@ import type { ReclaimClient } from '@reclaimprotocol/client/api'
|
|
|
2
2
|
import assert from 'node:assert'
|
|
3
3
|
import { isAbsolute, relative, resolve } from 'node:path'
|
|
4
4
|
import { defineTool, type RegisteredTool } from '../../server.ts'
|
|
5
|
-
import { upsertEnvVar } from './eth-key.ts'
|
|
5
|
+
import { backupEnvVar, readDotenvKey, upsertEnvVar } from './eth-key.ts'
|
|
6
6
|
import { saveOrgKey } from './org-key-store.ts'
|
|
7
7
|
|
|
8
8
|
/**
|
|
@@ -24,7 +24,9 @@ export function issueOrgTokenTool(client: ReclaimClient): RegisteredTool {
|
|
|
24
24
|
+ 'server env file in the project, so the app can read it at runtime. '
|
|
25
25
|
+ 'Also saves it on this machine for the org-key tools. Returns the '
|
|
26
26
|
+ 'org, file, variable, and last four characters; never the key. '
|
|
27
|
-
+ 'Replaces an existing variable of the same name in the file
|
|
27
|
+
+ 'Replaces an existing variable of the same name in the file, first '
|
|
28
|
+
+ 'appending the old value to `<envFile>.backup` (gitignored); tell '
|
|
29
|
+
+ 'the developer its path. '
|
|
28
30
|
+ 'Adds the file to .gitignore. Use when the app needs a key, or '
|
|
29
31
|
+ 'before `create_verification_session`.',
|
|
30
32
|
inputSchema: {
|
|
@@ -69,6 +71,14 @@ export function issueOrgTokenTool(client: ReclaimClient): RegisteredTool {
|
|
|
69
71
|
const { data } = await client.call('IssueOrgToken', {
|
|
70
72
|
params: { orgId: args.orgId },
|
|
71
73
|
})
|
|
74
|
+
const old = readDotenvKey(
|
|
75
|
+
resolve(args.projectDir, envFile), envVar,
|
|
76
|
+
)
|
|
77
|
+
const backupFile = old && old !== data.orgSecret
|
|
78
|
+
? backupEnvVar(
|
|
79
|
+
args.projectDir, envFile, envVar, 'replaced by issue_org_token',
|
|
80
|
+
)
|
|
81
|
+
: undefined
|
|
72
82
|
const path = upsertEnvVar(
|
|
73
83
|
args.projectDir, envFile, envVar, data.orgSecret,
|
|
74
84
|
)
|
|
@@ -78,6 +88,7 @@ export function issueOrgTokenTool(client: ReclaimClient): RegisteredTool {
|
|
|
78
88
|
envFile: path,
|
|
79
89
|
envVar,
|
|
80
90
|
last4: data.last4,
|
|
91
|
+
backupFile: backupFile ?? null,
|
|
81
92
|
}
|
|
82
93
|
},
|
|
83
94
|
)
|