Every 1.0.4 home must pass the offline SDK cutover, but the runtime service could start Core on a home whose migration failed, so it served legacy. Callers also asked Core for the memory authority before many calls and kept v1 data-plane branches that only ever 409 on SDK. Migration and startup: - Write every migration step to logs/sdk-migration.log (content-free JSON lines) from Desktop, CLI, the runtime service and Core. - Migrate before the runtime service spawns Core; Core refuses a legacy home with sdk_migration_required. - Desktop shows a dialog that reveals the log and opens a GitHub issue, then quits. CLI install/upgrade no longer roll back to a version without the gate; start/restart/status/doctor name the log. - Cycle the service when the migration status is unreadable so no pre-upgrade Runtime keeps serving. SDK only: - Web, host-bridge and CLI call v2 only; drop the authority query. - Delete Core's v1 data plane, the Reader and /api/v2/memory-authority. - Fix MCP memory write on v2, MemHub_conversations, memhub recall and status, v2 call events and WorkBuddy delivery reports. - Remove dead features: matters, boundary peek/ack, memory plan/apply, the legacy library and its v1 operations. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
332 lines
8.2 KiB
JavaScript
332 lines
8.2 KiB
JavaScript
#!/usr/bin/env node
|
|
// Real-process evidence for CLI-20/21: runs the built CLI against a temporary
|
|
// local HTTP stand-in for Core over a real socket, in a throwaway data
|
|
// directory. Synthetic data only; no real user library, no Runtime, no model.
|
|
//
|
|
// node apps/cli/scripts/real-read-probe.mjs
|
|
//
|
|
// It prints every request the stand-in received, so "the CLI only issued GET
|
|
// to the allowlist" is observed rather than asserted.
|
|
|
|
import { spawn } from "node:child_process";
|
|
import { mkdtemp, writeFile } from "node:fs/promises";
|
|
import { createServer } from "node:http";
|
|
import { tmpdir } from "node:os";
|
|
import path from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const CLI = fileURLToPath(new URL("../dist/one-memory.cjs", import.meta.url));
|
|
const INSTANCE = "probe-instance";
|
|
const TOKEN = "probe-token-not-a-real-credential";
|
|
|
|
const MEMORY = {
|
|
memory_id: "probe-1",
|
|
content: "部署脚本要先跑 verify_merge_plan。",
|
|
layer: "l2_fact",
|
|
status: "active",
|
|
is_latest: true,
|
|
supersedes: [],
|
|
superseded_by: [],
|
|
tags: [],
|
|
owner: null,
|
|
source_raw_ids: ["probe-raw-1"],
|
|
user_id: "local",
|
|
agent_id: "codex",
|
|
session_id: "probe-session",
|
|
source: "codex",
|
|
memory_at: 1758240000,
|
|
gmt_created: 1758240000,
|
|
};
|
|
|
|
const ROUTES = new Map([
|
|
[
|
|
"/api/v1/runtime/identity",
|
|
{
|
|
status: "ok",
|
|
service: "one-memory-core",
|
|
version: "probe-1.0.0",
|
|
instance_id: INSTANCE,
|
|
},
|
|
],
|
|
[
|
|
"/api/v1/sources",
|
|
[
|
|
{
|
|
source_id: "codex",
|
|
display_name: "Codex",
|
|
storage_kinds: ["history"],
|
|
available: true,
|
|
authorized: true,
|
|
detected: true,
|
|
root_label: "Codex history",
|
|
candidate_path: "/synthetic/codex",
|
|
candidate_kind: "default",
|
|
imported_conversations: 3,
|
|
imported_messages: 41,
|
|
capabilities: {
|
|
historical_import: "implemented",
|
|
incremental_import: "implemented",
|
|
live_sync: "implemented",
|
|
recall: "implemented",
|
|
installer: "not_implemented",
|
|
verification: "fixture_only",
|
|
},
|
|
sync_completeness: "additions_only",
|
|
},
|
|
],
|
|
],
|
|
[
|
|
"/api/v1/live-sync",
|
|
[
|
|
{
|
|
source_id: "codex",
|
|
mode: "polling",
|
|
state: "running",
|
|
last_job_id: null,
|
|
last_attempt_at: null,
|
|
last_success_at: null,
|
|
last_error_code: null,
|
|
},
|
|
],
|
|
],
|
|
[
|
|
"/api/v1/memory-processing",
|
|
{
|
|
pending: 1,
|
|
fresh_pending: 1,
|
|
backlog_pending: 0,
|
|
outside_window_pending: 0,
|
|
running: 0,
|
|
waiting_for_model: 1,
|
|
completed: 4,
|
|
failed: 0,
|
|
blocked_stages: [],
|
|
},
|
|
],
|
|
[
|
|
"/api/v1/import-jobs",
|
|
[
|
|
{
|
|
job_id: "probe-job",
|
|
user_id: "local",
|
|
source_id: "codex",
|
|
operation: "history",
|
|
status: "running",
|
|
phase: "read",
|
|
checkpoint: null,
|
|
discovered: 3,
|
|
discovered_targets: 1,
|
|
emitted: 2,
|
|
stored: 2,
|
|
revised: 0,
|
|
merged: 0,
|
|
skipped: 0,
|
|
errors: 0,
|
|
created_at: "2026-09-19T00:00:00Z",
|
|
updated_at: "2026-09-19T00:00:01Z",
|
|
},
|
|
],
|
|
],
|
|
[
|
|
"/api/v1/model-profiles",
|
|
[
|
|
{
|
|
profile_id: "p1",
|
|
name: "probe",
|
|
provider_kind: "openai_compatible",
|
|
model: "probe-model",
|
|
has_api_key: true,
|
|
credential_state: "available",
|
|
is_active: true,
|
|
api_key: "sk-probe-must-never-be-printed",
|
|
},
|
|
],
|
|
],
|
|
[
|
|
"/api/v2/home-summary",
|
|
{
|
|
schema_version: 2,
|
|
memory_count: 1,
|
|
latest: MEMORY,
|
|
observed_at: "2026-09-19T00:00:01Z",
|
|
},
|
|
],
|
|
[
|
|
"/api/v2/memory-library",
|
|
{ memories: [MEMORY], total: 1, offset: 0, limit: 100 },
|
|
],
|
|
["/api/v2/memories/probe-1", MEMORY],
|
|
]);
|
|
|
|
const seen = [];
|
|
|
|
const server = createServer((request, response) => {
|
|
const url = new URL(request.url, "http://127.0.0.1");
|
|
seen.push({
|
|
method: request.method,
|
|
path: url.pathname,
|
|
query: url.search,
|
|
authorized: request.headers.authorization === `Bearer ${TOKEN}`,
|
|
});
|
|
if (
|
|
request.method !== "GET" ||
|
|
request.headers.authorization !== `Bearer ${TOKEN}`
|
|
) {
|
|
response.writeHead(403).end("{}");
|
|
return;
|
|
}
|
|
const body = ROUTES.get(url.pathname);
|
|
response.writeHead(body === undefined ? 404 : 200, {
|
|
"content-type": "application/json",
|
|
});
|
|
response.end(JSON.stringify(body ?? { detail: "not_found" }));
|
|
});
|
|
|
|
const dataDir = await mkdtemp(path.join(tmpdir(), "om-cli-probe-"));
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
const port = server.address().port;
|
|
|
|
await writeFile(
|
|
path.join(dataDir, "runtime.json"),
|
|
JSON.stringify({
|
|
schema_version: 1,
|
|
service: "one-memory-core",
|
|
instance_id: INSTANCE,
|
|
pid: process.pid,
|
|
parent_pid: null,
|
|
host: "127.0.0.1",
|
|
port,
|
|
started_at: new Date().toISOString(),
|
|
}),
|
|
);
|
|
await writeFile(
|
|
path.join(dataDir, "runtime-service.json"),
|
|
JSON.stringify({
|
|
schema_version: 1,
|
|
service: "one-memory-runtime",
|
|
instance_id: INSTANCE,
|
|
pid: process.pid,
|
|
status: "running",
|
|
core_port: port,
|
|
}),
|
|
);
|
|
await writeFile(path.join(dataDir, "runtime-token"), TOKEN, { mode: 0o600 });
|
|
|
|
console.log(`data dir: ${dataDir}`);
|
|
console.log(`core stand-in: http://127.0.0.1:${port}\n`);
|
|
|
|
for (const argv of [
|
|
["status"],
|
|
["status", "--json"],
|
|
["sources", "list", "--json"],
|
|
["sync", "status", "--json"],
|
|
["progress", "--json"],
|
|
["memories", "list"],
|
|
["memories", "show", "probe-1", "--json"],
|
|
]) {
|
|
const before = seen.length;
|
|
const result = await runCli(argv);
|
|
console.log(`$ one-memory ${argv.join(" ")}`);
|
|
console.log(`exit=${result.code}`);
|
|
console.log(`--stdout--\n${result.stdout}--stderr--\n${result.stderr}`);
|
|
console.log(
|
|
`--requests--\n${seen
|
|
.slice(before)
|
|
.map(
|
|
(item) =>
|
|
`${item.method} ${item.path}${item.query} auth=${item.authorized}`,
|
|
)
|
|
.join("\n")}\n`,
|
|
);
|
|
if (result.stdout.includes("sk-probe-must-never-be-printed")) {
|
|
console.log("LEAK: the server's api_key reached stdout");
|
|
process.exitCode = 1;
|
|
}
|
|
}
|
|
|
|
const watchBefore = seen.length;
|
|
const watch = await runWatchCli([
|
|
"status",
|
|
"--watch",
|
|
"--interval",
|
|
"1",
|
|
"--json",
|
|
]);
|
|
console.log("$ one-memory status --watch --interval 1 --json");
|
|
console.log(
|
|
`exit=${watch.code} events=${watch.stdout.trim().split("\n").length}`,
|
|
);
|
|
console.log(
|
|
`--requests--\n${seen
|
|
.slice(watchBefore)
|
|
.map(
|
|
(item) =>
|
|
`${item.method} ${item.path}${item.query} auth=${item.authorized}`,
|
|
)
|
|
.join("\n")}\n`,
|
|
);
|
|
|
|
const methods = [...new Set(seen.map((item) => item.method))];
|
|
console.log(`methods observed: ${methods.join(", ")}`);
|
|
console.log(
|
|
`unauthenticated requests: ${seen.filter((item) => !item.authorized).length}`,
|
|
);
|
|
server.close();
|
|
|
|
function runCli(argv) {
|
|
return new Promise((resolve) => {
|
|
const child = spawn(process.execPath, [CLI, ...argv], {
|
|
env: {
|
|
PATH: process.env.PATH,
|
|
HOME: dataDir,
|
|
ONE_MEMORY_DATA_DIR: dataDir,
|
|
},
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
});
|
|
let stdout = "";
|
|
let stderr = "";
|
|
child.stdout.on("data", (chunk) => {
|
|
stdout += chunk;
|
|
});
|
|
child.stderr.on("data", (chunk) => {
|
|
stderr += chunk;
|
|
});
|
|
child.on("close", (code) => resolve({ code, stdout, stderr }));
|
|
});
|
|
}
|
|
|
|
function runWatchCli(argv) {
|
|
return new Promise((resolve, reject) => {
|
|
const child = spawn(process.execPath, [CLI, ...argv], {
|
|
env: {
|
|
PATH: process.env.PATH,
|
|
HOME: dataDir,
|
|
ONE_MEMORY_DATA_DIR: dataDir,
|
|
},
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
});
|
|
let stdout = "";
|
|
let stderr = "";
|
|
let signalled = false;
|
|
const timeout = setTimeout(() => {
|
|
child.kill("SIGKILL");
|
|
reject(new Error("watch_probe_timeout"));
|
|
}, 8_000);
|
|
child.stdout.on("data", (chunk) => {
|
|
stdout += chunk;
|
|
if (!signalled && stdout.trim().split("\n").length >= 2) {
|
|
signalled = true;
|
|
child.kill("SIGINT");
|
|
}
|
|
});
|
|
child.stderr.on("data", (chunk) => {
|
|
stderr += chunk;
|
|
});
|
|
child.on("error", reject);
|
|
child.on("close", (code) => {
|
|
clearTimeout(timeout);
|
|
resolve({ code, stdout, stderr });
|
|
});
|
|
});
|
|
}
|