@funny-booth/agent-core 0.1.45 → 0.1.46
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/dist/approval.js +43 -6
- package/dist/extraMcps.js +1 -1
- package/dist/feedback.js +1 -1
- package/dist/fillSecretHelper.js +1 -1
- package/dist/index.js +1 -1
- package/dist/launcher.js +1 -1
- package/dist/mcpPackages.js +1 -1
- package/dist/secretInput.js +10 -2
- package/dist/secretNames.js +52 -0
- package/dist/secretPrints.js +72 -0
- package/dist/secretTools.js +119 -0
- package/dist/vault.d.ts +12 -1
- package/dist/vault.js +63 -21
- package/mod/hooks/redact.js +98 -0
- package/mod/hooks/register.js +67 -38
- package/mod/hooks/vaultGate.js +54 -0
- package/mod/hooks/words.js +1 -0
- package/package.json +1 -1
- package/dist/secretValues.js +0 -22
package/dist/vault.d.ts
CHANGED
|
@@ -7,9 +7,20 @@ export declare class VaultError extends Error {
|
|
|
7
7
|
}
|
|
8
8
|
export declare function vaultKey(): Buffer;
|
|
9
9
|
export declare function resetVault(): string[];
|
|
10
|
+
/** 人が入れた秘密の目印を入れておく項目。名前の検査(secretNameError)で使えない形にしてあり、秘密の名前とぶつからない。 */
|
|
11
|
+
export declare const META_KEY = "funny-booth:meta";
|
|
12
|
+
export type SecretOrigin = "human" | "model";
|
|
10
13
|
export declare function readSecrets(): Record<string, string>;
|
|
11
|
-
|
|
14
|
+
/** 人が入れた秘密の名前(get_secret が値を返さないもの)。 */
|
|
15
|
+
export declare function humanSecretNames(): Set<string>;
|
|
16
|
+
/**
|
|
17
|
+
* 秘密を入れる。origin が human なら人が入れた目印を付け、model なら外す(AI が値を知っている)。
|
|
18
|
+
* 鍵の作成も同じロックを使うため、ロックを取る前に鍵を用意しておく(ロックは再入できない)
|
|
19
|
+
*/
|
|
20
|
+
export declare function setSecret(name: string, value: string, origin?: SecretOrigin): void;
|
|
12
21
|
export declare function deleteSecret(name: string): boolean;
|
|
22
|
+
/** 金庫のファイルの場所(Mod が「書き換わったか」を確かめるため。secretPrints.ts)。 */
|
|
23
|
+
export declare function secretsFile(): string;
|
|
13
24
|
export declare function readSecretSites(): Record<string, string[]>;
|
|
14
25
|
/** 入れてよいサイトを決め直す(空の配列なら外す)。サイトはドメイン名にそろえたもの(secretSites.normalizeSite)を渡す。 */
|
|
15
26
|
export declare function setSecretSites(name: string, sites: string[]): void;
|
package/dist/vault.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// - 暗号鍵 : OS の鍵保管庫(macOS キーチェーン / Windows 資格情報マネージャー / Linux Secret Service)。
|
|
7
7
|
// 鍵保管庫が無い環境(WSL2・ヘッドレス Linux)では ~/.funny-booth/vault/vault.key に置き、
|
|
8
8
|
// そのことを表示する。最初に決めた保管先は store.json に記録し、以後はそこだけを使う。
|
|
9
|
-
// - 秘密 : ~/.funny-booth/vault/secrets.enc(名前→値の JSON を AES-256-CBC
|
|
9
|
+
// - 秘密 : ~/.funny-booth/vault/secrets.enc(名前→値の JSON を AES-256-CBC で暗号化。人が入れた秘密の目印も中に持つ=META_KEY)
|
|
10
10
|
// - ログイン: ~/.{toolName}/portal-token.enc(ツールごとにアカウントが違うのでツール別。鍵は共有)
|
|
11
11
|
// - 入れてよいサイト: ~/.funny-booth/vault/secret-sites.json(秘密の名前→ドメインの一覧。値ではないので暗号化しない。
|
|
12
12
|
// ブラウザへ値を見せずに入れる道具が、ここにあるサイトにだけ入れる。secretSites.ts)
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
// - 旧形式(~/.{toolName}/vault.key + secrets.enc のツール別金庫)は migrateToolVault で共有金庫へ移す。
|
|
18
18
|
// ============================================================================
|
|
19
19
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
-
exports.VaultError = void 0;
|
|
20
|
+
exports.META_KEY = exports.VaultError = void 0;
|
|
21
21
|
exports.configDir = configDir;
|
|
22
22
|
exports.ensureConfigDir = ensureConfigDir;
|
|
23
23
|
exports.vaultDir = vaultDir;
|
|
@@ -26,8 +26,10 @@ exports.decrypt = decrypt;
|
|
|
26
26
|
exports.vaultKey = vaultKey;
|
|
27
27
|
exports.resetVault = resetVault;
|
|
28
28
|
exports.readSecrets = readSecrets;
|
|
29
|
+
exports.humanSecretNames = humanSecretNames;
|
|
29
30
|
exports.setSecret = setSecret;
|
|
30
31
|
exports.deleteSecret = deleteSecret;
|
|
32
|
+
exports.secretsFile = secretsFile;
|
|
31
33
|
exports.readSecretSites = readSecretSites;
|
|
32
34
|
exports.setSecretSites = setSecretSites;
|
|
33
35
|
exports.fillableSecret = fillableSecret;
|
|
@@ -304,45 +306,84 @@ function resetVault() {
|
|
|
304
306
|
});
|
|
305
307
|
}
|
|
306
308
|
// ── 秘密(全ツール共有)────────────────────────────────────────────────────
|
|
307
|
-
|
|
309
|
+
//
|
|
310
|
+
// 復号した中身は「名前→値」の JSON。人が入れた秘密(ask_secret の画面・CLI の secret set・VS Code の「パスワードを保存」)の
|
|
311
|
+
// 目印は、同じ JSON の中の META_KEY に入れる(暗号化の内側なので、AI がファイルを書き換えて目印を外せない)。
|
|
312
|
+
// 形を変えずに1つの項目を足すだけにしたのは、同じ金庫を古い版の agent-core(別のツール)が読み書きしても壊れないため
|
|
313
|
+
// (古い版には META_KEY が名前の1つに見えるだけで、書き込むときもそのまま残る)。
|
|
314
|
+
// 目印の無い秘密(今までに入れたもの・AI が store_secret で入れたもの)は今までどおり。
|
|
315
|
+
/** 人が入れた秘密の目印を入れておく項目。名前の検査(secretNameError)で使えない形にしてあり、秘密の名前とぶつからない。 */
|
|
316
|
+
exports.META_KEY = "funny-booth:meta";
|
|
317
|
+
function parseVault(obj) {
|
|
318
|
+
const { [exports.META_KEY]: metaText, ...secrets } = obj;
|
|
319
|
+
let human = [];
|
|
320
|
+
if (typeof metaText === "string") {
|
|
321
|
+
const meta = JSON.parse(metaText);
|
|
322
|
+
if (Array.isArray(meta.human))
|
|
323
|
+
human = meta.human.filter((n) => typeof n === "string" && n in secrets);
|
|
324
|
+
}
|
|
325
|
+
return { secrets, human: new Set(human) };
|
|
326
|
+
}
|
|
327
|
+
function readVaultFile(file, key) {
|
|
308
328
|
if (!(0, fs_1.existsSync)(file))
|
|
309
|
-
return {};
|
|
329
|
+
return { secrets: {}, human: new Set() };
|
|
310
330
|
const plain = decrypt((0, fs_1.readFileSync)(file, "utf-8").trim(), key);
|
|
311
331
|
// 復号できない金庫を空として扱うと、次の書き込みで中身を消してしまう。黙らずに止める
|
|
312
332
|
if (plain === null)
|
|
313
333
|
throw new VaultError(`金庫を復号できません(${file})。鍵が変わった可能性があります。${RESET_HINT}`);
|
|
314
334
|
const obj = JSON.parse(plain);
|
|
315
|
-
return obj && typeof obj === "object" ? obj : {};
|
|
335
|
+
return parseVault(obj && typeof obj === "object" ? obj : {});
|
|
316
336
|
}
|
|
317
|
-
function
|
|
337
|
+
function readVault() {
|
|
338
|
+
return readVaultFile((0, path_1.join)(vaultDir(), SECRETS_FILE), vaultKey());
|
|
339
|
+
}
|
|
340
|
+
function writeVault(v) {
|
|
318
341
|
(0, fs_1.mkdirSync)(vaultDir(), { recursive: true, mode: 0o700 });
|
|
319
|
-
|
|
342
|
+
const human = [...v.human].filter((n) => n in v.secrets);
|
|
343
|
+
const obj = human.length ? { ...v.secrets, [exports.META_KEY]: JSON.stringify({ human }) } : v.secrets;
|
|
344
|
+
atomicWrite((0, path_1.join)(vaultDir(), SECRETS_FILE), encrypt(JSON.stringify(obj), vaultKey()));
|
|
320
345
|
}
|
|
321
|
-
//
|
|
346
|
+
// 金庫全体を復号して名前→値のマップで返す(無ければ空。目印の項目は含めない)
|
|
322
347
|
function readSecrets() {
|
|
323
|
-
return
|
|
348
|
+
return readVault().secrets;
|
|
349
|
+
}
|
|
350
|
+
/** 人が入れた秘密の名前(get_secret が値を返さないもの)。 */
|
|
351
|
+
function humanSecretNames() {
|
|
352
|
+
return readVault().human;
|
|
324
353
|
}
|
|
325
|
-
|
|
326
|
-
|
|
354
|
+
/**
|
|
355
|
+
* 秘密を入れる。origin が human なら人が入れた目印を付け、model なら外す(AI が値を知っている)。
|
|
356
|
+
* 鍵の作成も同じロックを使うため、ロックを取る前に鍵を用意しておく(ロックは再入できない)
|
|
357
|
+
*/
|
|
358
|
+
function setSecret(name, value, origin = "model") {
|
|
327
359
|
vaultKey();
|
|
328
360
|
withVaultLock(() => {
|
|
329
|
-
const
|
|
330
|
-
secrets[name] = value;
|
|
331
|
-
|
|
361
|
+
const v = readVault();
|
|
362
|
+
v.secrets[name] = value;
|
|
363
|
+
if (origin === "human")
|
|
364
|
+
v.human.add(name);
|
|
365
|
+
else
|
|
366
|
+
v.human.delete(name);
|
|
367
|
+
writeVault(v);
|
|
332
368
|
});
|
|
333
369
|
}
|
|
334
370
|
function deleteSecret(name) {
|
|
335
371
|
vaultKey();
|
|
336
372
|
return withVaultLock(() => {
|
|
337
|
-
const
|
|
338
|
-
if (!(name
|
|
373
|
+
const v = readVault();
|
|
374
|
+
if (!Object.prototype.hasOwnProperty.call(v.secrets, name))
|
|
339
375
|
return false;
|
|
340
|
-
delete secrets[name];
|
|
341
|
-
|
|
376
|
+
delete v.secrets[name];
|
|
377
|
+
v.human.delete(name);
|
|
378
|
+
writeVault(v);
|
|
342
379
|
writeSites(name, null); // 入れてよいサイトも一緒に消す
|
|
343
380
|
return true;
|
|
344
381
|
});
|
|
345
382
|
}
|
|
383
|
+
/** 金庫のファイルの場所(Mod が「書き換わったか」を確かめるため。secretPrints.ts)。 */
|
|
384
|
+
function secretsFile() {
|
|
385
|
+
return (0, path_1.join)(vaultDir(), SECRETS_FILE);
|
|
386
|
+
}
|
|
346
387
|
// ── 入れてよいサイト(秘密の名前 → ドメインの一覧)────────────────────────────
|
|
347
388
|
const SITES_FILE = "secret-sites.json";
|
|
348
389
|
function readSecretSites() {
|
|
@@ -457,11 +498,12 @@ function migrateToolVault(toolName) {
|
|
|
457
498
|
return { migrated: false, conflicts: [] }; // 別プロセスが先に移した
|
|
458
499
|
const oldKey = parseKey((0, fs_1.readFileSync)(oldKeyFile, "utf-8").trim());
|
|
459
500
|
const oldSecretsFile = (0, path_1.join)(dir, SECRETS_FILE);
|
|
460
|
-
const oldSecrets =
|
|
501
|
+
const oldSecrets = readVaultFile(oldSecretsFile, oldKey).secrets;
|
|
461
502
|
const tokenFile = (0, path_1.join)(dir, TOKEN_FILE);
|
|
462
503
|
const oldTokenPlain = (0, fs_1.existsSync)(tokenFile) ? decrypt((0, fs_1.readFileSync)(tokenFile, "utf-8").trim(), oldKey) : null;
|
|
463
|
-
const
|
|
464
|
-
|
|
504
|
+
const current = readVault();
|
|
505
|
+
const { merged, conflicts } = mergeSecrets(current.secrets, oldSecrets, toolName);
|
|
506
|
+
writeVault({ secrets: merged, human: current.human });
|
|
465
507
|
if (oldTokenPlain)
|
|
466
508
|
saveTokens(toolName, parseTokens(oldTokenPlain));
|
|
467
509
|
// 読み戻して一致を確かめてから旧いものを消す(確かめられなければ旧いものは残す)
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
// 道具の結果から金庫の秘密の値を伏せる計算(register.js から使う。Mod の外の Node.js でもテストできるよう、Mod の $ は使わない)。
|
|
2
|
+
//
|
|
3
|
+
// Mod は値そのものを持たない。agent-core の secretPrints.js から受け取った「指紋」(名前・長さ・pre・fp)だけで探す:
|
|
4
|
+
// 1. 秘密の長さごとに、文を1文字ずつずらしながら pre(多項式の値を PRE_MOD で割った余り)を計算する(転がし計算で速い)
|
|
5
|
+
// 2. pre が合った所だけ、key とその部分をつないだ SHA-256 を計算し、fp と同じなら秘密(偶然 pre が合っただけなら伏せない)
|
|
6
|
+
// 3. 重なったら長い秘密を優先する(短い値が長い値の一部でも取りこぼさない)
|
|
7
|
+
// PRE_MOD・preBase は agent-core の secretPrints.ts と同じ計算(test/redact.test.mjs で一致を確かめる)。
|
|
8
|
+
|
|
9
|
+
export const PRE_MOD = 65521
|
|
10
|
+
|
|
11
|
+
export function preBase(key) {
|
|
12
|
+
return 256 + (parseInt(key.slice(0, 8), 16) % (PRE_MOD - 256))
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/** このセッションだけの key(64 桁の 16 進数)。 */
|
|
16
|
+
export function newKey(random) {
|
|
17
|
+
return [...random(new Uint8Array(32))].map((b) => b.toString(16).padStart(2, '0')).join('')
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** 指紋から探すための表(長さ → pre → 指紋の一覧)を作る。 */
|
|
21
|
+
export function buildTable(prints) {
|
|
22
|
+
const table = new Map()
|
|
23
|
+
for (const p of prints) {
|
|
24
|
+
if (!table.has(p.len)) table.set(p.len, new Map())
|
|
25
|
+
const byPre = table.get(p.len)
|
|
26
|
+
if (!byPre.has(p.pre)) byPre.set(p.pre, [])
|
|
27
|
+
byPre.get(p.pre).push(p)
|
|
28
|
+
}
|
|
29
|
+
return table
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function sha256Hex(digest, text) {
|
|
33
|
+
const buf = await digest(new TextEncoder().encode(text))
|
|
34
|
+
return [...new Uint8Array(buf)].map((b) => b.toString(16).padStart(2, '0')).join('')
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** 文の中の秘密の場所(重ならない。長い秘密が先)。digest は SHA-256 を返す関数(Mod では crypto.subtle.digest)。 */
|
|
38
|
+
export async function findSecrets(text, table, key, digest) {
|
|
39
|
+
const base = preBase(key)
|
|
40
|
+
const found = []
|
|
41
|
+
for (const [len, byPre] of table) {
|
|
42
|
+
if (text.length < len) continue
|
|
43
|
+
let pow = 1 // base^(len-1)
|
|
44
|
+
for (let i = 1; i < len; i++) pow = (pow * base) % PRE_MOD
|
|
45
|
+
let h = 0
|
|
46
|
+
for (let i = 0; i < len; i++) h = (h * base + text.charCodeAt(i)) % PRE_MOD
|
|
47
|
+
for (let i = 0; ; i++) {
|
|
48
|
+
const candidates = byPre.get(h)
|
|
49
|
+
if (candidates) {
|
|
50
|
+
const fp = await sha256Hex(digest, key + text.slice(i, i + len))
|
|
51
|
+
const hit = candidates.find((c) => c.fp === fp)
|
|
52
|
+
if (hit) found.push({ start: i, end: i + len, name: hit.name })
|
|
53
|
+
}
|
|
54
|
+
if (i + len >= text.length) break
|
|
55
|
+
h = (((h - ((text.charCodeAt(i) * pow) % PRE_MOD) + PRE_MOD) * base) + text.charCodeAt(i + len)) % PRE_MOD
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
found.sort((a, b) => (b.end - b.start) - (a.end - a.start) || a.start - b.start)
|
|
59
|
+
const taken = []
|
|
60
|
+
for (const f of found) if (!taken.some((t) => f.start < t.end && t.start < f.end)) taken.push(f)
|
|
61
|
+
return taken.sort((a, b) => a.start - b.start)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** 秘密を ●●●●(名前) に置き換えた文。 */
|
|
65
|
+
export async function redactText(text, table, key, digest) {
|
|
66
|
+
if (table.size === 0 || typeof text !== 'string' || text === '') return text
|
|
67
|
+
const hits = await findSecrets(text, table, key, digest)
|
|
68
|
+
if (!hits.length) return text
|
|
69
|
+
let out = ''
|
|
70
|
+
let at = 0
|
|
71
|
+
for (const h of hits) { out += text.slice(at, h.start) + `●●●●(${h.name})`; at = h.end }
|
|
72
|
+
return out + text.slice(at)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/** 入れ子の値の中の文をすべて伏せる。changed は1か所でも伏せたか。 */
|
|
76
|
+
export async function redactDeep(value, table, key, digest) {
|
|
77
|
+
let changed = false
|
|
78
|
+
const walk = async (v) => {
|
|
79
|
+
if (typeof v === 'string') {
|
|
80
|
+
const r = await redactText(v, table, key, digest)
|
|
81
|
+
if (r !== v) changed = true
|
|
82
|
+
return r
|
|
83
|
+
}
|
|
84
|
+
if (Array.isArray(v)) {
|
|
85
|
+
const out = []
|
|
86
|
+
for (const x of v) out.push(await walk(x))
|
|
87
|
+
return out
|
|
88
|
+
}
|
|
89
|
+
if (v && typeof v === 'object') {
|
|
90
|
+
const out = {}
|
|
91
|
+
for (const [k, x] of Object.entries(v)) out[k] = await walk(x)
|
|
92
|
+
return out
|
|
93
|
+
}
|
|
94
|
+
return v
|
|
95
|
+
}
|
|
96
|
+
const result = await walk(value)
|
|
97
|
+
return { value: result, changed }
|
|
98
|
+
}
|
package/mod/hooks/register.js
CHANGED
|
@@ -11,14 +11,18 @@
|
|
|
11
11
|
// 金庫の秘密の値(6 文字以上)が道具の結果に出ていれば、記録にも AI にも渡る前に伏せる。
|
|
12
12
|
// 道具の結果は2か所に残るので両方で伏せる: 道具の詳しい記録(記録の toolUseResult)は tool.call で、
|
|
13
13
|
// AI が読む文(tool_result)とその他の行は session.append で。
|
|
14
|
-
//
|
|
15
|
-
//
|
|
14
|
+
// 値そのものは受け取らない。agent-core の secretPrints.js から、このセッションだけの key で作った指紋を受け取り、
|
|
15
|
+
// 指紋で探す(redact.js)。金庫のファイルが書き換わったら(途中で秘密を足した等)指紋を取り直す。
|
|
16
|
+
// 3. 金庫・鍵に触る道具の呼び出しを止める(vaultGate.js。関所であって壁ではない。サブエージェントにも効かせる)
|
|
17
|
+
// 4. 見た目(入力欄の上の帯・道具の行と作業中の行の日本語化・起動のあいさつと雷の光・豆知識・終わったときのお知らせ)
|
|
16
18
|
// どれも表示だけで、描くのに失敗したら Claude Code の元の表示に戻す(会話は止めない)。
|
|
17
19
|
// 何を出すかの計算は look.js、言葉は words.js。
|
|
18
20
|
|
|
19
21
|
import { BOLT, FLASH, DONE_MS, TIP_AFTER_MS, TOAST_AFTER_MS, greetingAt, toolLabel, toolDetail, groupSummary, duration, faceOf, bandParts } from './look.js'
|
|
20
22
|
import { knowledgeSection, loadRequest } from './knowledgeText.js'
|
|
21
23
|
import { SPINNER_WORD, GROUP_HINT, TOAST_DONE, TOAST_ASK, TIPS } from './words.js'
|
|
24
|
+
import { buildTable, newKey, redactDeep, redactText } from './redact.js'
|
|
25
|
+
import { GATE_REASON, vaultTouch } from './vaultGate.js'
|
|
22
26
|
|
|
23
27
|
const LOAD = /^mcp__[a-z0-9-]+__load_knowledge$/
|
|
24
28
|
const knowledge = new Map() // キーワード → 本文(読み込んだ順)
|
|
@@ -27,7 +31,9 @@ const knowledge = new Map() // キーワード → 本文(読み込んだ順
|
|
|
27
31
|
const injected = new Set()
|
|
28
32
|
// 節に入れたのに AI が見当たらないと言うことがある(受付箱 cc03280f)。同じカードを2回目に読まれたら本文を返す
|
|
29
33
|
const stubbed = new Set()
|
|
30
|
-
|
|
34
|
+
// 伏せるための指紋(値は持たない)。key はこのセッションだけの乱数。file・mtime で金庫の書き換わりを見る
|
|
35
|
+
const prints = { key: newKey((a) => crypto.getRandomValues(a)), table: new Map(), file: '', mtime: -1, loading: null, failedAt: 0 }
|
|
36
|
+
const digest = (bytes) => crypto.subtle.digest('SHA-256', bytes)
|
|
31
37
|
let startKeyword = '' // 起動プログラムが渡したカードのキーワード(公式のエージェントのときだけ)
|
|
32
38
|
|
|
33
39
|
function stub(keyword) {
|
|
@@ -35,40 +41,57 @@ function stub(keyword) {
|
|
|
35
41
|
`(その節が見当たらないときは、もう一度 load_knowledge で "${keyword}" を読むと本文を返します)`
|
|
36
42
|
}
|
|
37
43
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
44
|
+
const redact = (text) => redactText(text, prints.table, prints.key, digest)
|
|
45
|
+
|
|
46
|
+
async function redactBlocks(content) {
|
|
47
|
+
const out = []
|
|
48
|
+
for (const b of content) {
|
|
49
|
+
if (b.type === 'text' && typeof b.text === 'string') out.push({ ...b, text: await redact(b.text) })
|
|
50
|
+
else if (b.type === 'tool_result' && typeof b.content === 'string') out.push({ ...b, content: await redact(b.content) })
|
|
51
|
+
else if (b.type === 'tool_result' && Array.isArray(b.content)) {
|
|
52
|
+
const inner = []
|
|
53
|
+
for (const c of b.content) inner.push(c.type === 'text' ? { ...c, text: await redact(c.text) } : c)
|
|
54
|
+
out.push({ ...b, content: inner })
|
|
55
|
+
} else out.push(b)
|
|
56
|
+
}
|
|
41
57
|
return out
|
|
42
58
|
}
|
|
43
59
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
60
|
+
// agent-core の小さな入口を動かし、標準出力の JSON を返す。失敗したら理由(標準エラー)を添えて例外にする
|
|
61
|
+
async function runJson($, script, stdin) {
|
|
62
|
+
const r = await $.process.run(['node', `${$.plugin.root}/../dist/${script}`], stdin === undefined ? undefined : { stdin })
|
|
63
|
+
if (r.exitCode !== 0) throw new Error((r.stderr || `終了コード ${r.exitCode}`).trim())
|
|
64
|
+
return JSON.parse(r.stdout)
|
|
47
65
|
}
|
|
48
66
|
|
|
49
|
-
function
|
|
50
|
-
if (
|
|
51
|
-
|
|
52
|
-
if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value).map(([k, v]) => [k, redactDeep(v)]))
|
|
53
|
-
return value
|
|
67
|
+
async function vaultMtime($) {
|
|
68
|
+
if (!prints.file) return -1
|
|
69
|
+
try { return (await $.fs.stat(prints.file)).mtimeMs } catch { return 0 } // まだ金庫が無い
|
|
54
70
|
}
|
|
55
71
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
72
|
+
// 指紋を取る(起動時と、金庫のファイルが書き換わったとき)。読めなければ理由を画面に残し、1分は取り直さない(会話は止めない)
|
|
73
|
+
async function loadPrints($) {
|
|
74
|
+
if (prints.loading) return prints.loading
|
|
75
|
+
prints.loading = (async () => {
|
|
76
|
+
try {
|
|
77
|
+
const r = await runJson($, 'secretPrints.js', JSON.stringify({ key: prints.key }))
|
|
78
|
+
prints.file = r.file
|
|
79
|
+
prints.table = buildTable(r.prints)
|
|
80
|
+
prints.mtime = await vaultMtime($)
|
|
81
|
+
} catch (err) {
|
|
82
|
+
prints.failedAt = Date.now()
|
|
83
|
+
$.ui.log(`秘密を伏せる準備ができませんでした: ${err.message}`)
|
|
84
|
+
} finally { prints.loading = null }
|
|
85
|
+
})()
|
|
86
|
+
return prints.loading
|
|
65
87
|
}
|
|
66
88
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
89
|
+
async function freshPrints($) {
|
|
90
|
+
if (!prints.file) {
|
|
91
|
+
if (Date.now() - prints.failedAt > 60_000) await loadPrints($)
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
if ((await vaultMtime($)) !== prints.mtime) await loadPrints($)
|
|
72
95
|
}
|
|
73
96
|
|
|
74
97
|
// 見た目の状態(このセッションの間だけ)
|
|
@@ -131,12 +154,7 @@ export function register(on) {
|
|
|
131
154
|
$.ui.log(`手順書をシステムの指示に入れられませんでした(load_knowledge で読みます): ${err.message}`)
|
|
132
155
|
}
|
|
133
156
|
}
|
|
134
|
-
|
|
135
|
-
secrets = await runJson($, 'secretValues.js')
|
|
136
|
-
} catch (err) {
|
|
137
|
-
// 金庫を読めなければ伏せる対象が無いだけ(会話は止めない)。理由は画面に残す
|
|
138
|
-
$.ui.log(`秘密を伏せる準備ができませんでした: ${err.message}`)
|
|
139
|
-
}
|
|
157
|
+
await loadPrints($)
|
|
140
158
|
return next(e)
|
|
141
159
|
})
|
|
142
160
|
|
|
@@ -150,15 +168,25 @@ export function register(on) {
|
|
|
150
168
|
return next(e)
|
|
151
169
|
})
|
|
152
170
|
|
|
171
|
+
// 金庫・鍵に触る呼び出しは、道具を動かす前に断る(AI には短い理由だけ。何に触ったかは画面に残す。サブエージェントも同じ)。
|
|
153
172
|
// 秘密が出た道具の結果は、詳しい記録ごと伏せた形で返す(伏せたものを Claude Code が記録し、AI に渡す)
|
|
154
173
|
on('tool.call', async ($, e, next) => {
|
|
174
|
+
const what = vaultTouch(e)
|
|
175
|
+
if (what) {
|
|
176
|
+
$.ui.log(`金庫・鍵に触る呼び出しを止めました(${e.tool}・${what})`)
|
|
177
|
+
return { deny: GATE_REASON }
|
|
178
|
+
}
|
|
155
179
|
// 雷くん: 質問のときは首をかしげ(お知らせも出す)、失敗したら頭をかく
|
|
156
180
|
const mine = !e.agentId
|
|
157
181
|
if (mine && e.tool === 'AskUserQuestion') { setFace($, 'ask'); $.ui.toast(TOAST_ASK) }
|
|
158
182
|
const r = await next(e)
|
|
159
183
|
if (mine) setFace($, r?.isError ? 'fail' : 'work')
|
|
160
|
-
if (
|
|
161
|
-
|
|
184
|
+
if (r.deny) return r
|
|
185
|
+
await freshPrints($)
|
|
186
|
+
if (prints.table.size === 0) return r
|
|
187
|
+
const red = await redactDeep(r.result, prints.table, prints.key, digest)
|
|
188
|
+
if (!red.changed) return r
|
|
189
|
+
return { result: red.value, ...(r.context ? { context: r.context } : {}) }
|
|
162
190
|
})
|
|
163
191
|
|
|
164
192
|
// 起動のあいさつ(お客さんの画面に出る)には AI への指示を書かないので、「この手順書で始める」はここで伝える
|
|
@@ -176,8 +204,9 @@ export function register(on) {
|
|
|
176
204
|
})
|
|
177
205
|
|
|
178
206
|
on('session.append', { door: 'tool-result' }, async ($, e, next) => {
|
|
179
|
-
|
|
180
|
-
|
|
207
|
+
await freshPrints($)
|
|
208
|
+
if (prints.table.size === 0) return next(e)
|
|
209
|
+
return next({ ...e, message: { ...e.message, content: await redactBlocks(e.message.content) } })
|
|
181
210
|
})
|
|
182
211
|
|
|
183
212
|
// あいさつは、お客さんが最初に打つまで出しておく(1回目は起動プログラムが送る最初の一言)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// 金庫・鍵に触る道具の呼び出しを止める関所(register.js の tool.call から使う。サブエージェントの呼び出しにも効かせる)。
|
|
2
|
+
//
|
|
3
|
+
// これは「関所」であって「壁」ではない。AI は同じ PC の同じ利用者として動くので、名前を組み立て直す・別の書き方をする
|
|
4
|
+
// (例: パスの一部を変数や * に分ける、別のプログラムを書いて動かす)と、ここをすり抜けられる。
|
|
5
|
+
// 狙いは、ページや文書に仕込まれた指示に従って AI が素直に金庫を読みに行く、よくある形を止めることと、
|
|
6
|
+
// 止めたことを画面に残して気づけるようにすること。秘密の値そのものの守りは、金庫の暗号化・人が入れた秘密を返さない
|
|
7
|
+
// get_secret・ブラウザへ入れる前の本人の確認(agent-core)が受け持つ。
|
|
8
|
+
//
|
|
9
|
+
// 調べるのは、ファイルやコマンドを扱う組み込みの道具の入力の文字列すべて(Bash のコマンド・Read のパス・Edit の中身など)。
|
|
10
|
+
// 普段の開発(npm test・git status・プロジェクトのファイルを読む)は止めない(test/vaultGate.test.mjs)。
|
|
11
|
+
|
|
12
|
+
export const GATE_REASON = '金庫・鍵に触るコマンドは使えません。秘密は金庫の道具(list_secrets・get_secret・ask_secret・browser_fill_secret など)で扱ってください。'
|
|
13
|
+
|
|
14
|
+
const GATED_TOOLS = new Set(['Bash', 'PowerShell', 'Monitor', 'Read', 'Grep', 'Glob', 'LS', 'Edit', 'MultiEdit', 'Write', 'NotebookEdit', 'NotebookRead'])
|
|
15
|
+
|
|
16
|
+
// tool.call の入力のうち、道具の引数ではないもの
|
|
17
|
+
const NOT_INPUT = new Set(['tool', 'tool_use_id', 'consent', 'agentId'])
|
|
18
|
+
|
|
19
|
+
// [何に触るか, 見分け方]。金庫の置き場所は agent-core の vault.ts(~/.funny-booth/vault・Windows は %USERPROFILE%\.funny-booth\vault)
|
|
20
|
+
const RULES = [
|
|
21
|
+
['金庫のフォルダ', /\.funny-booth[\\/]+vault/i],
|
|
22
|
+
['金庫のファイル', /\b(secrets\.enc|vault\.key|secret-sites\.json)\b/i],
|
|
23
|
+
['ログインの保存先', /\bportal-token(\.[a-z0-9-]+)?\.enc\b/i],
|
|
24
|
+
['追加した MCP に渡す秘密の一覧', /\bextra-mcps\.json\b/i],
|
|
25
|
+
// OS の鍵保管庫に置いた金庫の鍵(サービス funny-booth・アカウント vault-key)と、鍵保管庫を読むコマンド
|
|
26
|
+
['金庫の鍵', /\bvault-key\b/i],
|
|
27
|
+
['macOS のキーチェーン', /\bsecurity\s+(find-generic-password|find-internet-password|dump-keychain|export)\b/i],
|
|
28
|
+
['Linux の鍵保管庫', /\bsecret-tool\b/i],
|
|
29
|
+
['Windows の資格情報', /\bcmdkey\b|CredentialManager|Get-StoredCredential|PasswordVault|\bCredRead|Windows\.Security\.Credentials/i],
|
|
30
|
+
['鍵保管庫の部品', /napi-rs[\\/]+keyring/i],
|
|
31
|
+
// agent-core の中の、金庫を開く部品(伏せるための指紋の入口を含む)
|
|
32
|
+
['金庫を開く部品', /\b(vault|fillSecretHelper|secretPrints)\.js\b/i],
|
|
33
|
+
// 端末の secret コマンドで、AI が秘密を入れ替える・消す・金庫を作り直す(人が端末で打つのは止めない)
|
|
34
|
+
['金庫を書き換えるコマンド', /\b[a-z0-9-]*(agent(os)?|agent-core|index)(\.js)?\s+secret\s+(set|rm|delete|reset)\b/i],
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
function strings(value, out = []) {
|
|
38
|
+
if (typeof value === 'string') out.push(value)
|
|
39
|
+
else if (Array.isArray(value)) for (const v of value) strings(v, out)
|
|
40
|
+
else if (value && typeof value === 'object') for (const v of Object.values(value)) strings(v, out)
|
|
41
|
+
return out
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** 金庫・鍵に触る呼び出しなら、何に触るか(画面に残す短い言葉)。触らなければ null。 */
|
|
45
|
+
export function vaultTouch(e) {
|
|
46
|
+
if (!GATED_TOOLS.has(e.tool)) return null
|
|
47
|
+
const texts = strings(Object.fromEntries(Object.entries(e).filter(([k]) => !NOT_INPUT.has(k))))
|
|
48
|
+
for (const t of texts) {
|
|
49
|
+
// フォルダと名前を分けて書いても(cd ~/.funny-booth && cat vault/…)止める
|
|
50
|
+
if (/\.funny-booth/i.test(t) && /vault/i.test(t)) return '金庫のフォルダ'
|
|
51
|
+
for (const [what, re] of RULES) if (re.test(t)) return what
|
|
52
|
+
}
|
|
53
|
+
return null
|
|
54
|
+
}
|
package/mod/hooks/words.js
CHANGED
package/package.json
CHANGED
package/dist/secretValues.js
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.REDACT_MIN = void 0;
|
|
4
|
-
exports.secretPairs = secretPairs;
|
|
5
|
-
// FUNNY BOOTH の Mod(mod/hooks/register.js)が、道具の結果から伏せる秘密の値を受け取るための小さな入口。
|
|
6
|
-
// 金庫の秘密のうち 6 文字以上のものを [名前, 値] の JSON で標準出力に出す(短い値は普通の文字まで伏せてしまうため除く)。
|
|
7
|
-
// 受け取るのは Mod(Claude Code の中のプログラム)だけで、AI や会話の記録には渡らない。
|
|
8
|
-
const vault_1 = require("./vault");
|
|
9
|
-
exports.REDACT_MIN = 6;
|
|
10
|
-
function secretPairs(secrets) {
|
|
11
|
-
return Object.entries(secrets).filter(([, v]) => typeof v === "string" && v.length >= exports.REDACT_MIN)
|
|
12
|
-
.sort((a, b) => b[1].length - a[1].length); // 長い値から伏せる(短い値が長い値の一部のとき取りこぼさない)
|
|
13
|
-
}
|
|
14
|
-
if (require.main === module) {
|
|
15
|
-
try {
|
|
16
|
-
process.stdout.write(JSON.stringify(secretPairs((0, vault_1.readSecrets)())));
|
|
17
|
-
}
|
|
18
|
-
catch {
|
|
19
|
-
process.stdout.write("[]");
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
//# sourceMappingURL=secretValues.js.map
|