Compare commits

..
Author SHA1 Message Date
alvinfeiandCursor 5d0f569ae4 fix(memhub): satisfy biome format checks for custom harness pairing
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-21 11:28:56 +08:00
alvinfei 4ab8618550 feat: pair custom harnesses with one-time codes 2026-10-21 11:13:42 +08:00
20 changed files with 137 additions and 312 deletions

No files matched your search

+8 -12
View File
@@ -4,11 +4,7 @@ import { createReadStream } from "node:fs";
import { access } from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import {
type HealthResponse,
type ImportJob,
isHostId,
} from "@one-memory/contracts";
import type { HealthResponse, ImportJob } from "@one-memory/contracts";
import {
type AcquiredRuntimeDistribution,
acquireRuntimeDistribution,
@@ -5160,10 +5156,10 @@ type HostCommand =
args: unknown[];
};
// Host ids are read with the same grammar as the permission layer and the
// grant store (`@one-memory/contracts`): `deepseek-harness:web` is a real id —
// that host is configured per profile — and `host list` prints it, so rejecting
// it here would mean listing a host the CLI then refuses to act on.
// `deepseek-harness:web` is a real id — that host is configured per profile —
// and `host list` prints it. Rejecting it here would mean listing a host the
// CLI then refuses to act on.
const HOST_ID = /^[a-z0-9][a-z0-9-]{0,31}(:[a-z0-9][a-z0-9-]{0,31})?$/;
const FINGERPRINT = /^[0-9a-f]{64}$/;
/** Host subcommands.
@@ -5186,7 +5182,7 @@ function parseHostCommand(rest: readonly string[]): HostCommand | null {
const token = tokens[index];
if (token === "--host") {
const value = tokens[index + 1];
if (!value || !isHostId(value)) return null;
if (!value || !HOST_ID.test(value)) return null;
if (hostId !== undefined) return null;
hostId = value;
index += 1;
@@ -5219,7 +5215,7 @@ function parseHostCommand(rest: readonly string[]): HostCommand | null {
qualifier !== undefined &&
(verb === "status" || verb === "install" || verb === "uninstall")
) {
if (!isHostId(qualifier)) return null;
if (!HOST_ID.test(qualifier)) return null;
hostId = qualifier;
// Drop the consumed host positional so the verb/qualifier checks below see
// the same shape as the flag form.
@@ -5235,7 +5231,7 @@ function parseHostCommand(rest: readonly string[]): HostCommand | null {
positional[2] !== undefined
) {
const trailing = positional[2];
if (!isHostId(trailing)) return null;
if (!HOST_ID.test(trailing)) return null;
hostId = trailing;
positional.splice(2, 1);
verb = positional[0];
-3
View File
@@ -41,10 +41,7 @@ export default defineConfig({
// The shared operations package ships TypeScript sources, so it must be
// inlined into the CJS bundle the same way runtime-control is; otherwise the
// emitted bundle tries to `require` a `.js` file that only exists as `.ts`.
// `@one-memory/contracts` is the same kind of package and is imported for a
// value (the host-id grammar), so it has to be inlined too.
noExternal: [
"@one-memory/contracts",
"@one-memory/operations",
"@one-memory/runtime-control",
"axios",
+3 -3
View File
@@ -1685,7 +1685,7 @@ def create_app(
# --- Reader-only internal endpoints. Registered here, before the public
# routes below; no early return, so every route after this still registers.
@app.get("/internal/semantic-ids", dependencies=protected)
@app.get("/internal/semantic-ids")
async def internal_semantic_ids(
query: str = Query(min_length=1, max_length=2_000),
limit: int = Query(default=12, ge=1, le=20),
@@ -1697,7 +1697,7 @@ def create_app(
return {"ids": []}
return {"ids": [memory_id for memory_id, _score in hits]}
@app.post("/internal/call-event", dependencies=protected)
@app.post("/internal/call-event")
async def internal_call_event(request: Request) -> dict[str, bool]:
"""Side effects the Reader cannot write: it opens with query_only."""
try:
@@ -1716,7 +1716,7 @@ def create_app(
return {"recorded": False}
return {"recorded": bool(recorded)}
@app.get("/internal/graph-snapshot", dependencies=protected)
@app.get("/internal/graph-snapshot")
async def internal_graph_snapshot(
limit: int = Query(default=160, ge=1, le=300),
) -> dict[str, object]:
@@ -31,6 +31,7 @@ from one_memory.hy_runtime_config import (
diagnostic_log_level,
)
from one_memory.hy_source_context import HySourceContext, HyWriteBindings
from one_memory.hy_store_concurrency import native_store_read_slot
from one_memory.model_profiles import ModelProfileSnapshot, ProviderKind, attach_disable_thinking
from one_memory.model_request_audit import audit_precipitation_request
from one_memory.semantic_deadline import SemanticMemoryHit
@@ -983,29 +984,27 @@ class HyMemoryBridge:
caller could only rank by position — which forced semantic hits to be
placed as a block ahead of everything the keyword lane found. The
score is what lets the two lanes be ranked against each other.
Native search runs without a process-local write gate. MemHub expects
concurrent read during precipitation; store work is a short burst after
network-bound embed/LLM steps, and refusing reads with
``hy_memory_store_busy`` only pushed callers onto weaker fallbacks.
"""
client = await self._get_client()
try:
result = await _joined_thread(
client.search,
query,
user_ids=[user_id],
agent_ids=[] if agent_id == "*" else [agent_id],
limit=limit,
min_score=hy_relevance_floor(),
schema_min_score=hy_relevance_floor(),
schema_limit=limit,
intention_limit=limit,
scene="normal",
reader=HY_MEMORY_READER,
)
except Exception as error:
raise HyMemoryBridgeError("hy_memory_search_failed") from error
async with native_store_read_slot() as admitted:
if not admitted:
raise HyMemoryBridgeError("hy_memory_store_busy")
client = await self._get_client()
try:
result = await _joined_thread(
client.search,
query,
user_ids=[user_id],
agent_ids=[] if agent_id == "*" else [agent_id],
limit=limit,
min_score=hy_relevance_floor(),
schema_min_score=hy_relevance_floor(),
schema_limit=limit,
intention_limit=limit,
scene="normal",
reader=HY_MEMORY_READER,
)
except Exception as error:
raise HyMemoryBridgeError("hy_memory_search_failed") from error
hits = _replied_memories(result, "hy_memory_search_shape_unknown")
found: list[SemanticMemoryHit] = []
seen: set[str] = set()
@@ -0,0 +1,41 @@
"""Process-local coordination for HY's native stores."""
from __future__ import annotations
import asyncio
import weakref
from collections.abc import AsyncIterator
from contextlib import asynccontextmanager
_WRITE_LOCKS: weakref.WeakKeyDictionary[asyncio.AbstractEventLoop, asyncio.Lock] = (
weakref.WeakKeyDictionary()
)
def native_store_write_lock() -> asyncio.Lock:
"""Return the write lock for the current event loop."""
loop = asyncio.get_running_loop()
lock = _WRITE_LOCKS.get(loop)
if lock is None:
lock = asyncio.Lock()
_WRITE_LOCKS[loop] = lock
return lock
@asynccontextmanager
async def native_store_read_slot() -> AsyncIterator[bool]:
"""Admit a native HY read only while no write is active.
Chroma's native reader may retain the GIL after an asyncio timeout. Reads
therefore share the physical-store lock, but never wait behind a writer:
host recall can immediately fall back to the governance ledger.
"""
lock = native_store_write_lock()
if lock.locked():
yield False
return
await lock.acquire()
try:
yield True
finally:
lock.release()
@@ -140,6 +140,8 @@ async def search_within(
return (), "failed"
error = pending.exception()
if error is not None:
if str(error) == "hy_memory_store_busy":
return (), "timed_out"
return (), "failed"
found = pending.result()
normalized = normalize_semantic_hits(found)
-41
View File
@@ -55,44 +55,3 @@ def test_browser_origin_is_checked_in_addition_to_token() -> None:
)
== 200
)
async def internal_request(
path: str,
*,
token: str | None,
method: str = "GET",
) -> int:
settings = Settings(api_token=SecretStr("test-token"))
app = create_app(settings)
headers = {}
if token:
headers["Authorization"] = f"Bearer {token}"
async with AsyncClient(
transport=ASGITransport(app=app),
base_url="http://test",
) as client:
response = await client.request(method, path, headers=headers, json={})
return response.status_code
def test_reader_internal_endpoints_require_local_token() -> None:
for path in (
"/internal/semantic-ids?query=anything",
"/internal/graph-snapshot",
):
assert asyncio.run(internal_request(path, token=None)) == 401
assert asyncio.run(internal_request(path, token="test-token")) == 200
assert (
asyncio.run(
internal_request("/internal/call-event", token=None, method="POST")
)
== 401
)
assert (
asyncio.run(
internal_request("/internal/call-event", token="test-token", method="POST")
)
== 200
)
@@ -436,6 +436,13 @@ def test_unavailable_semantic_result_cannot_discard_lexical_memory(app, monkeypa
assert body["paths"]["memory"]["reason"] == "lexical_only"
def test_busy_hy_store_is_reported_as_a_timed_out_semantic_lane() -> None:
async def busy(_query: str) -> tuple[str, ...]:
raise RuntimeError("hy_memory_store_busy")
assert asyncio.run(search_within(busy, "q", 1.0)) == ((), "timed_out")
def test_context_diagnostics_report_each_lane_without_changing_v1(app, monkeypatch): # noqa: F811
from one_memory import context_routes
@@ -6,6 +6,7 @@ import sqlite3
import threading
import time
from collections.abc import Mapping, Sequence
from contextlib import asynccontextmanager
from datetime import datetime
from pathlib import Path
from types import SimpleNamespace
@@ -1198,6 +1199,40 @@ def test_bridge_preserves_sdk_formatted_search_content(tmp_path) -> None:
assert hits[0].formatting_complete is True
def test_semantic_search_skips_native_store_while_a_write_is_active(tmp_path, monkeypatch) -> None:
client = FakeHyMemoryClient(
{"success": True},
search_result={"memories": [{"memory_id": "must-not-be-read"}]},
)
bridge = HyMemoryBridge(
profile=snapshot(
ProviderKind.OLLAMA,
base_url="http://127.0.0.1:11434",
has_api_key=False,
),
api_key=None,
data_dir=tmp_path,
client=client,
)
@asynccontextmanager
async def busy_store():
yield False
monkeypatch.setattr("one_memory.hy_memory_bridge.native_store_read_slot", busy_store)
with pytest.raises(HyMemoryBridgeError, match="hy_memory_store_busy"):
asyncio.run(
bridge.search_ids(
"导入期间查询",
user_id="local",
agent_id="*",
)
)
assert client.search_calls == []
def test_search_ids_does_not_create_nodes_tasks_or_collections(tmp_path) -> None:
"""Search is a read. Creating store objects here would invent a graph.
+1 -1
View File
@@ -37,7 +37,7 @@ Preview 不会弹出给用户;开关点击本身被视为确认。关闭时通
| [Qwen Code](qwen-code.md) | 开放 | MCP + Skill | 无自动 Hook | `$QWEN_HOME/projects` 扫描 |
| [Pi](pi.md) | 开放 | CLI Skill,不使用 MCP | JS Capture Extension | Pi session 扫描 + Extension 实时入口 |
| [OpenClaw](openclaw.md) | 正式产品冻结 | 当前没有;仅有未开放的底层 MCP 写入器 | 没有 Hook、Plugin 或 Prefetch | Adapter 也被冻结,开关不可用 |
| [DeepSeek Harness](deepseek-harness.md) | 详情页排除,来源管理页按 Profile 接入 | Profile 级 MCP Patch | 无自动 Hook | `$DSH_HOME/sessions` 扫描 |
| [DeepSeek Harness](deepseek-harness.md) | 详情页排除;Profile UI 有 Bug | Profile 级 MCP Patch | 无自动 Hook | `$DSH_HOME/sessions` 扫描 |
## 同步开关的统一精确语义
@@ -1,18 +1,18 @@
# DeepSeek Harness 一键插入支持
更新日期:2026-10-18
更新日期:2026-10-14
## 结论
DeepSeek Harness 的 Profile 级 MCP 安装器已经实现,Profile Scoped Host ID(`deepseek-harness:<profile>`)在权限层按不透明标签处理,可正常走完 Preview 与安装。
DeepSeek Harness 的 Profile 级 MCP 安装器已经实现,但当前桌面 Profile 安装会因 Host ID 校验 Bug 在 Preview 阶段失败。
## 接入方式和产品状态
| 项目 | DeepSeek Harness 当前实现 |
|---|---|
| 应用详情页一键插入 | 有意排除,因为无法选择 Profile |
| 来源管理页 Profile 安装 | 可用;每个 Profile 独立 Patch、Credential 与 Grant |
| Harness 查询 MemHub | 通过 Profile 级 Cordis MCP Client |
| 来源管理页 Profile 安装 | UI 显示,但当前因 Host ID Bug 失败 |
| Harness 查询 MemHub | 理论上通过 Profile 级 Cordis MCP Client |
| Skill | 没有 |
| Prompt 前自动 Prefetch | 没有 |
| Capture Hook/Plugin | 没有 |
@@ -22,7 +22,7 @@ DeepSeek Harness 的 Profile 级 MCP 安装器已经实现,Profile Scoped Host
### “一键插入应用”
应用详情页不可用。来源管理页对每个 Profile 写 `cordis.patch.yml`、创建独立 Credential 并授权;Host ID 形如 `deepseek-harness:<profile>`,冒号只作为标签存在,权限层的安装目录按 Host ID 摘要生成,不参与路径拼接。
应用详情页不可用。来源管理页理论上对每个 Profile 写 `cordis.patch.yml`、创建独立 Credential 并授权;但当前 `deepseek-harness:<profile>` 含冒号,被权限层拒绝,所以实际不写文件。
### 打开“同步开关”
@@ -89,9 +89,9 @@ DeepSeek Harness 不安装 Skill,也不安装 Capture Hook;新会话进入 M
- 首次导入批量连接也排除它。
- 来源管理页会列出每个 Profile,并显示独立安装按钮。
## 已修复:Profile Host ID 校验
## 当前确定 Bug
此前来源管理页安装会在写入 `cordis.patch.yml` 和创建 Credential 之前失败,原因是:
来源管理页安装实际会失败:
```text
系统生成 hostId = deepseek-harness:<profile>
@@ -99,7 +99,7 @@ HostPermissions 只接受 [a-z][a-z0-9-]*
HostGrantStore 也不接受冒号
```
Host ID 是不可解释的标签:安装目录对它取摘要,不会把它拼进路径。现在权限层与 Grant 存储都接受一个可选的 `:<profile>` 段,因此 Profile 安装可以正常走完。这不是 DeepSeek Harness 不支持 MCP 的问题,而是 Profile Host ID 格式与权限校验不一致,现已对齐。
因此安装在写入 `cordis.patch.yml` 和创建 Credential 之前就被拒绝。这不是 DeepSeek Harness 不支持 MCP,而是 Profile Host ID 格式与权限校验不一致。
## 修复后的权限语义风险
@@ -109,7 +109,7 @@ Host ID 是不可解释的标签:安装目录对它取摘要,不会把它拼
- `all-apps`;
- 所有已授权 Source 和 Workspace。
这比应用详情页通用授权更宽。Host ID 修复后仍应确认是否希望每个 Profile 默认获得如此宽的权限。
这比应用详情页通用授权更宽。修复 Host ID 后应确认是否希望每个 Profile 默认获得如此宽的权限。
## 重复安装与卸载
@@ -1,64 +0,0 @@
import { describe, expect, it } from "vitest";
import { HOST_ID_MAX_LENGTH, HOST_ID_PATTERN, isHostId } from "./index";
/**
* The grammar is shared by the CLI, the permission layer and the grant store,
* so the boundary cases are pinned once here. The optional profile segment
* exists because a host can be configured per profile
* (`deepseek-harness:desktop`).
*/
const VALID: readonly string[] = [
"codex",
"claude-code",
"pi",
"a1",
"9lives", // the CLI has always accepted a digit-leading host id
"codex-",
"deepseek-harness:desktop",
"deepseek-harness:web",
"a:b",
"2code:9profile",
"a".repeat(64), // host segment at its maximum
`a:${"b".repeat(32)}`, // profile segment at its maximum
`${"a".repeat(64)}:${"b".repeat(32)}`, // the whole id at its maximum
];
const INVALID: readonly string[] = [
"",
" codex",
"codex ",
"DeepSeek-Harness:web", // upper case
"deepseek harness:web", // space
"-codex", // hyphen-first host
"deepseek-harness:", // empty profile
"deepseek-harness:a:b", // more than one profile segment
":web",
"a:",
"a::b",
"a:b:c",
`a${"b".repeat(64)}`, // host segment one over the maximum
`a:${"b".repeat(33)}`, // profile segment one over the maximum
"deepseek_harness:web", // underscore
"deepseek-harness:web\n", // `$` alone would accept this
];
describe("host id grammar", () => {
it.each(VALID)("accepts %j", (value) => {
expect(isHostId(value)).toBe(true);
expect(HOST_ID_PATTERN.test(value)).toBe(true);
expect(value.length).toBeLessThanOrEqual(HOST_ID_MAX_LENGTH);
});
it.each(INVALID)("rejects %j", (value) => {
expect(isHostId(value)).toBe(false);
expect(HOST_ID_PATTERN.test(value)).toBe(false);
});
it("is a type guard for strings only", () => {
for (const value of [undefined, null, 7, {}, []]) {
expect(isHostId(value)).toBe(false);
}
expect(isHostId("codex")).toBe(true);
});
});
-43
View File
@@ -1,47 +1,4 @@
export const CORE_SERVICE = "one-memory-core" as const;
/**
* The one host-id grammar.
*
* A host id is an opaque label for the app a recall grant belongs to. It is
* never a path component: the permission layer keys an installation by a
* digest of the whole id, so a colon is a label separator, not a directory
* separator.
*
* One app can be configured per profile, which is the reason for the single
* optional `:<profile>` segment — DeepSeek Harness reports
* `deepseek-harness:<profile>` and `host list` prints those ids.
*
* The CLI, the permission layer and the grant store all have to read the same
* shape: an id that `host list` prints must be an id `host install` accepts.
* There is one definition here rather than three regexes that drift.
*/
const HOST_SEGMENT_MAX = 64;
const PROFILE_SEGMENT_MAX = 32;
/**
* Lower-case letters, digits and hyphens; first character alphanumeric; at
* most one `:` followed by an alphanumeric-first profile segment.
*
* The trailing `(?![\s\S])` is "end of input". `$` alone would also match
* before a final newline, so `"codex\n"` would pass a pattern a caller reads
* as an exact match.
*/
export const HOST_ID_PATTERN = new RegExp(
`^[a-z0-9][a-z0-9-]{0,${HOST_SEGMENT_MAX - 1}}(?::[a-z0-9][a-z0-9-]{0,${PROFILE_SEGMENT_MAX - 1}})?(?![\\s\\S])`,
);
/** Longest id the grammar can produce: host + ":" + profile. */
export const HOST_ID_MAX_LENGTH = HOST_SEGMENT_MAX + 1 + PROFILE_SEGMENT_MAX;
export function isHostId(value: unknown): value is string {
return (
typeof value === "string" &&
value.length <= HOST_ID_MAX_LENGTH &&
HOST_ID_PATTERN.test(value)
);
}
export type {
ConnectionSetupCapability,
ConnectionSetupExecute,
@@ -44,7 +44,7 @@ await build({
format: ["esm"],
platform: "node",
target: "node24",
noExternal: ["@one-memory/contracts", "axios"],
noExternal: ["axios"],
banner: {
js: 'import { createRequire as __hookCreateRequire } from "node:module"; const require = __hookCreateRequire(import.meta.url);',
},
@@ -913,36 +913,3 @@ describe.skipIf(process.platform === "win32")("HostGrantStore", () => {
});
});
});
it("accepts one profile segment in a host id and still rejects malformed ones", async () => {
// DeepSeek Harness is configured per profile and reports
// `deepseek-harness:<profile>`; the installation directory is a digest of the
// id, so the colon is a label rather than a path component. The boundary
// cases themselves live in `@one-memory/contracts`; this tests that the grant
// store reads the same grammar and revalidates it on load.
const context = await fixture();
const issued = await context.store.createInstallation({
hostId: "deepseek-harness:desktop",
});
expect(issued.receipt.hostId).toBe("deepseek-harness:desktop");
// Every row is validated again on load; the colon has to survive that pass.
const reopened = await HostGrantStore.open(context.directory);
expect(
(await reopened.listInstallations()).map((row) => row.hostId),
).toContain("deepseek-harness:desktop");
for (const hostId of ["9lives", "a:b", `a:${"b".repeat(32)}`]) {
await expect(
context.store.createInstallation({ hostId }),
).resolves.toMatchObject({ receipt: { hostId } });
}
for (const hostId of [
"deepseek-harness:",
"deepseek-harness:a:b",
"DeepSeek-Harness:web",
"deepseek harness:web",
]) {
await expect(
context.store.createInstallation({ hostId }),
).rejects.toMatchObject({ code: "host_grant_invalid_input" });
}
});
@@ -18,7 +18,6 @@ import {
import path from "node:path";
import { setTimeout as delay } from "node:timers/promises";
import { inspect } from "node:util";
import { isHostId } from "@one-memory/contracts";
/**
* Trusted local control plane, not an MCP tool or an HTTP authentication route.
@@ -712,12 +711,12 @@ function validMutation(input: HostGrantMutation): boolean {
}
function validHostId(value: unknown): value is string {
// Same grammar as the permission layer and the CLI: a host id is an opaque
// label, one optional `:<profile>` segment included, and the grant store
// must not reject an id the other two accept. Grants are keyed by
// installation UUID and the installation directory is a digest of the host
// id, so the colon never becomes part of a path.
return isHostId(value);
return (
typeof value === "string" &&
value.length <= 64 &&
value === value.trim() &&
/^[a-z][a-z0-9-]*$/.test(value)
);
}
function validDisplayName(value: unknown): value is string {
@@ -169,7 +169,7 @@ it.each(["recall-and-sync", "sync-only"] as const)(
format: ["esm"],
platform: "node",
target: "node20",
noExternal: ["@one-memory/contracts", "axios"],
noExternal: ["axios"],
banner: {
js: 'import { createRequire as __mhCreateRequire } from "node:module"; const require = __mhCreateRequire(import.meta.url);',
},
+1 -5
View File
@@ -7,9 +7,5 @@ export default defineConfig({
outDir: "dist",
// Preserve the separately built sync-hook asset directory.
clean: false,
// `@one-memory/contracts` is a private, source-only workspace package
// (`exports` points at `src/index.ts`). Left external, the emitted CJS asks
// Node for a `.js` file that only exists as `.ts`, and the MCP entry fails at
// startup; inline it the way runtime-control already inlines it.
noExternal: ["@one-memory/contracts", "axios"],
noExternal: ["axios"],
});
@@ -14,7 +14,6 @@ import path from "node:path";
import { RecallBroker, readHostCredentialFile } from "@one-memory/host-bridge";
import {
codexLayout,
deepseekHarnessLayout,
piLayout,
type RecallInstallPreview,
} from "@one-memory/host-bridge/installer";
@@ -259,61 +258,6 @@ it("remote preview is read-only, exact approval installs one real Codex identity
expect(f.dispatched).toContain("host:install");
});
it("accepts a profile-scoped DeepSeek Harness host id instead of calling the transport unsupported", async () => {
// A Harness profile is configured by one `cordis.patch.yml`, so the host ID
// carries the profile (`deepseek-harness:<profile>`). The installation
// directory is a digest of that ID, so the colon is a label, not a path, and
// the permission layer has to read it as one. Before this, `supported()`
// rejected every Harness profile and `host install` told the caller its
// transport was unsupported.
const f = await fixture();
const dshHome = path.join(f.root, ".dsh");
const profileDirectory = path.join(dshHome, "profiles", "desktop");
await mkdir(profileDirectory, { recursive: true, mode: 0o700 });
await chmod(dshHome, 0o700);
await writeFile(path.join(profileDirectory, "cordis.yml"), "[]\n", {
mode: 0o600,
});
await writeFile(path.join(profileDirectory, "cordis.patch.yml"), "[]\n", {
mode: 0o600,
});
const layout = {
...deepseekHarnessLayout({ dshHome, profile: "desktop" }),
hostId: "deepseek-harness:desktop",
hostLabel: "DeepSeek Harness · desktop",
};
expect(await f.owner.getPermission(layout)).toMatchObject({
state: "not-installed",
receipt: null,
});
const preview: RecallInstallPreview = await f.owner.previewInstall(layout);
expect(preview.hostId).toBe("deepseek-harness:desktop");
expect(preview.fingerprint).toMatch(/^[a-f0-9]{64}$/);
const patchFile = preview.files[0];
if (!patchFile?.hunk) throw Error("preview missing patch hunk");
const added = patchFile.hunk.added.join("\n");
expect(added).toContain("one-memory-recall");
expect(added).toContain("'@deepseek-ai/dsh-mcp-client'");
if (layout.configPath === null) throw Error("layout missing config path");
expect(await readFile(layout.configPath, "utf8")).toBe("[]\n");
expect(await f.owner.hostGrantStore.listInstallations()).toEqual([]);
});
it("reads the same host-id grammar the CLI uses, including a digit-leading id", async () => {
// The CLI has always accepted `9codex`; the permission layer used to require
// a leading letter, so `host list` could print an id `host install` refused.
// Both import `isHostId` from `@one-memory/contracts` now.
const f = await fixture();
const layout = { ...f.layout, hostId: "9codex" };
expect(await f.owner.getPermission(layout)).toMatchObject({
state: "not-installed",
receipt: null,
});
const preview = await f.owner.previewInstall(layout);
expect(preview.hostId).toBe("9codex");
});
it("remote explicit revoke changes the original owner receipt and rejects stale or false confirmation", async () => {
const f = await fixture();
const preview = await f.client.previewInstall(f.layout);
@@ -10,7 +10,6 @@ import {
unlink,
} from "node:fs/promises";
import path from "node:path";
import { isHostId } from "@one-memory/contracts";
import {
createClaudeCodeRemovalTarget,
createClaudeCodeTarget,
@@ -1061,19 +1060,10 @@ function status(
};
}
/** Whether a layout can hold a revocable credential.
*
* The id grammar lives in `@one-memory/contracts` so that this layer, the
* grant store and the CLI cannot drift: a host id `host list` prints has to be
* one `host install` accepts. Profile scoping is what DeepSeek Harness needs —
* it configures recall per profile (`deepseek-harness:desktop`) — and the
* installation directory is a digest of the whole id, so the colon never
* becomes a path component.
*/
function supported(layout: RecallHostLayout): boolean {
return (
(layout.channel === "mcp" || layout.channel === "cli") &&
isHostId(layout.hostId) &&
/^[a-z][a-z0-9-]{0,63}$/.test(layout.hostId) &&
layout.hostId !== "openclaw"
);
}