relic-mcp 0.3.1 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/dist/relic-mcp.js +344 -38
- package/package.json +1 -1
- package/skills/relic/SKILL.md +21 -3
- package/src/publish.ts +115 -1
- package/src/server.ts +182 -20
- 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
|
|
@@ -98,9 +109,12 @@ export const TOOL_DEFINITION = {
|
|
|
98
109
|
name: TOOL_NAME,
|
|
99
110
|
title: 'Publish a relic',
|
|
100
111
|
description:
|
|
101
|
-
'Encrypt a file on this machine and publish it as a relic, returning
|
|
102
|
-
'shareable URL.
|
|
103
|
-
'
|
|
112
|
+
'Encrypt a file on this machine and publish it as a new relic, returning ' +
|
|
113
|
+
'a shareable URL. Publishing an update this way costs a second URL that ' +
|
|
114
|
+
'nobody holding the first one will ever see; use relic_republish instead ' +
|
|
115
|
+
'so the existing URL keeps working. The encryption key is generated ' +
|
|
116
|
+
'locally and never sent to the service. Takes a filesystem path, never ' +
|
|
117
|
+
'inline content.',
|
|
104
118
|
inputSchema: {
|
|
105
119
|
type: 'object',
|
|
106
120
|
properties: {
|
|
@@ -123,6 +137,14 @@ export const TOOL_DEFINITION = {
|
|
|
123
137
|
'relic is kept until it is deleted. Shorter is better for ' +
|
|
124
138
|
'sensitive content.',
|
|
125
139
|
},
|
|
140
|
+
force_new: {
|
|
141
|
+
type: 'boolean',
|
|
142
|
+
default: false,
|
|
143
|
+
description:
|
|
144
|
+
'Optional. Publish a deliberately separate relic even when this ' +
|
|
145
|
+
'machine already published the same source. Defaults to false. Use ' +
|
|
146
|
+
'only when you want two independent URLs for one file.',
|
|
147
|
+
},
|
|
126
148
|
},
|
|
127
149
|
required: ['path'],
|
|
128
150
|
additionalProperties: false,
|
|
@@ -228,6 +250,64 @@ export const REPUBLISH_TOOL_DEFINITION = {
|
|
|
228
250
|
},
|
|
229
251
|
} as const;
|
|
230
252
|
|
|
253
|
+
export const LOOKUP_TOOL_DEFINITION = {
|
|
254
|
+
name: LOOKUP_TOOL_NAME,
|
|
255
|
+
title: 'Look up a published source',
|
|
256
|
+
description:
|
|
257
|
+
'Look up whether this machine already published a file and return the ' +
|
|
258
|
+
'relic id needed by relic_republish. Reads local publish state only and ' +
|
|
259
|
+
'never calls the service.',
|
|
260
|
+
inputSchema: {
|
|
261
|
+
type: 'object',
|
|
262
|
+
properties: {
|
|
263
|
+
path: {
|
|
264
|
+
type: 'string',
|
|
265
|
+
description: 'Filesystem path to the source to look up.',
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
required: ['path'],
|
|
269
|
+
additionalProperties: false,
|
|
270
|
+
},
|
|
271
|
+
outputSchema: {
|
|
272
|
+
type: 'object',
|
|
273
|
+
properties: {
|
|
274
|
+
found: { type: 'boolean' },
|
|
275
|
+
relic_id: { type: ['string', 'null'] },
|
|
276
|
+
version: { type: ['integer', 'null'], minimum: 1 },
|
|
277
|
+
resolved_path: { type: 'string' },
|
|
278
|
+
source_identity: { type: 'string' },
|
|
279
|
+
source_description: { type: 'string' },
|
|
280
|
+
republish_call: {
|
|
281
|
+
type: ['object', 'null'],
|
|
282
|
+
properties: {
|
|
283
|
+
name: { type: 'string', const: REPUBLISH_TOOL_NAME },
|
|
284
|
+
arguments: {
|
|
285
|
+
type: 'object',
|
|
286
|
+
properties: {
|
|
287
|
+
relic_id: { type: 'string' },
|
|
288
|
+
path: { type: 'string' },
|
|
289
|
+
},
|
|
290
|
+
required: ['relic_id', 'path'],
|
|
291
|
+
additionalProperties: false,
|
|
292
|
+
},
|
|
293
|
+
},
|
|
294
|
+
required: ['name', 'arguments'],
|
|
295
|
+
additionalProperties: false,
|
|
296
|
+
},
|
|
297
|
+
},
|
|
298
|
+
required: [
|
|
299
|
+
'found',
|
|
300
|
+
'relic_id',
|
|
301
|
+
'version',
|
|
302
|
+
'resolved_path',
|
|
303
|
+
'source_identity',
|
|
304
|
+
'source_description',
|
|
305
|
+
'republish_call',
|
|
306
|
+
],
|
|
307
|
+
additionalProperties: false,
|
|
308
|
+
},
|
|
309
|
+
} as const;
|
|
310
|
+
|
|
231
311
|
export const DESCRIBE_TOOL_DEFINITION = {
|
|
232
312
|
name: DESCRIBE_TOOL_NAME,
|
|
233
313
|
title: 'Describe the Relic client',
|
|
@@ -269,9 +349,9 @@ export const CAPABILITIES = { tools: {} } as const;
|
|
|
269
349
|
* The plugin ships a skill with the same facts, but a skill only reaches
|
|
270
350
|
* Claude Code, and only when somebody installs the plugin rather than wiring
|
|
271
351
|
* this server directly. Every other client saw tool descriptions and nothing
|
|
272
|
-
* else, which left
|
|
352
|
+
* else, which left five things an agent cannot read off a schema.
|
|
273
353
|
*
|
|
274
|
-
* Item
|
|
354
|
+
* Item five is the reason this exists at all rather than living only in the
|
|
275
355
|
* publish result. The result is returned after the file is written, which is
|
|
276
356
|
* too late for an agent that already linked a stylesheet from a CDN. This
|
|
277
357
|
* lands before generation, which is the only moment the advice can be taken.
|
|
@@ -279,20 +359,24 @@ export const CAPABILITIES = { tools: {} } as const;
|
|
|
279
359
|
* It costs context on every session, so it stays short and it stays true.
|
|
280
360
|
* Anything that needs a paragraph belongs in the skill or the disclosure.
|
|
281
361
|
*/
|
|
282
|
-
export const INSTRUCTIONS = `Relic
|
|
283
|
-
|
|
284
|
-
|
|
362
|
+
export const INSTRUCTIONS = `Relic encrypts a file on this machine and uploads \
|
|
363
|
+
only ciphertext. The key lives in the URL fragment, which browsers never send \
|
|
364
|
+
to a server.
|
|
285
365
|
|
|
286
|
-
|
|
366
|
+
Five things that change how you should act:
|
|
287
367
|
|
|
288
368
|
1. The link is the credential. Anyone holding it, fragment included, can read \
|
|
289
369
|
the file. Do not paste it into a tracker, a log, or a public channel.
|
|
290
370
|
2. Publishing puts the key in this transcript. That is structural rather than \
|
|
291
371
|
a defect, and worth saying plainly when you hand the link over.
|
|
292
|
-
3.
|
|
372
|
+
3. If a source was published before, use relic_republish so its URL keeps \
|
|
373
|
+
working. Publishing it as new costs a second URL that nobody holding the first \
|
|
374
|
+
one will ever see. relic_publish refuses by default, relic_lookup_source \
|
|
375
|
+
recovers the id, and force_new is only for a deliberate second link.
|
|
376
|
+
4. A relic can be republished only from the machine that published it, which \
|
|
293
377
|
is where its key and publish token are stored. Anywhere else it refuses, and \
|
|
294
378
|
no retry changes that.
|
|
295
|
-
|
|
379
|
+
5. Rendered HTML and JSX run in an isolated frame with no network access. \
|
|
296
380
|
Inline the styles, scripts, fonts, and images a page needs, because a CDN \
|
|
297
381
|
reference renders as nothing. Decide that before you write the file.`;
|
|
298
382
|
|
|
@@ -362,6 +446,7 @@ export async function handleMessage(
|
|
|
362
446
|
result: {
|
|
363
447
|
tools: [
|
|
364
448
|
TOOL_DEFINITION,
|
|
449
|
+
LOOKUP_TOOL_DEFINITION,
|
|
365
450
|
REPUBLISH_TOOL_DEFINITION,
|
|
366
451
|
DESCRIBE_TOOL_DEFINITION,
|
|
367
452
|
],
|
|
@@ -418,8 +503,9 @@ async function callTool(
|
|
|
418
503
|
plaintext_transmitted_to_service: false,
|
|
419
504
|
ciphertext_destination: 'object storage, via a signed URL',
|
|
420
505
|
local_publish_state:
|
|
421
|
-
'relic id, key, and publish token per relic,
|
|
422
|
-
'the user config directory;
|
|
506
|
+
'relic id, source identity, key, and publish token per relic, ' +
|
|
507
|
+
'written 0600 under the user config directory; key and token ' +
|
|
508
|
+
'are never printed or sent',
|
|
423
509
|
service_origin: deps.serviceOrigin,
|
|
424
510
|
},
|
|
425
511
|
isError: false,
|
|
@@ -427,6 +513,10 @@ async function callTool(
|
|
|
427
513
|
};
|
|
428
514
|
}
|
|
429
515
|
|
|
516
|
+
if (params['name'] === LOOKUP_TOOL_NAME) {
|
|
517
|
+
return callLookup(id, params, deps);
|
|
518
|
+
}
|
|
519
|
+
|
|
430
520
|
if (params['name'] === REPUBLISH_TOOL_NAME) {
|
|
431
521
|
return callRepublish(id, params, deps);
|
|
432
522
|
}
|
|
@@ -462,9 +552,23 @@ async function callTool(
|
|
|
462
552
|
);
|
|
463
553
|
}
|
|
464
554
|
|
|
555
|
+
const forceNew = args['force_new'];
|
|
556
|
+
if (forceNew !== undefined && typeof forceNew !== 'boolean') {
|
|
557
|
+
return errorResponse(
|
|
558
|
+
id,
|
|
559
|
+
ERROR_CODES.invalidParams,
|
|
560
|
+
'`force_new` must be a boolean or omitted'
|
|
561
|
+
);
|
|
562
|
+
}
|
|
563
|
+
|
|
465
564
|
try {
|
|
466
565
|
const result = await publish(
|
|
467
|
-
{
|
|
566
|
+
{
|
|
567
|
+
path,
|
|
568
|
+
filename,
|
|
569
|
+
ttl_days: ttlDays.days,
|
|
570
|
+
force_new: forceNew === true,
|
|
571
|
+
},
|
|
468
572
|
deps
|
|
469
573
|
);
|
|
470
574
|
return {
|
|
@@ -507,6 +611,63 @@ async function callTool(
|
|
|
507
611
|
}
|
|
508
612
|
}
|
|
509
613
|
|
|
614
|
+
async function callLookup(
|
|
615
|
+
id: string | number | null,
|
|
616
|
+
params: Record<string, unknown>,
|
|
617
|
+
deps: PublishDeps
|
|
618
|
+
): Promise<JsonRpcResponse> {
|
|
619
|
+
const args = (params['arguments'] ?? {}) as Record<string, unknown>;
|
|
620
|
+
const path = args['path'];
|
|
621
|
+
if (typeof path !== 'string' || path.length === 0) {
|
|
622
|
+
return errorResponse(
|
|
623
|
+
id,
|
|
624
|
+
ERROR_CODES.invalidParams,
|
|
625
|
+
'`path` is required and must be a string'
|
|
626
|
+
);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
try {
|
|
630
|
+
const lookup = await lookupPublishedSource(path, deps);
|
|
631
|
+
const match = lookup.match;
|
|
632
|
+
const republishCall =
|
|
633
|
+
match === undefined
|
|
634
|
+
? null
|
|
635
|
+
: republishToolCall(match.relic_id, lookup.resolved_path);
|
|
636
|
+
const structuredContent = {
|
|
637
|
+
found: match !== undefined,
|
|
638
|
+
relic_id: match?.relic_id ?? null,
|
|
639
|
+
version: match?.version ?? null,
|
|
640
|
+
resolved_path: lookup.resolved_path,
|
|
641
|
+
source_identity: lookup.source.identity,
|
|
642
|
+
source_description: lookup.source.description,
|
|
643
|
+
republish_call: republishCall,
|
|
644
|
+
};
|
|
645
|
+
return {
|
|
646
|
+
jsonrpc: '2.0',
|
|
647
|
+
id,
|
|
648
|
+
result: {
|
|
649
|
+
content: [
|
|
650
|
+
{
|
|
651
|
+
type: 'text',
|
|
652
|
+
text:
|
|
653
|
+
match === undefined
|
|
654
|
+
? `No prior relic is recorded for ${lookup.source.description}.`
|
|
655
|
+
: `Found ${lookup.source.description} as version ` +
|
|
656
|
+
`${match.version} of relic ${match.relic_id}.\n` +
|
|
657
|
+
`Call relic_republish(${JSON.stringify(
|
|
658
|
+
republishCall?.arguments
|
|
659
|
+
)}).`,
|
|
660
|
+
},
|
|
661
|
+
],
|
|
662
|
+
structuredContent,
|
|
663
|
+
isError: false,
|
|
664
|
+
},
|
|
665
|
+
};
|
|
666
|
+
} catch (error) {
|
|
667
|
+
return { jsonrpc: '2.0', id, result: toolError(error) };
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
510
671
|
async function callRepublish(
|
|
511
672
|
id: string | number | null,
|
|
512
673
|
params: Record<string, unknown>,
|
|
@@ -691,13 +852,14 @@ What the service operator can see: that a relic exists, roughly how big it is,
|
|
|
691
852
|
what coarse class it was declared as, the publishing IP, and when it was
|
|
692
853
|
fetched. Never the contents, and never the key.
|
|
693
854
|
|
|
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
|
-
|
|
855
|
+
What this keeps on disk: for each relic you publish, its id, source identity,
|
|
856
|
+
key, and publish token, in a 0600 file under your user config directory. The
|
|
857
|
+
source index lets a fresh session find the id for relic_republish. The key and
|
|
858
|
+
token let that republish keep the same URL, and they are why republishing works
|
|
697
859
|
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
|
-
|
|
860
|
+
service ever holds, and neither secret is ever printed or logged. Deleting the
|
|
861
|
+
file changes nothing for existing links; it only ends this machine's ability
|
|
862
|
+
to update those relics.
|
|
701
863
|
|
|
702
864
|
What this does NOT protect against: the key is returned to your agent in the
|
|
703
865
|
URL, so it enters the model's context and your session transcript. That is
|