Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions PRIVACY.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ Trace artifacts, cache files, plugins, user adapters, and site memory are stored

## Local site-memory seed lookup

`WEBCMD_GLOBAL_MEMORY_URL` enables a public unauthenticated GET `<base>/v1/site-memory/seeds/<punycode-product-key>` on first access when no local memory exists. The request uses a 2-second timeout and no retry. It discloses the resolved product/domain.
Webcmd defaults to a public unauthenticated GET `https://api.webcmd.dev/v1/site-memory/seeds/<punycode-product-key>` on first access when no local product memory exists. The request discloses only the resolved product/domain; it sends no credentials, page contents, local memory, or candidate evidence. `WEBCMD_GLOBAL_MEMORY=off` disables the request. `WEBCMD_GLOBAL_MEMORY_URL` is only a developer/test override.

With no URL configured, learning is local-only and Webcmd makes no seed request. `WEBCMD_GLOBAL_MEMORY=off` disables even a configured URL.
The lookup uses a 2-second timeout and no retry, and never refreshes initialized memory.

## Candidate public-IP provenance

Expand Down
16 changes: 11 additions & 5 deletions src/site-memory/seed-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ const urlEnv = { WEBCMD_GLOBAL_MEMORY_URL: 'https://api.webcmd.dev' };

describe('global seed client', () => {
it.each([{}, { WEBCMD_GLOBAL_MEMORY_URL: ' ' }])(
'does not fetch without a configured remote URL',
'uses the official default URL without a non-empty override',
async (env) => {
const fetch = vi.fn();
const fetch = vi.fn(async (input: RequestInfo | URL) => {
expect(String(input)).toBe('https://api.webcmd.dev/v1/site-memory/seeds/example.test');
return jsonResponse({ revision: 'seed-1', site: '# Example\n' });
});
await expect(createHttpSeedProvider({ fetch, env }).lookup('example.test'))
.resolves.toEqual({ status: 'unattempted' });
expect(fetch).not.toHaveBeenCalled();
.resolves.toEqual({ status: 'available', revision: 'seed-1', site: '# Example\n' });
expect(fetch).toHaveBeenCalledTimes(1);
},
);

Expand Down Expand Up @@ -78,7 +81,10 @@ describe('global seed client', () => {

expect(result).toEqual({ status: 'lookup-failed' });
expect(calls).toBe(1);
expect(Date.now() - started).toBeGreaterThanOrEqual(2000);
// A few ms of scheduler slack under CI is expected: AbortSignal.timeout's
// internal timer can fire a hair before the full duration has elapsed
// relative to Date.now()'s millisecond sampling.
expect(Date.now() - started).toBeGreaterThanOrEqual(1990);
expect(Date.now() - started).toBeLessThan(4000);
});

Expand Down
5 changes: 3 additions & 2 deletions src/site-memory/seed-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export interface GlobalSeedProvider {
}

const LOOKUP_TIMEOUT_MS = 2000;
export const DEFAULT_GLOBAL_MEMORY_URL = 'https://api.webcmd.dev';

export function createHttpSeedProvider(options: {
fetch?: typeof fetch;
Expand All @@ -15,8 +16,8 @@ export function createHttpSeedProvider(options: {

return {
async lookup(productKey, signal) {
const baseUrl = env.WEBCMD_GLOBAL_MEMORY_URL?.trim();
if (env.WEBCMD_GLOBAL_MEMORY === 'off' || !baseUrl) return { status: 'unattempted' };
if (env.WEBCMD_GLOBAL_MEMORY === 'off') return { status: 'unattempted' };
const baseUrl = env.WEBCMD_GLOBAL_MEMORY_URL?.trim() || DEFAULT_GLOBAL_MEMORY_URL;

const base = baseUrl.replace(/\/+$/, '');
const url = `${base}/v1/site-memory/seeds/${encodeURIComponent(productKey)}`;
Expand Down
2 changes: 1 addition & 1 deletion src/site-memory/self-learning.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ describe('self-learning lifecycle', () => {
url: 'https://local.test/',
taskId: 'task-local',
homeDir,
seedProvider: createHttpSeedProvider({ fetch: localFetch, env: {} }),
seedProvider: createHttpSeedProvider({ fetch: localFetch, env: { WEBCMD_GLOBAL_MEMORY: 'off' } }),
});

expect(seeded.manifest?.seed).toEqual({ status: 'available', revision: 'seed-1' });
Expand Down
8 changes: 4 additions & 4 deletions src/skills.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -542,10 +542,10 @@ describe('public copy', () => {
expect(intro).toMatch(/except|seed lookup/i);
expect(privacy).toMatch(/unauthenticated GET/i);
expect(privacy).toContain('/v1/site-memory/seeds/');
expect(privacy).not.toMatch(/default(?: base)?(?: is|:)[^\n]*api\.webcmd\.dev/i);
expect(privacy).toMatch(/WEBCMD_GLOBAL_MEMORY_URL[^\n]*(?:enables|configured)/i);
expect(privacy).toMatch(/absent|no URL|not configured|without a(?:n)?(?: configured)? URL/i);
expect(privacy).toMatch(/local-only|no request|does not (?:make|send|perform) (?:a |the )?seed/i);
expect(privacy).toMatch(/defaults?[^\n]*api\.webcmd\.dev/i);
expect(privacy).toMatch(/WEBCMD_GLOBAL_MEMORY_URL[^\n]*(?:developer|test)[^\n]*override/i);
expect(privacy).toMatch(/no local product memory/i);
expect(privacy).toMatch(/never refresh(?:es)? initialized memory/i);
expect(privacy).toMatch(/2-second timeout|2 second timeout/i);
expect(privacy).toMatch(/no retry/i);
expect(privacy).toContain('WEBCMD_GLOBAL_MEMORY=off');
Expand Down
Loading