Merge PR #112: 藏書地圖看得到自己的知識——租戶字串改從寫入端來(Arcrun#108)

總管複驗(不聽自述,自己重跑並與 base 逐條比對):
  cypher  14 failed / 441 passed(base 14 / 400)⇒ 多 41 條新測試全過
          失敗清單 diff 無輸出=逐字元相同、零回歸
  閘實測  照 #105/#108 的原句形狀種一個違規(env.CONSOLE_TENANT || leo)
          → 掃描器當場抓到並指名道姓;移除後恢復綠 ⇒ 不是假綠

這道閘的價值超過本票:它擋的是「身分來自環境變數」整族,
#105 那句話今天再寫一次也會被擋。符合 leo 08-12 立的
「做平台要減少 hotfix」——修掉 bug 不算完成,要留下下次再犯會被擋住的機制。

agent 誠實標的殘項(不擋併):
  · kbdb_get_map 回 1851、Portal 總圖畫面、部分授權帳號實測——都要部署 leo21c,紅線沒碰
  · 想把閘也接進 .claude/hooks/pre-write-guard.sh(寫入前就擋),該檔受保護改不動;
    檢查器已備妥 --stdin 模式,總管代接
This commit was merged in pull request #112.
This commit is contained in:
Leo
2026-08-12 16:24:45 +00:00
25 changed files with 1513 additions and 201 deletions
+27
View File
@@ -17,6 +17,7 @@ import { loadConfig } from '../lib/config.js';
import {
wranglerAvailable,
downloadAndDeploy,
namespaceHasKnowledge,
type DeployContext,
} from '../lib/deploy.js';
@@ -63,6 +64,32 @@ export async function cmdUpdate(opts: { force?: boolean } = {}): Promise<void> {
kbdbEmbed: config.kbdb_embed !== false,
};
// Arcrun#108:把「你的知識住在哪個命名空間」同步給雲端——但**先驗再寫**。
//
// 病灶:你 push 工作流、小幫手上傳知識、MCP 查詢,用的都是 config 的 `api_key`
// 而 cypher 讀藏書地圖/搜尋/工作流時,過濾用的 owner_id 來自 worker 的環境變數
// repo toml 帶的官方預設 `CONSOLE_TENANT = "leo"`)。兩個來源對不上 ⇒ 你的東西全被濾掉。
//
// 為什麼不無條件寫:一鍵安裝的實例,知識可能本來就寫在 `CONSOLE_TENANT` 底下。
// 無條件蓋成本機 api_key,會把一台**原本正常**的實例指向空的那一格
// ——那就是 #97/#106 那類「更新一次把人家的東西弄不見」。所以查得到才寫,查不到就不碰。
if (config.api_key && config.cypher_executor_url) {
process.stdout.write(chalk.gray(' → 核對雲端要用哪個知識命名空間...'));
const hasKnowledge = await namespaceHasKnowledge(config.cypher_executor_url, config.api_key);
if (hasKnowledge === true) {
ctx.knowledgeNamespace = config.api_key;
console.log(chalk.green(' ✓'));
console.log(chalk.gray(` ARCRUN_NAMESPACE = ${config.api_key}(這個命名空間底下查得到你的知識庫)`));
} else if (hasKnowledge === false) {
console.log(chalk.yellow(' ⚠'));
console.log(chalk.gray(` ${config.api_key} 底下目前查不到任何知識庫 → 這趟不動雲端的命名空間設定`));
console.log(chalk.gray(' (若藏書地圖是空的,請把這行連同 acr update 的輸出一起回報)'));
} else {
console.log(chalk.yellow(' ⚠'));
console.log(chalk.gray(' 問不到實例(可能正在啟動或版本較舊)→ 這趟不動雲端的命名空間設定'));
}
}
// mode:'update' → 資源解析在「一顆該更新的 worker 都找不到」時會停手而不是重建一整套
//Arcrun#97 的另一道門:名字對不上時別假裝這是全新安裝)。
const result = await downloadAndDeploy(ctx, 'main', { force: opts.force, mode: 'update' });
+53
View File
@@ -298,6 +298,44 @@ export interface DeployContext {
// [[vectorize]]+[ai] binding(取消 wrangler.toml 註解段)→ embed 模組啟用。未設/false → 不建、不注入,
// base 維持 LIKE keywordfree-tier 友善)。
kbdbEmbed?: boolean;
/**
* Arcrun#108:這台實例的知識命名空間(=`~/.arcrun/config.yaml` 的 `api_key`),
* 會寫進 cypher worker 的 `ARCRUN_NAMESPACE` var,讓「讀」用的 owner_id 與「寫」的一致。
*
* **只在驗證過該 namespace 底下真的有知識時才給值**(見 `resolveKnowledgeNamespace`)——
* 給了就會覆蓋 worker 上的既有值,沒給則原封保留(preservedVars)。
*/
knowledgeNamespace?: string;
}
/**
* 這把 namespace 底下到底有沒有知識?(Arcrun#108 的「先驗再寫」)
*
* 打的是實例自己的 `GET /kbdb/map?owner_id=<ns>`cypher 既有的純轉發端點,CLI 平常就在用
* 這條路 + `X-Arcrun-API-Key`)。回傳:
* true 這個 namespace 底下查得到庫 → 寫 ARCRUN_NAMESPACE 是安全的
* false = 查得到但是空的 → 不寫(可能知識其實在別的命名空間,蓋下去會把畫面弄空)
* null = 問不到(實例還沒起來 / 舊版沒這條路 / 網路斷)→ 不寫,也不宣稱任何事
*
* 誠實邊界:這支只回答「有沒有」,不猜「應該是哪一個」。猜錯的代價是把人家的資料藏起來。
*/
export async function namespaceHasKnowledge(
cypherUrl: string,
namespace: string,
): Promise<boolean | null> {
if (!cypherUrl || !namespace) return null;
try {
const res = await fetch(
`${cypherUrl.replace(/\/+$/, '')}/kbdb/map?owner_id=${encodeURIComponent(namespace)}`,
{ headers: { 'X-Arcrun-API-Key': namespace } },
);
if (!res.ok) return null;
const body = (await res.json().catch(() => null)) as { libraries?: unknown } | null;
if (!body || !Array.isArray(body.libraries)) return null;
return body.libraries.length > 0;
} catch {
return null;
}
}
/**
@@ -561,6 +599,21 @@ export async function downloadAndDeploy(
vars.ARCRUN_BUNDLE_VERSION = stamp.version;
if (stamp.commit) vars.ARCRUN_BUNDLE_COMMIT = stamp.commit;
}
// Arcrun#108:把「你的知識實際住在哪個命名空間」告訴雲端。
//
// 為什麼需要:cypher 讀藏書地圖/搜尋/工作流時要用一個 owner_id 去過濾,而它以前拿的是
// repo toml 帶的官方預設值(`CONSOLE_TENANT = "leo"`)。寫入端(CLI push、小幫手上傳、
// MCP)用的卻是你 `~/.arcrun/config.yaml` 的 `api_key` ⇒ 兩邊對不上就整個空掉
//(leo 實撞:1854 條三元組被過濾成 0 個庫)。
//
// 🔴 **只在「這個 namespace 底下真的查得到知識」時才寫**(呼叫端已先驗過,見
// resolveKnowledgeNamespace)。理由是反過來的那個災難:一鍵安裝的實例,知識可能
// 本來就寫在 CONSOLE_TENANT 底下;若這裡無條件蓋成本機 api_key,會把一台**原本正常**
// 的實例改成指向空的那一格——跟 #97/#106 同一類「更新一次把人家的東西弄不見」。
// 驗不過就不寫;既有值由 preservedVars 原封保留,等於這趟什麼都沒改。
if (ctx.knowledgeNamespace && script === VERSION_STAMP_WORKER) {
vars.ARCRUN_NAMESPACE = ctx.knowledgeNamespace;
}
if (Object.keys(vars).length > 0) extraVarsByDir.set(dir, vars);
}
if (preservedTotal.length > 0) {