relic-mcp 0.3.1 → 0.3.3
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/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/relic-mcp.js +350 -41
- package/package.json +1 -1
- package/skills/relic/SKILL.md +26 -3
- package/src/publish.ts +115 -1
- package/src/server.ts +191 -21
- package/src/state.ts +312 -33
package/src/publish.ts
CHANGED
|
@@ -21,7 +21,12 @@ import {
|
|
|
21
21
|
type RendererClass,
|
|
22
22
|
relicUrl,
|
|
23
23
|
} from '@relic/format';
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
loadPublishedSource,
|
|
26
|
+
resolveSourceIdentity,
|
|
27
|
+
type SourceIdentity,
|
|
28
|
+
savePublishState,
|
|
29
|
+
} from './state.ts';
|
|
25
30
|
|
|
26
31
|
/**
|
|
27
32
|
* Codes for the legs the app server is not in.
|
|
@@ -37,6 +42,7 @@ export type ClientCode =
|
|
|
37
42
|
| 'source_is_directory'
|
|
38
43
|
| 'source_not_regular_file'
|
|
39
44
|
| 'source_unreadable'
|
|
45
|
+
| 'source_already_published'
|
|
40
46
|
| 'local_size_precheck_failed'
|
|
41
47
|
| 'upload_failed'
|
|
42
48
|
| 'service_unreachable'
|
|
@@ -96,6 +102,11 @@ export interface PublishInput {
|
|
|
96
102
|
* to survive the wire.
|
|
97
103
|
*/
|
|
98
104
|
readonly ttl_days?: number | undefined;
|
|
105
|
+
/**
|
|
106
|
+
* Bypass prior-publish refusal only when a second independent URL is
|
|
107
|
+
* intentional. False and undefined both preserve the existing URL.
|
|
108
|
+
*/
|
|
109
|
+
readonly force_new?: boolean | undefined;
|
|
99
110
|
}
|
|
100
111
|
|
|
101
112
|
export interface PublishResult {
|
|
@@ -121,15 +132,117 @@ export interface PublishDeps {
|
|
|
121
132
|
readonly files: FileReader;
|
|
122
133
|
readonly fetch: typeof globalThis.fetch;
|
|
123
134
|
readonly clientName: string;
|
|
135
|
+
/** Test seam for a filesystem that does not exist outside memory. */
|
|
136
|
+
readonly identifySource?: (path: string) => Promise<SourceIdentity>;
|
|
124
137
|
/** Retries on a colliding ID, which format.md 1.4 obliges the client to do. */
|
|
125
138
|
readonly maxCollisionRetries?: number;
|
|
126
139
|
}
|
|
127
140
|
|
|
141
|
+
export interface RepublishToolCall {
|
|
142
|
+
readonly name: 'relic_republish';
|
|
143
|
+
readonly arguments: {
|
|
144
|
+
readonly relic_id: string;
|
|
145
|
+
readonly path: string;
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
export interface PriorPublish {
|
|
150
|
+
readonly relic_id: string;
|
|
151
|
+
readonly version: number;
|
|
152
|
+
readonly source: SourceIdentity;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export interface PublishedSourceLookup {
|
|
156
|
+
readonly resolved_path: string;
|
|
157
|
+
readonly source: SourceIdentity;
|
|
158
|
+
readonly match?: PriorPublish | undefined;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** The complete call an agent can make without retaining an earlier session. */
|
|
162
|
+
export function republishToolCall(
|
|
163
|
+
relicId: string,
|
|
164
|
+
path: string
|
|
165
|
+
): RepublishToolCall {
|
|
166
|
+
return {
|
|
167
|
+
name: 'relic_republish',
|
|
168
|
+
arguments: { relic_id: relicId, path },
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
async function lookupResolvedSource(
|
|
173
|
+
resolvedPath: string,
|
|
174
|
+
deps: PublishDeps
|
|
175
|
+
): Promise<PublishedSourceLookup> {
|
|
176
|
+
let source: SourceIdentity;
|
|
177
|
+
try {
|
|
178
|
+
source = await (deps.identifySource ?? resolveSourceIdentity)(resolvedPath);
|
|
179
|
+
} catch (error) {
|
|
180
|
+
const code = (error as NodeJS.ErrnoException).code;
|
|
181
|
+
throw new PublishError(
|
|
182
|
+
code === 'ENOENT' ? 'source_not_found' : 'source_unreadable',
|
|
183
|
+
`could not resolve source identity for ${resolvedPath}: ` +
|
|
184
|
+
(error as Error).message,
|
|
185
|
+
{ path: resolvedPath }
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
const published = await loadPublishedSource(source);
|
|
191
|
+
return {
|
|
192
|
+
resolved_path: resolvedPath,
|
|
193
|
+
source,
|
|
194
|
+
...(published === undefined
|
|
195
|
+
? {}
|
|
196
|
+
: {
|
|
197
|
+
match: {
|
|
198
|
+
relic_id: published.relic_id,
|
|
199
|
+
version: published.state.version,
|
|
200
|
+
source: published.source,
|
|
201
|
+
},
|
|
202
|
+
}),
|
|
203
|
+
};
|
|
204
|
+
} catch (error) {
|
|
205
|
+
throw new PublishError('local_state_unreadable', (error as Error).message);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** Look up a path in local state without reading its bytes or calling a server. */
|
|
210
|
+
export async function lookupPublishedSource(
|
|
211
|
+
path: string,
|
|
212
|
+
deps: PublishDeps
|
|
213
|
+
): Promise<PublishedSourceLookup> {
|
|
214
|
+
return lookupResolvedSource(deps.files.resolve(path), deps);
|
|
215
|
+
}
|
|
216
|
+
|
|
128
217
|
export async function publish(
|
|
129
218
|
input: PublishInput,
|
|
130
219
|
deps: PublishDeps
|
|
131
220
|
): Promise<PublishResult> {
|
|
132
221
|
const source = await readSource(input.path, deps.files);
|
|
222
|
+
const sourceLookup = await lookupResolvedSource(source.resolvedPath, deps);
|
|
223
|
+
if (sourceLookup.match !== undefined && input.force_new !== true) {
|
|
224
|
+
const match = sourceLookup.match;
|
|
225
|
+
const republishCall = republishToolCall(
|
|
226
|
+
match.relic_id,
|
|
227
|
+
source.resolvedPath
|
|
228
|
+
);
|
|
229
|
+
const cost = 'a second URL that nobody holding the first one will ever see';
|
|
230
|
+
throw new PublishError(
|
|
231
|
+
'source_already_published',
|
|
232
|
+
`${match.source.description} is already version ${match.version} of ` +
|
|
233
|
+
`relic ${match.relic_id}. Publishing it as new would cost ${cost}. ` +
|
|
234
|
+
`Call relic_republish(${JSON.stringify(republishCall.arguments)}) ` +
|
|
235
|
+
'instead. Set force_new to true only when you intend a separate relic.',
|
|
236
|
+
{
|
|
237
|
+
relic_id: match.relic_id,
|
|
238
|
+
version: match.version,
|
|
239
|
+
source_identity: match.source.identity,
|
|
240
|
+
source_description: match.source.description,
|
|
241
|
+
cost,
|
|
242
|
+
republish_call: republishCall,
|
|
243
|
+
}
|
|
244
|
+
);
|
|
245
|
+
}
|
|
133
246
|
|
|
134
247
|
// 1. Challenge. This returns the cap before a grant is requested, so the
|
|
135
248
|
// precheck below uses a number that came from the server moments ago
|
|
@@ -243,6 +356,7 @@ export async function publish(
|
|
|
243
356
|
key: encodeKey(key),
|
|
244
357
|
publish_token: publishToken,
|
|
245
358
|
version: 1,
|
|
359
|
+
source: sourceLookup.source,
|
|
246
360
|
});
|
|
247
361
|
} catch (error) {
|
|
248
362
|
// The relic itself is live and the URL works, so both are handed over
|
package/src/server.ts
CHANGED
|
@@ -33,9 +33,11 @@ import {
|
|
|
33
33
|
unsupportedVersionError,
|
|
34
34
|
} from './protocol.ts';
|
|
35
35
|
import {
|
|
36
|
+
lookupPublishedSource,
|
|
36
37
|
type PublishDeps,
|
|
37
38
|
PublishError,
|
|
38
39
|
publish,
|
|
40
|
+
republishToolCall,
|
|
39
41
|
ServerRefusal,
|
|
40
42
|
} from './publish.ts';
|
|
41
43
|
import { republish } from './republish.ts';
|
|
@@ -86,6 +88,15 @@ export const DESCRIBE_TOOL_NAME = 'relic_describe_client';
|
|
|
86
88
|
*/
|
|
87
89
|
export const REPUBLISH_TOOL_NAME = 'relic_republish';
|
|
88
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Source lookup is a separate read-only tool.
|
|
93
|
+
*
|
|
94
|
+
* An agent needs the id before it chooses publish or republish, and a lookup
|
|
95
|
+
* hidden inside either write tool would only be observable after choosing the
|
|
96
|
+
* wrong one. This call reads local state and never contacts the service.
|
|
97
|
+
*/
|
|
98
|
+
export const LOOKUP_TOOL_NAME = 'relic_lookup_source';
|
|
99
|
+
|
|
89
100
|
/**
|
|
90
101
|
* The ceiling on a publisher-supplied lifetime, matching the grant
|
|
91
102
|
* contract's `maxTtlDays`. Refusing here keeps a typo like 36500 from
|
|
@@ -94,13 +105,22 @@ export const REPUBLISH_TOOL_NAME = 'relic_republish';
|
|
|
94
105
|
*/
|
|
95
106
|
const MAX_TTL_DAYS = 3650;
|
|
96
107
|
|
|
108
|
+
const VERSION_HISTORY_DISCLOSURE =
|
|
109
|
+
"Anyone holding a relic's link can fetch every version it has ever held, " +
|
|
110
|
+
'so republishing does not withdraw earlier content.';
|
|
111
|
+
|
|
97
112
|
export const TOOL_DEFINITION = {
|
|
98
113
|
name: TOOL_NAME,
|
|
99
114
|
title: 'Publish a relic',
|
|
100
115
|
description:
|
|
101
|
-
'Encrypt a file on this machine and publish it as a relic, returning
|
|
102
|
-
'shareable URL.
|
|
103
|
-
'
|
|
116
|
+
'Encrypt a file on this machine and publish it as a new relic, returning ' +
|
|
117
|
+
'a shareable URL. Publishing an update this way costs a second URL that ' +
|
|
118
|
+
'nobody holding the first one will ever see; use relic_republish instead ' +
|
|
119
|
+
'so the existing URL keeps working. ' +
|
|
120
|
+
VERSION_HISTORY_DISCLOSURE +
|
|
121
|
+
' The encryption key is generated locally and never sent to the service. ' +
|
|
122
|
+
'Takes a filesystem path, never ' +
|
|
123
|
+
'inline content.',
|
|
104
124
|
inputSchema: {
|
|
105
125
|
type: 'object',
|
|
106
126
|
properties: {
|
|
@@ -123,6 +143,14 @@ export const TOOL_DEFINITION = {
|
|
|
123
143
|
'relic is kept until it is deleted. Shorter is better for ' +
|
|
124
144
|
'sensitive content.',
|
|
125
145
|
},
|
|
146
|
+
force_new: {
|
|
147
|
+
type: 'boolean',
|
|
148
|
+
default: false,
|
|
149
|
+
description:
|
|
150
|
+
'Optional. Publish a deliberately separate relic even when this ' +
|
|
151
|
+
'machine already published the same source. Defaults to false. Use ' +
|
|
152
|
+
'only when you want two independent URLs for one file.',
|
|
153
|
+
},
|
|
126
154
|
},
|
|
127
155
|
required: ['path'],
|
|
128
156
|
additionalProperties: false,
|
|
@@ -165,7 +193,8 @@ export const REPUBLISH_TOOL_DEFINITION = {
|
|
|
165
193
|
description:
|
|
166
194
|
'Publish a new version of a relic this machine originally published, ' +
|
|
167
195
|
'encrypting under the same key so the existing share URL keeps working. ' +
|
|
168
|
-
|
|
196
|
+
VERSION_HISTORY_DISCLOSURE +
|
|
197
|
+
" Only possible from the machine that holds the relic's key and publish " +
|
|
169
198
|
'token; a relic that was taken down can never be revived.',
|
|
170
199
|
inputSchema: {
|
|
171
200
|
type: 'object',
|
|
@@ -228,6 +257,64 @@ export const REPUBLISH_TOOL_DEFINITION = {
|
|
|
228
257
|
},
|
|
229
258
|
} as const;
|
|
230
259
|
|
|
260
|
+
export const LOOKUP_TOOL_DEFINITION = {
|
|
261
|
+
name: LOOKUP_TOOL_NAME,
|
|
262
|
+
title: 'Look up a published source',
|
|
263
|
+
description:
|
|
264
|
+
'Look up whether this machine already published a file and return the ' +
|
|
265
|
+
'relic id needed by relic_republish. Reads local publish state only and ' +
|
|
266
|
+
'never calls the service.',
|
|
267
|
+
inputSchema: {
|
|
268
|
+
type: 'object',
|
|
269
|
+
properties: {
|
|
270
|
+
path: {
|
|
271
|
+
type: 'string',
|
|
272
|
+
description: 'Filesystem path to the source to look up.',
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
required: ['path'],
|
|
276
|
+
additionalProperties: false,
|
|
277
|
+
},
|
|
278
|
+
outputSchema: {
|
|
279
|
+
type: 'object',
|
|
280
|
+
properties: {
|
|
281
|
+
found: { type: 'boolean' },
|
|
282
|
+
relic_id: { type: ['string', 'null'] },
|
|
283
|
+
version: { type: ['integer', 'null'], minimum: 1 },
|
|
284
|
+
resolved_path: { type: 'string' },
|
|
285
|
+
source_identity: { type: 'string' },
|
|
286
|
+
source_description: { type: 'string' },
|
|
287
|
+
republish_call: {
|
|
288
|
+
type: ['object', 'null'],
|
|
289
|
+
properties: {
|
|
290
|
+
name: { type: 'string', const: REPUBLISH_TOOL_NAME },
|
|
291
|
+
arguments: {
|
|
292
|
+
type: 'object',
|
|
293
|
+
properties: {
|
|
294
|
+
relic_id: { type: 'string' },
|
|
295
|
+
path: { type: 'string' },
|
|
296
|
+
},
|
|
297
|
+
required: ['relic_id', 'path'],
|
|
298
|
+
additionalProperties: false,
|
|
299
|
+
},
|
|
300
|
+
},
|
|
301
|
+
required: ['name', 'arguments'],
|
|
302
|
+
additionalProperties: false,
|
|
303
|
+
},
|
|
304
|
+
},
|
|
305
|
+
required: [
|
|
306
|
+
'found',
|
|
307
|
+
'relic_id',
|
|
308
|
+
'version',
|
|
309
|
+
'resolved_path',
|
|
310
|
+
'source_identity',
|
|
311
|
+
'source_description',
|
|
312
|
+
'republish_call',
|
|
313
|
+
],
|
|
314
|
+
additionalProperties: false,
|
|
315
|
+
},
|
|
316
|
+
} as const;
|
|
317
|
+
|
|
231
318
|
export const DESCRIBE_TOOL_DEFINITION = {
|
|
232
319
|
name: DESCRIBE_TOOL_NAME,
|
|
233
320
|
title: 'Describe the Relic client',
|
|
@@ -269,9 +356,9 @@ export const CAPABILITIES = { tools: {} } as const;
|
|
|
269
356
|
* The plugin ships a skill with the same facts, but a skill only reaches
|
|
270
357
|
* Claude Code, and only when somebody installs the plugin rather than wiring
|
|
271
358
|
* this server directly. Every other client saw tool descriptions and nothing
|
|
272
|
-
* else, which left
|
|
359
|
+
* else, which left five things an agent cannot read off a schema.
|
|
273
360
|
*
|
|
274
|
-
* Item
|
|
361
|
+
* Item five is the reason this exists at all rather than living only in the
|
|
275
362
|
* publish result. The result is returned after the file is written, which is
|
|
276
363
|
* too late for an agent that already linked a stylesheet from a CDN. This
|
|
277
364
|
* lands before generation, which is the only moment the advice can be taken.
|
|
@@ -279,20 +366,23 @@ export const CAPABILITIES = { tools: {} } as const;
|
|
|
279
366
|
* It costs context on every session, so it stays short and it stays true.
|
|
280
367
|
* Anything that needs a paragraph belongs in the skill or the disclosure.
|
|
281
368
|
*/
|
|
282
|
-
export const INSTRUCTIONS = `Relic
|
|
283
|
-
|
|
284
|
-
|
|
369
|
+
export const INSTRUCTIONS = `Relic encrypts a file on this machine and uploads \
|
|
370
|
+
only ciphertext. The key lives in the URL fragment, which browsers never send \
|
|
371
|
+
to a server.
|
|
285
372
|
|
|
286
|
-
|
|
373
|
+
Five things that change how you should act:
|
|
287
374
|
|
|
288
375
|
1. The link is the credential. Anyone holding it, fragment included, can read \
|
|
289
376
|
the file. Do not paste it into a tracker, a log, or a public channel.
|
|
290
377
|
2. Publishing puts the key in this transcript. That is structural rather than \
|
|
291
378
|
a defect, and worth saying plainly when you hand the link over.
|
|
292
|
-
3.
|
|
379
|
+
3. Check existing sources with relic_lookup_source. Use relic_republish when \
|
|
380
|
+
found; relic_publish otherwise costs a second URL. \
|
|
381
|
+
${VERSION_HISTORY_DISCLOSURE}
|
|
382
|
+
4. A relic can be republished only from the machine that published it, which \
|
|
293
383
|
is where its key and publish token are stored. Anywhere else it refuses, and \
|
|
294
384
|
no retry changes that.
|
|
295
|
-
|
|
385
|
+
5. Rendered HTML and JSX run in an isolated frame with no network access. \
|
|
296
386
|
Inline the styles, scripts, fonts, and images a page needs, because a CDN \
|
|
297
387
|
reference renders as nothing. Decide that before you write the file.`;
|
|
298
388
|
|
|
@@ -362,6 +452,7 @@ export async function handleMessage(
|
|
|
362
452
|
result: {
|
|
363
453
|
tools: [
|
|
364
454
|
TOOL_DEFINITION,
|
|
455
|
+
LOOKUP_TOOL_DEFINITION,
|
|
365
456
|
REPUBLISH_TOOL_DEFINITION,
|
|
366
457
|
DESCRIBE_TOOL_DEFINITION,
|
|
367
458
|
],
|
|
@@ -418,8 +509,9 @@ async function callTool(
|
|
|
418
509
|
plaintext_transmitted_to_service: false,
|
|
419
510
|
ciphertext_destination: 'object storage, via a signed URL',
|
|
420
511
|
local_publish_state:
|
|
421
|
-
'relic id, key, and publish token per relic,
|
|
422
|
-
'the user config directory;
|
|
512
|
+
'relic id, source identity, key, and publish token per relic, ' +
|
|
513
|
+
'written 0600 under the user config directory; key and token ' +
|
|
514
|
+
'are never printed or sent',
|
|
423
515
|
service_origin: deps.serviceOrigin,
|
|
424
516
|
},
|
|
425
517
|
isError: false,
|
|
@@ -427,6 +519,10 @@ async function callTool(
|
|
|
427
519
|
};
|
|
428
520
|
}
|
|
429
521
|
|
|
522
|
+
if (params['name'] === LOOKUP_TOOL_NAME) {
|
|
523
|
+
return callLookup(id, params, deps);
|
|
524
|
+
}
|
|
525
|
+
|
|
430
526
|
if (params['name'] === REPUBLISH_TOOL_NAME) {
|
|
431
527
|
return callRepublish(id, params, deps);
|
|
432
528
|
}
|
|
@@ -462,9 +558,23 @@ async function callTool(
|
|
|
462
558
|
);
|
|
463
559
|
}
|
|
464
560
|
|
|
561
|
+
const forceNew = args['force_new'];
|
|
562
|
+
if (forceNew !== undefined && typeof forceNew !== 'boolean') {
|
|
563
|
+
return errorResponse(
|
|
564
|
+
id,
|
|
565
|
+
ERROR_CODES.invalidParams,
|
|
566
|
+
'`force_new` must be a boolean or omitted'
|
|
567
|
+
);
|
|
568
|
+
}
|
|
569
|
+
|
|
465
570
|
try {
|
|
466
571
|
const result = await publish(
|
|
467
|
-
{
|
|
572
|
+
{
|
|
573
|
+
path,
|
|
574
|
+
filename,
|
|
575
|
+
ttl_days: ttlDays.days,
|
|
576
|
+
force_new: forceNew === true,
|
|
577
|
+
},
|
|
468
578
|
deps
|
|
469
579
|
);
|
|
470
580
|
return {
|
|
@@ -497,6 +607,7 @@ async function callTool(
|
|
|
497
607
|
isolationNote(result.renderer_class) +
|
|
498
608
|
`What Relic knows: ${result.disclosure_url}`,
|
|
499
609
|
},
|
|
610
|
+
{ type: 'text', text: VERSION_HISTORY_DISCLOSURE },
|
|
500
611
|
],
|
|
501
612
|
structuredContent: result,
|
|
502
613
|
isError: false,
|
|
@@ -507,6 +618,63 @@ async function callTool(
|
|
|
507
618
|
}
|
|
508
619
|
}
|
|
509
620
|
|
|
621
|
+
async function callLookup(
|
|
622
|
+
id: string | number | null,
|
|
623
|
+
params: Record<string, unknown>,
|
|
624
|
+
deps: PublishDeps
|
|
625
|
+
): Promise<JsonRpcResponse> {
|
|
626
|
+
const args = (params['arguments'] ?? {}) as Record<string, unknown>;
|
|
627
|
+
const path = args['path'];
|
|
628
|
+
if (typeof path !== 'string' || path.length === 0) {
|
|
629
|
+
return errorResponse(
|
|
630
|
+
id,
|
|
631
|
+
ERROR_CODES.invalidParams,
|
|
632
|
+
'`path` is required and must be a string'
|
|
633
|
+
);
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
try {
|
|
637
|
+
const lookup = await lookupPublishedSource(path, deps);
|
|
638
|
+
const match = lookup.match;
|
|
639
|
+
const republishCall =
|
|
640
|
+
match === undefined
|
|
641
|
+
? null
|
|
642
|
+
: republishToolCall(match.relic_id, lookup.resolved_path);
|
|
643
|
+
const structuredContent = {
|
|
644
|
+
found: match !== undefined,
|
|
645
|
+
relic_id: match?.relic_id ?? null,
|
|
646
|
+
version: match?.version ?? null,
|
|
647
|
+
resolved_path: lookup.resolved_path,
|
|
648
|
+
source_identity: lookup.source.identity,
|
|
649
|
+
source_description: lookup.source.description,
|
|
650
|
+
republish_call: republishCall,
|
|
651
|
+
};
|
|
652
|
+
return {
|
|
653
|
+
jsonrpc: '2.0',
|
|
654
|
+
id,
|
|
655
|
+
result: {
|
|
656
|
+
content: [
|
|
657
|
+
{
|
|
658
|
+
type: 'text',
|
|
659
|
+
text:
|
|
660
|
+
match === undefined
|
|
661
|
+
? `No prior relic is recorded for ${lookup.source.description}.`
|
|
662
|
+
: `Found ${lookup.source.description} as version ` +
|
|
663
|
+
`${match.version} of relic ${match.relic_id}.\n` +
|
|
664
|
+
`Call relic_republish(${JSON.stringify(
|
|
665
|
+
republishCall?.arguments
|
|
666
|
+
)}).`,
|
|
667
|
+
},
|
|
668
|
+
],
|
|
669
|
+
structuredContent,
|
|
670
|
+
isError: false,
|
|
671
|
+
},
|
|
672
|
+
};
|
|
673
|
+
} catch (error) {
|
|
674
|
+
return { jsonrpc: '2.0', id, result: toolError(error) };
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
|
|
510
678
|
async function callRepublish(
|
|
511
679
|
id: string | number | null,
|
|
512
680
|
params: Record<string, unknown>,
|
|
@@ -571,6 +739,7 @@ async function callRepublish(
|
|
|
571
739
|
'\n' +
|
|
572
740
|
`What Relic knows: ${result.disclosure_url}`,
|
|
573
741
|
},
|
|
742
|
+
{ type: 'text', text: VERSION_HISTORY_DISCLOSURE },
|
|
574
743
|
],
|
|
575
744
|
structuredContent: result,
|
|
576
745
|
isError: false,
|
|
@@ -691,13 +860,14 @@ What the service operator can see: that a relic exists, roughly how big it is,
|
|
|
691
860
|
what coarse class it was declared as, the publishing IP, and when it was
|
|
692
861
|
fetched. Never the contents, and never the key.
|
|
693
862
|
|
|
694
|
-
What this keeps on disk: for each relic you publish, its id,
|
|
695
|
-
publish token, in a 0600 file under your user config directory.
|
|
696
|
-
|
|
863
|
+
What this keeps on disk: for each relic you publish, its id, source identity,
|
|
864
|
+
key, and publish token, in a 0600 file under your user config directory. The
|
|
865
|
+
source index lets a fresh session find the id for relic_republish. The key and
|
|
866
|
+
token let that republish keep the same URL, and they are why republishing works
|
|
697
867
|
only on the machine that published. The token's SHA-256 is the only copy the
|
|
698
|
-
service ever holds, and neither secret is ever printed or logged. Deleting
|
|
699
|
-
|
|
700
|
-
|
|
868
|
+
service ever holds, and neither secret is ever printed or logged. Deleting the
|
|
869
|
+
file changes nothing for existing links; it only ends this machine's ability
|
|
870
|
+
to update those relics.
|
|
701
871
|
|
|
702
872
|
What this does NOT protect against: the key is returned to your agent in the
|
|
703
873
|
URL, so it enters the model's context and your session transcript. That is
|