@chibitek/chibi 0.1.162 → 0.1.169
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/assets/{abnfDiagram-O67JEVCF-D0_WX2x_.js → abnfDiagram-O67JEVCF-aQkRN0ee.js} +1 -1
- package/dist/assets/{arc-lzBXqTbJ.js → arc-BT7qPVAa.js} +1 -1
- package/dist/assets/{architectureDiagram-NJMV4G6O-KQZbO2M3.js → architectureDiagram-NJMV4G6O-oMlARqKW.js} +1 -1
- package/dist/assets/{blockDiagram-BEXU5L5S-zb3pW64Y.js → blockDiagram-BEXU5L5S-CKUrb4js.js} +1 -1
- package/dist/assets/{c4Diagram-YGBWAQC7-CADaXczd.js → c4Diagram-YGBWAQC7-BkL5cRIA.js} +1 -1
- package/dist/assets/channel-BZDQCBTd.js +1 -0
- package/dist/assets/{chunk-2Q5K7J3B-DJP27cEp.js → chunk-2Q5K7J3B-BMitkm2s.js} +1 -1
- package/dist/assets/{chunk-3FUC2YCW-Cp9Z7Ybl.js → chunk-3FUC2YCW-DOhVN2ed.js} +2 -2
- package/dist/assets/{chunk-5VM5RSS4-DjXxom_0.js → chunk-5VM5RSS4-BFkFo_ng.js} +1 -1
- package/dist/assets/{chunk-GTNCS2PH-DpEJ08zA.js → chunk-GTNCS2PH-QbxYUf3Q.js} +1 -1
- package/dist/assets/{chunk-GWA4HPMP-CQ4Kadr-.js → chunk-GWA4HPMP-BZChfTYw.js} +1 -1
- package/dist/assets/{chunk-JWPE2WC7-CwXmVMdh.js → chunk-JWPE2WC7-BXpmYglA.js} +1 -1
- package/dist/assets/{chunk-NETBCI7D-Vl_kib_R.js → chunk-NETBCI7D-CIZTrz1i.js} +1 -1
- package/dist/assets/{chunk-WEXAMYUT-BhAVbfJe.js → chunk-WEXAMYUT-FhqQ0sO0.js} +1 -1
- package/dist/assets/{chunk-XXDRQBXY-s1a4nKQE.js → chunk-XXDRQBXY-5ThtmFbP.js} +1 -1
- package/dist/assets/{classDiagram-v2-NBCMYWYE-DeGQSNky.js → classDiagram-v2-NBCMYWYE-BzObC5Qw.js} +1 -1
- package/dist/assets/{cose-bilkent-JH36ORCC-D5sUHYFG.js → cose-bilkent-JH36ORCC-BASV9Okv.js} +1 -1
- package/dist/assets/{cynefin-OW5HDTMX-DmTvN9T2.js → cynefin-OW5HDTMX-B0zQYMFh.js} +1 -1
- package/dist/assets/{cynefinDiagram-VND7K2PF-Cqtt1jCF.js → cynefinDiagram-VND7K2PF-DMEO1cHA.js} +1 -1
- package/dist/assets/{dagre-6A5THRUB-DRZKkSJg.js → dagre-6A5THRUB-BG04TSi_.js} +1 -1
- package/dist/assets/{diagram-22UHCM2B-1od4dKOU.js → diagram-22UHCM2B-CfpVNMhg.js} +1 -1
- package/dist/assets/{diagram-3UASUU5V-D3_yCQOJ.js → diagram-3UASUU5V-DG9L4I1L.js} +1 -1
- package/dist/assets/{diagram-ATOU4E4O-D9UgsVGC.js → diagram-ATOU4E4O-Db7Bf63S.js} +1 -1
- package/dist/assets/{diagram-CDSNMT55-DMxsJ_X5.js → diagram-CDSNMT55-Cdzv1tJn.js} +1 -1
- package/dist/assets/{diagram-MLGK6HIB-CK47Pp3D.js → diagram-MLGK6HIB-BBMeXGKE.js} +1 -1
- package/dist/assets/{diagram-MPIPVDR6-D0axYDk-.js → diagram-MPIPVDR6-BAQ22zk3.js} +1 -1
- package/dist/assets/{ebnfDiagram-ZINNZB2B-hkbV8j2o.js → ebnfDiagram-ZINNZB2B-BDZgSFSF.js} +1 -1
- package/dist/assets/{elk-276RUBZZ-DL9jncwy.js → elk-276RUBZZ-Bx4-nN8N.js} +1 -1
- package/dist/assets/{erDiagram-OPXOYQCR-etb_6SR6.js → erDiagram-OPXOYQCR-D2Pn-cqm.js} +1 -1
- package/dist/assets/{flowDiagram-KWPJA3E3-4Dgg4I8f.js → flowDiagram-KWPJA3E3-hsbvL3Ow.js} +1 -1
- package/dist/assets/{ganttDiagram-FUAMR5RP-DRBB4AaO.js → ganttDiagram-FUAMR5RP-DBUgYe4p.js} +1 -1
- package/dist/assets/{gitGraphDiagram-X574FWY7-CzgKN-tE.js → gitGraphDiagram-X574FWY7-ChGeSINg.js} +1 -1
- package/dist/assets/{index-fHtcwzVK.js → index-ByXr4FRl.js} +195 -196
- package/dist/assets/index-COwCaN7p.css +1 -0
- package/dist/assets/{index-BT2giPxq.js → index-Ch93Ygtq.js} +1 -1
- package/dist/assets/{infoDiagram-VRGFBTTK-DwpjLCMe.js → infoDiagram-VRGFBTTK-DY2EeC-A.js} +1 -1
- package/dist/assets/{ishikawaDiagram-OU5B5YK6-H-CWnU1-.js → ishikawaDiagram-OU5B5YK6-BznYuY04.js} +1 -1
- package/dist/assets/{journeyDiagram-ZHPQQLJL-CY-oDrpq.js → journeyDiagram-ZHPQQLJL-CX74ZLbW.js} +1 -1
- package/dist/assets/{kanban-definition-PNTS6WVX-DMGIKH3g.js → kanban-definition-PNTS6WVX-0DPavB-t.js} +1 -1
- package/dist/assets/{linear-BHhtTLK8.js → linear-BHeqbvM8.js} +1 -1
- package/dist/assets/{mermaid.core-BWK_Ui3n.js → mermaid.core-Dm-Ny6a0.js} +5 -5
- package/dist/assets/{mindmap-definition-NLK3R4M7-Cc17hw2c.js → mindmap-definition-NLK3R4M7-BPrMElav.js} +1 -1
- package/dist/assets/{pegDiagram-GJSIUBJH-BqFjUBJK.js → pegDiagram-GJSIUBJH-CaD7wyyX.js} +1 -1
- package/dist/assets/{pieDiagram-5QR66LMP-ikmgTCa3.js → pieDiagram-5QR66LMP-lbSp8R4K.js} +1 -1
- package/dist/assets/{quadrantDiagram-O4NWA36T-CC3s5Dsc.js → quadrantDiagram-O4NWA36T-Bs3Sn2of.js} +1 -1
- package/dist/assets/{railroadDiagram-XR7U4H2S-Dtwf869F.js → railroadDiagram-XR7U4H2S-C88ykXCV.js} +1 -1
- package/dist/assets/{requirementDiagram-PLB6GJNP-C5p7AgwQ.js → requirementDiagram-PLB6GJNP-mgIQnORW.js} +1 -1
- package/dist/assets/{sankeyDiagram-IPEJSGJF-8NKN770X.js → sankeyDiagram-IPEJSGJF-K5esEw-m.js} +1 -1
- package/dist/assets/{sequenceDiagram-PO4LG4MO-DQucgrWO.js → sequenceDiagram-PO4LG4MO-D5v8PJqQ.js} +1 -1
- package/dist/assets/{sizeCapture-INFHLROL-Cxmcj9Nl.js → sizeCapture-INFHLROL-BHBBDTEI.js} +1 -1
- package/dist/assets/{stateDiagram-v2-GCMORJYK-DEdvHXnJ.js → stateDiagram-v2-GCMORJYK-CeRdNmKQ.js} +1 -1
- package/dist/assets/{swimlanes-2SLR337P-BOGePqj1.js → swimlanes-2SLR337P-D8CbMCI8.js} +1 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-CQHcTGnI.js +8 -0
- package/dist/assets/{timeline-definition-EJHVYXUP-DfpRxJgE.js → timeline-definition-EJHVYXUP-CMG02UV1.js} +1 -1
- package/dist/assets/{usecaseDiagram-POWQR4AR-BOmQFDT4.js → usecaseDiagram-POWQR4AR-Bm8h6ijK.js} +1 -1
- package/dist/assets/{vennDiagram-UO4OBE2U-84pfbvUi.js → vennDiagram-UO4OBE2U-Dk9tFjlM.js} +1 -1
- package/dist/assets/{wardleyDiagram-VNRHLVJA-Cbxnw8ip.js → wardleyDiagram-VNRHLVJA-61vrb2Ft.js} +1 -1
- package/dist/assets/{xychartDiagram-PMCCYNJV-BLiz8vqY.js → xychartDiagram-PMCCYNJV-C7uO44ng.js} +1 -1
- package/dist/handbook/atomic-handbook.html +5 -0
- package/dist/index.html +2 -2
- package/dist-server/container-mcp.js +5 -0
- package/dist-server/drivers/agents-proxy.js +65 -41
- package/dist-server/index.js +4652 -4232
- package/dist-server/openmausbot.js +499 -282
- package/dist-server/pair-cli.js +499 -282
- package/dist-server/server/auto-approve.js +4 -1
- package/dist-server/server/brand.js +11 -0
- package/dist-server/server/config.js +13 -18
- package/dist-server/server/connection-credential.js +59 -0
- package/dist-server/server/container-computer.js +5 -26
- package/dist-server/server/credential-card-request.js +52 -0
- package/dist-server/server/dictation.js +6 -3
- package/dist-server/server/drivers/agents-call.js +3 -23
- package/dist-server/server/drivers/agents-catalog.js +2 -20
- package/dist-server/server/drivers/agents-credential.js +57 -0
- package/dist-server/server/drivers/builtIn.js +9 -1
- package/dist-server/server/drivers/claude-elicitation.js +76 -0
- package/dist-server/server/drivers/claude-stdin.js +47 -0
- package/dist-server/server/drivers/claude.js +23 -39
- package/dist-server/server/drivers/codex-elicitation.js +93 -0
- package/dist-server/server/drivers/codex.js +41 -107
- package/dist-server/server/engine-platform-fleet.js +32 -0
- package/dist-server/server/external-room.js +24 -0
- package/dist-server/server/index.js +101 -114
- package/dist-server/server/mcp-elicitation.js +120 -0
- package/dist-server/server/mcp-http.js +52 -2
- package/dist-server/server/mcp-probe.js +3 -0
- package/dist-server/server/peer-roster.js +43 -0
- package/dist-server/server/phone-secret.js +12 -4
- package/dist-server/server/routes/dictation.js +6 -1
- package/dist-server/server/system-prompt.js +1 -1
- package/dist-server/server/text-actions.js +25 -0
- package/dist-server/shared/credential-request.js +15 -6
- package/dist-server/shared/engine-platforms.js +19 -0
- package/dist-server/vps-container-mcp.js +5 -0
- package/package.json +1 -1
- package/dist/assets/channel-Br57lm8F.js +0 -1
- package/dist/assets/index-B2fJMh8N.css +0 -1
- package/dist/assets/swimlanesDiagram-TC7HE7FX-BSe9TZ6t.js +0 -8
- package/dist-server/connector-proxy.js +0 -205
- package/dist-server/server/composio.js +0 -1051
- package/dist-server/server/connector-proxy.js +0 -249
- package/dist-server/server/mcp-public-registry.js +0 -325
- package/dist-server/shared/connector-availability.js +0 -12
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
// own it is the harness that answers each tool permission (`god-mode`).
|
|
13
13
|
// Questions never come through here: a bot's question always reaches a human.
|
|
14
14
|
import { supportsApprovalMode } from "../shared/approval-mode.js";
|
|
15
|
+
import { isElicitationTool } from "./mcp-elicitation.js";
|
|
15
16
|
/** A failed delivery is a runtime error, not another permission decision.
|
|
16
17
|
* An expired ask must never become a fresh Allow/Deny card. */
|
|
17
18
|
export async function deliverFullAccessApproval(adapter, threadId, requestId, turnId, isCurrent = () => false) {
|
|
@@ -64,7 +65,9 @@ export function autoVerdict(mode, tool, context) {
|
|
|
64
65
|
// A question is for a person, whatever channel it arrived on — and
|
|
65
66
|
// whatever the mode: even Full has no answer to give, only an approval
|
|
66
67
|
// that would run the tool with none.
|
|
67
|
-
|
|
68
|
+
// An MCP server asked the person to confirm a change (elicitation): no
|
|
69
|
+
// mode answers for them, Full included (server/mcp-elicitation.ts).
|
|
70
|
+
if (isElicitationTool(tool) || ASKS_A_PERSON.has(tool.replace(/^mcp__[^_]+__/, "").toLowerCase())) {
|
|
68
71
|
return { approve: null, source: "no-grant" };
|
|
69
72
|
}
|
|
70
73
|
// Full's promise is literal: even a sandbox widening is approved. Entering
|
|
@@ -29,6 +29,15 @@ export const brandSchema = z
|
|
|
29
29
|
.max(MAX_LOGO_CHARS, `logo must be under ${MAX_LOGO_CHARS} characters (inline a small SVG or link an https:// image)`)
|
|
30
30
|
.refine((v) => v.startsWith("data:image/") || v.startsWith("https://"), "logo must be a data:image/... URI or an https:// URL")
|
|
31
31
|
.optional(),
|
|
32
|
+
/** The company whose people use this deployment: the setup wizard greets
|
|
33
|
+
* people from it with its logo. Its own name and logo, so a client's
|
|
34
|
+
* deployment can carry the client's. */
|
|
35
|
+
companyName: z.string().trim().min(1).max(80, "companyName must be 80 characters or fewer").optional(),
|
|
36
|
+
companyLogo: z
|
|
37
|
+
.string()
|
|
38
|
+
.max(MAX_LOGO_CHARS, `companyLogo must be under ${MAX_LOGO_CHARS} characters`)
|
|
39
|
+
.refine((v) => v.startsWith("data:image/") || v.startsWith("https://") || v.startsWith("/"), "companyLogo must be a data:image/... URI, an https:// URL or a path on this server")
|
|
40
|
+
.optional(),
|
|
32
41
|
/** Where "get help" points for this deployment. */
|
|
33
42
|
supportUrl: z.string().url().startsWith("https://", "supportUrl must be an https:// URL").optional(),
|
|
34
43
|
/** Tab and home-screen icon: an inline data:image/... URI (PNG or SVG, small) or an https:// URL. */
|
|
@@ -49,6 +58,8 @@ export const DEFAULT_BRAND = {
|
|
|
49
58
|
accentLight: "#004F71",
|
|
50
59
|
accentDark: "#00B398",
|
|
51
60
|
logo: "/app-icon.svg",
|
|
61
|
+
companyName: "Chibitek",
|
|
62
|
+
companyLogo: "/chibitek-logo.png",
|
|
52
63
|
};
|
|
53
64
|
export function brandFile() {
|
|
54
65
|
return process.env.OMB_BRAND_FILE || join(DATA_DIR, "brand.json");
|
|
@@ -19,6 +19,7 @@ import { isRemoteMcpServer, parseStoredMcpServer } from "./mcp-registry.js";
|
|
|
19
19
|
import { hydrateMcpServers, mcpLaunchBlocker, mcpLaunchBlockerReason, mcpServersForDisk } from "./mcp-secrets.js";
|
|
20
20
|
import { secureStdioLaunch } from "../electron/mcp-secrets.mjs";
|
|
21
21
|
import { parseJson, schemaIssue } from "./schema.js";
|
|
22
|
+
import { dropEnginesOffPlatform, onDeviceFleet, restoreEnginesOffPlatform } from "./engine-platform-fleet.js";
|
|
22
23
|
const optionalText = z.string().optional();
|
|
23
24
|
const SSH_ALIAS = /^[A-Za-z0-9][A-Za-z0-9_.-]{0,127}$/;
|
|
24
25
|
const LEGACY_BROWSER_PROFILE_ID = /^[A-Za-z0-9_-]{1,40}$/;
|
|
@@ -904,8 +905,9 @@ export function loadConfig() {
|
|
|
904
905
|
try {
|
|
905
906
|
cfg = parseStoredConfig(parseJson(readFileSync(join(DATA_DIR, "config.json"), "utf8")));
|
|
906
907
|
}
|
|
907
|
-
catch {
|
|
908
|
-
|
|
908
|
+
catch (error) {
|
|
909
|
+
if (error.code !== "ENOENT")
|
|
910
|
+
throw new Error("Saved Atomic settings could not be read; config.json has been left untouched.", { cause: error });
|
|
909
911
|
}
|
|
910
912
|
// MCP server secrets the desktop keeps in its encrypted store: config.json
|
|
911
913
|
// holds placeholders, filled here for this process only (server/mcp-secrets.ts)
|
|
@@ -1118,12 +1120,11 @@ export function saveConfig(patch, options = {}) {
|
|
|
1118
1120
|
const p = join(DATA_DIR, "config.json");
|
|
1119
1121
|
let disk = {};
|
|
1120
1122
|
try {
|
|
1121
|
-
|
|
1122
|
-
if (parsed.success)
|
|
1123
|
-
disk = configWithoutRemovedIntegration(parsed.data).document;
|
|
1123
|
+
disk = configWithoutRemovedIntegration(jsonObjectSchema.parse(parseJson(readFileSync(p, "utf8")))).document;
|
|
1124
1124
|
}
|
|
1125
|
-
catch {
|
|
1126
|
-
|
|
1125
|
+
catch (error) {
|
|
1126
|
+
if (error.code !== "ENOENT")
|
|
1127
|
+
throw new Error("Cannot save over unreadable Atomic settings; config.json has been left untouched.", { cause: error });
|
|
1127
1128
|
}
|
|
1128
1129
|
const checkedPatch = appConfigSchema.partial().extend({ threads: threadsPatchSchema.optional(), newBots: newBotsPatchSchema.optional() }).parse(patch);
|
|
1129
1130
|
const before = configSaveListeners.size ? structuredClone(disk) : disk;
|
|
@@ -1317,8 +1318,8 @@ export function withInstanceCli(cfg, instanceId, cli) {
|
|
|
1317
1318
|
return { ok: true, config: next };
|
|
1318
1319
|
}
|
|
1319
1320
|
/** Materialize defaults without freezing injected workspace settings or secrets. */
|
|
1320
|
-
export function persistableInstanceConfigs(cfg) {
|
|
1321
|
-
const map = instanceConfigs(cfg);
|
|
1321
|
+
export function persistableInstanceConfigs(cfg, platform = process.platform) {
|
|
1322
|
+
const map = instanceConfigs(cfg, undefined, undefined, platform);
|
|
1322
1323
|
for (const [id, entry] of Object.entries(map)) {
|
|
1323
1324
|
const environment = cfg.instances?.[id]?.environment;
|
|
1324
1325
|
if (environment)
|
|
@@ -1337,7 +1338,7 @@ export function persistableInstanceConfigs(cfg) {
|
|
|
1337
1338
|
else
|
|
1338
1339
|
delete entry.config;
|
|
1339
1340
|
}
|
|
1340
|
-
return map;
|
|
1341
|
+
return restoreEnginesOffPlatform(map, cfg.instances, platform);
|
|
1341
1342
|
}
|
|
1342
1343
|
/** The credential env instanceConfigs() injects for one driver at runtime.
|
|
1343
1344
|
* Each secret goes only to the driver that actually reads it: the API-key
|
|
@@ -1388,13 +1389,6 @@ const ATOMIC_FLEET = {
|
|
|
1388
1389
|
"atomic-engine": { driver: "customAcp", displayName: "Atomic Engine", config: { cli: "atomic acp" } },
|
|
1389
1390
|
ollamaCloud: { driver: "ollamaCloud", displayName: "Ollama Cloud" },
|
|
1390
1391
|
};
|
|
1391
|
-
/** Apple Intelligence runs only on Apple silicon Macs; elsewhere the engine
|
|
1392
|
-
* is left out of the fleet rather than listed as unavailable. */
|
|
1393
|
-
export function onDeviceFleet(platform = process.platform, arch = process.arch) {
|
|
1394
|
-
return platform === "darwin" && arch === "arm64"
|
|
1395
|
-
? { appleIntelligence: { driver: "appleIntelligence", displayName: "Apple Intelligence (on this Mac)" } }
|
|
1396
|
-
: {};
|
|
1397
|
-
}
|
|
1398
1392
|
function plainObject(value) {
|
|
1399
1393
|
return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
|
|
1400
1394
|
}
|
|
@@ -1427,7 +1421,7 @@ function separateClaudeLogin(instanceId, entry) {
|
|
|
1427
1421
|
const configDir = plainObject(entry.config)?.configDir;
|
|
1428
1422
|
return typeof configDir === "string" && configDir.trim() !== "";
|
|
1429
1423
|
}
|
|
1430
|
-
export function instanceConfigs(cfg, onDevice = onDeviceFleet(), accountKeys = instanceKeys()) {
|
|
1424
|
+
export function instanceConfigs(cfg, onDevice = onDeviceFleet(), accountKeys = instanceKeys(), platform = process.platform) {
|
|
1431
1425
|
// The default `grok` instance rides the `grokAgent` driver, not the API-key
|
|
1432
1426
|
// one: like claude and codex it needs no credential from us, just the CLI
|
|
1433
1427
|
// installed and logged in (it shows up unavailable otherwise). The API-key
|
|
@@ -1499,6 +1493,7 @@ export function instanceConfigs(cfg, onDevice = onDeviceFleet(), accountKeys = i
|
|
|
1499
1493
|
if (Object.hasOwn(map, id))
|
|
1500
1494
|
map[id] = withFleetDefaults(map[id], fallback);
|
|
1501
1495
|
}
|
|
1496
|
+
dropEnginesOffPlatform(map, platform);
|
|
1502
1497
|
for (const [id, sourceEntry] of Object.entries(map)) {
|
|
1503
1498
|
// instanceConfigs() builds a transient runtime map. Never mutate the
|
|
1504
1499
|
// caller's persisted entries while injecting workspace defaults: doing so
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { MAX_MCP_SERVERS, isRemoteMcpServer, parseStoredMcpServer } from "./mcp-registry.js";
|
|
3
|
+
const schema = z.object({
|
|
4
|
+
name: z.string().regex(/^[a-z][a-z0-9_-]{0,31}$/),
|
|
5
|
+
url: z.string().max(2048),
|
|
6
|
+
header: z.string().regex(/^[A-Za-z][A-Za-z0-9-]{0,127}$/),
|
|
7
|
+
prefix: z.literal("Bearer ").optional(),
|
|
8
|
+
}).strict();
|
|
9
|
+
const forbiddenHeaders = /^(host|content-.*|accept|connection|transfer-encoding|proxy-.*|mcp-.*|origin|referer|cookie|set-cookie)$/i;
|
|
10
|
+
const fail = (message, status = 400) => { throw Object.assign(new Error(message), { status }); };
|
|
11
|
+
/** Non-secret, reviewed destination only. No arbitrary config paths or URLs carrying secrets. */
|
|
12
|
+
export function connectionCredential(raw) {
|
|
13
|
+
const parsed = schema.safeParse(raw);
|
|
14
|
+
if (!parsed.success)
|
|
15
|
+
return fail("Provide a connection name, HTTPS URL and credential header, without the secret.");
|
|
16
|
+
const connection = parsed.data;
|
|
17
|
+
let url;
|
|
18
|
+
try {
|
|
19
|
+
url = new URL(connection.url);
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return fail("Use the service's full HTTPS MCP address.");
|
|
23
|
+
}
|
|
24
|
+
if (url.protocol !== "https:" || url.username || url.password || url.search || url.hash) {
|
|
25
|
+
return fail("Use an HTTPS MCP address without credentials, query parameters or fragments.");
|
|
26
|
+
}
|
|
27
|
+
if (forbiddenHeaders.test(connection.header))
|
|
28
|
+
return fail("Choose the service's credential header, such as x-api-key or Authorization.");
|
|
29
|
+
connection.url = url.href;
|
|
30
|
+
// Reuse the catalog's reserved-name and server validation.
|
|
31
|
+
const server = parseStoredMcpServer(connection.name, { type: "http", url: connection.url });
|
|
32
|
+
if (!server.ok)
|
|
33
|
+
return fail("This connection name or address is not supported.");
|
|
34
|
+
return connection;
|
|
35
|
+
}
|
|
36
|
+
/** Keep other headers and settings. Never redirect an existing connection. */
|
|
37
|
+
export function connectionCredentialServer(connection, current, value) {
|
|
38
|
+
const raw = current[connection.name];
|
|
39
|
+
const parsed = parseStoredMcpServer(connection.name, raw ?? {
|
|
40
|
+
type: "http", url: connection.url, enabled: true, autoAllowReadOnly: false,
|
|
41
|
+
});
|
|
42
|
+
if (!parsed.ok || !isRemoteMcpServer(parsed.server) || parsed.server.url !== connection.url) {
|
|
43
|
+
return fail("This connection changed or uses another address. Request a new secure card.", 409);
|
|
44
|
+
}
|
|
45
|
+
if (raw === undefined && Object.keys(current).length >= MAX_MCP_SERVERS)
|
|
46
|
+
return fail("The MCP server limit has been reached.");
|
|
47
|
+
const server = parsed.server;
|
|
48
|
+
if (value === undefined)
|
|
49
|
+
return server;
|
|
50
|
+
if (!value || Buffer.byteLength(value) > 4096 || (/[\r\n]/.test(value) || value.includes(String.fromCharCode(0))))
|
|
51
|
+
return fail("Enter a credential of 1 to 4096 bytes on one line.");
|
|
52
|
+
// HTTP field names are case-insensitive: replace just the chosen field.
|
|
53
|
+
const headers = Object.fromEntries(Object.entries(server.headers).filter(([key]) => key.toLowerCase() !== connection.header.toLowerCase()));
|
|
54
|
+
headers[connection.header] = (connection.prefix ?? "") + value;
|
|
55
|
+
return { ...server, headers };
|
|
56
|
+
}
|
|
57
|
+
export function sameConnectionCredential(a, b) {
|
|
58
|
+
return a?.name === b?.name && a?.url === b?.url && a?.header === b?.header && a?.prefix === b?.prefix;
|
|
59
|
+
}
|
|
@@ -323,28 +323,10 @@ export function localVmRecreatableOnDemand(status) {
|
|
|
323
323
|
&& status.container === "missing"
|
|
324
324
|
&& status.create_supported;
|
|
325
325
|
}
|
|
326
|
-
/**
|
|
327
|
-
*
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
* and its durable workspace mount, can simply be removed and run again: the
|
|
331
|
-
* person's files live in the host workspace folder, so only the desktop
|
|
332
|
-
* session resets. Docker or Podman stops containers whenever the runtime or
|
|
333
|
-
* the Mac restarts, so without this every such restart sent the person to
|
|
334
|
-
* App Settings to delete and recreate the Local VM by hand. Anything foreign,
|
|
335
|
-
* stale or unsafe still stays the person's call.
|
|
336
|
-
*/
|
|
337
|
-
export function localVmReplaceableWhenStopped(status) {
|
|
338
|
-
return Boolean(status.runtime
|
|
339
|
-
&& status.daemonUp
|
|
340
|
-
&& status.image
|
|
341
|
-
&& status.container === "stopped"
|
|
342
|
-
&& status.create_supported
|
|
343
|
-
&& status.managed
|
|
344
|
-
&& status.imageMatches
|
|
345
|
-
&& status.network !== "unsafe"
|
|
346
|
-
&& status.security !== "unsafe"
|
|
347
|
-
&& status.persistence === "durable");
|
|
326
|
+
/** A stopped VM may contain installed applications and files outside the mount.
|
|
327
|
+
* Replacement requires an explicit removal; startup never discards its disk. */
|
|
328
|
+
export function localVmReplaceableWhenStopped(_status) {
|
|
329
|
+
return false;
|
|
348
330
|
}
|
|
349
331
|
/** Whether Auto may attach this Local VM without a person choosing it: the
|
|
350
332
|
* desktop is ready, or its image is prepared and the container can simply be
|
|
@@ -375,11 +357,8 @@ function statusProblem(status) {
|
|
|
375
357
|
return "The existing Local VM is missing safety limits; recreate it";
|
|
376
358
|
if (status.persistence === "unsafe")
|
|
377
359
|
return "The existing Local VM is missing its durable workspace; recreate it";
|
|
378
|
-
if (status.container === "stopped" && localVmReplaceableWhenStopped(status)) {
|
|
379
|
-
return "The Local VM stopped; a fresh one starts the next time a bot uses it, and your files are kept";
|
|
380
|
-
}
|
|
381
360
|
if (status.container === "stopped")
|
|
382
|
-
return "This
|
|
361
|
+
return "The Local VM is stopped and its disk is kept. This image cannot safely resume. Export needed files before explicitly removing and recreating it.";
|
|
383
362
|
if (status.desktop_error)
|
|
384
363
|
return `The Local VM desktop failed to start: ${status.desktop_error}`;
|
|
385
364
|
if (!status.desktopReady)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { CREDENTIAL_TARGETS, credentialIsConfigured, isCredentialTargetId, isReusableCredentialRequest } from "../shared/credential-request.js";
|
|
3
|
+
import { connectionCredential, connectionCredentialServer, sameConnectionCredential } from "./connection-credential.js";
|
|
4
|
+
const answer = (status, body) => ({ status, body });
|
|
5
|
+
const handoff = (label) => `Securely provide the ${label} from Atomic on your phone or computer. It is never added to chat.`;
|
|
6
|
+
export function requestCredentialCard(body, from, fromThreadId, room, cfg, store, storage, refusalFor) {
|
|
7
|
+
if (!isCredentialTargetId(body.credentialId)) {
|
|
8
|
+
return answer(400, { error: "unsupported credential id" });
|
|
9
|
+
}
|
|
10
|
+
const credentialId = body.credentialId;
|
|
11
|
+
const target = CREDENTIAL_TARGETS[credentialId];
|
|
12
|
+
const connection = credentialId === "connectionSecret" ? connectionCredential(body.connection) : undefined;
|
|
13
|
+
if (connection) {
|
|
14
|
+
if (storage() !== "os")
|
|
15
|
+
return answer(503, { error: "Secure connection entry requires this computer's encrypted credential store." });
|
|
16
|
+
if (from.mcpServers && !from.mcpServers.includes(connection.name))
|
|
17
|
+
return answer(403, { error: "Enable this connection for this bot before requesting its credential." });
|
|
18
|
+
const server = connectionCredentialServer(connection, cfg.mcpServers ?? {});
|
|
19
|
+
const refusal = refusalFor(connection.name, server);
|
|
20
|
+
if (refusal)
|
|
21
|
+
return answer(403, { error: refusal });
|
|
22
|
+
}
|
|
23
|
+
if (credentialIsConfigured(cfg, credentialId)) {
|
|
24
|
+
return answer(200, { alreadyConfigured: true, label: target.label });
|
|
25
|
+
}
|
|
26
|
+
const existing = store.activePath(fromThreadId).find((message) => isReusableCredentialRequest(message, credentialId, from.id, room) && sameConnectionCredential(message.secret?.connection, connection));
|
|
27
|
+
if (existing) {
|
|
28
|
+
if (!existing.text?.trim()) {
|
|
29
|
+
store.patchMessage(fromThreadId, existing.id, {
|
|
30
|
+
text: handoff(target.label),
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
return answer(200, { messageId: existing.id, label: target.label });
|
|
34
|
+
}
|
|
35
|
+
const reason = typeof body.reason === "string" ? body.reason.trim().slice(0, 240) : "";
|
|
36
|
+
const message = store.appendMessage(fromThreadId, {
|
|
37
|
+
role: "bot",
|
|
38
|
+
kind: "secret",
|
|
39
|
+
text: handoff(target.label),
|
|
40
|
+
...(room ? { from: { botId: from.id, name: from.name, color: from.color } } : {}),
|
|
41
|
+
secret: {
|
|
42
|
+
target: credentialId,
|
|
43
|
+
...(connection ? { connection } : {}),
|
|
44
|
+
label: connection ? `${connection.name} credential` : target.label,
|
|
45
|
+
description: `${reason ? `${target.description} ${reason}` : target.description} ${connection ? `Save for ${connection.url}, field ${connection.header}. ` : ""}${from.name} can use it but never read it back.`,
|
|
46
|
+
placeholder: target.placeholder,
|
|
47
|
+
helpUrl: target.helpUrl,
|
|
48
|
+
requestKey: randomUUID(),
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
return answer(201, { messageId: message.id, label: target.label });
|
|
52
|
+
}
|
|
@@ -86,13 +86,16 @@ export function checkCleanup(input, output) {
|
|
|
86
86
|
}
|
|
87
87
|
return { ok: true, text: cleaned };
|
|
88
88
|
}
|
|
89
|
-
async function ask(provider, prompt, timeoutMs) {
|
|
89
|
+
export async function ask(provider, prompt, timeoutMs) {
|
|
90
90
|
if (!provider.generateText)
|
|
91
91
|
return null;
|
|
92
92
|
const expiry = new AbortController();
|
|
93
|
-
|
|
93
|
+
let timer;
|
|
94
|
+
const timeout = new Promise(resolve => {
|
|
95
|
+
timer = setTimeout(() => { expiry.abort(); resolve(null); }, timeoutMs);
|
|
96
|
+
});
|
|
94
97
|
try {
|
|
95
|
-
return await provider.generateText(prompt, { signal: expiry.signal });
|
|
98
|
+
return await Promise.race([provider.generateText(prompt, { signal: expiry.signal }), timeout]);
|
|
96
99
|
}
|
|
97
100
|
catch {
|
|
98
101
|
return null;
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
//
|
|
7
7
|
// Nothing here reads the environment or holds state of its own. The caller
|
|
8
8
|
// passes a ToolCallContext, and the per-turn counters live on it.
|
|
9
|
-
import {
|
|
9
|
+
import { requestCredential } from "./agents-credential.js";
|
|
10
10
|
import { normalizeCronSchedule } from "../../shared/routine-schedule.js";
|
|
11
11
|
import { peerRecallSpeaker } from "../peer-provenance.js";
|
|
12
12
|
import { peerName } from "../peer-roster.js";
|
|
@@ -835,28 +835,8 @@ export async function callTool(name, args, context) {
|
|
|
835
835
|
const message = typeof r.message === "string" ? r.message : `Updated room ${roomId}.`;
|
|
836
836
|
return { text: message };
|
|
837
837
|
}
|
|
838
|
-
if (name === "request_credential")
|
|
839
|
-
|
|
840
|
-
if (!isCredentialTargetId(credentialId)) {
|
|
841
|
-
return { text: "request_credential needs a supported credential_id.", isError: true };
|
|
842
|
-
}
|
|
843
|
-
const reason = typeof args.reason === "string" ? args.reason.trim().slice(0, 240) : "";
|
|
844
|
-
const r = await api("/api/internal/request-credential", {
|
|
845
|
-
method: "POST",
|
|
846
|
-
body: JSON.stringify({
|
|
847
|
-
fromBotId: BOT_ID,
|
|
848
|
-
fromThreadId: THREAD_ID,
|
|
849
|
-
credentialId,
|
|
850
|
-
...(reason ? { reason } : {}),
|
|
851
|
-
}),
|
|
852
|
-
});
|
|
853
|
-
if (r.alreadyConfigured) {
|
|
854
|
-
return { text: `${r.label ?? CREDENTIAL_TARGETS[credentialId].label} is already configured. Continue the task.` };
|
|
855
|
-
}
|
|
856
|
-
return {
|
|
857
|
-
text: `A secure ${r.label ?? CREDENTIAL_TARGETS[credentialId].label} request is ready. The desktop app and a freshly QR-paired mobile app show its secure entry card; older mobile pairings explain how to pair again or finish on the computer. End this turn; Atomic will resume the task after the user saves or declines. Never ask them to paste the key into chat.`,
|
|
858
|
-
};
|
|
859
|
-
}
|
|
838
|
+
if (name === "request_credential")
|
|
839
|
+
return requestCredential(args, BOT_ID, THREAD_ID, api);
|
|
860
840
|
if (name === "list_routines") {
|
|
861
841
|
const query = new URLSearchParams({ fromBotId: BOT_ID, fromThreadId: THREAD_ID });
|
|
862
842
|
const r = await api(`/api/internal/routines?${query.toString()}`);
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// turn. It is pinned byte for byte, and its size budgeted, by
|
|
11
11
|
// agents-catalog-wire.test.ts: change a description or a schema on purpose,
|
|
12
12
|
// then update the goldens there.
|
|
13
|
-
import {
|
|
13
|
+
import { credentialTool } from "./agents-credential.js";
|
|
14
14
|
import { PRIVATE_BOT_HIDDEN_TOOLS, agentToolAnnotations } from "../agent-tool-policy.js";
|
|
15
15
|
/** The profile a spawned proxy was given. Everything is off unless the
|
|
16
16
|
* harness says "1". */
|
|
@@ -450,25 +450,7 @@ const toolDefinitions = (externalRuntime) => [
|
|
|
450
450
|
required: ["room_id", "action"],
|
|
451
451
|
},
|
|
452
452
|
},
|
|
453
|
-
|
|
454
|
-
name: "request_credential",
|
|
455
|
-
description: "Ask the user for a supported API key through Atomic's secure credential flow. The desktop app and a freshly QR-paired mobile app show a secure entry card; older mobile pairings show how to pair again or finish on the computer. Never claim a secure field opened unless this request succeeds, and never ask the user to paste a secret into chat. The secret is saved by the desktop app and is never returned to you. After calling this tool, end the turn; Atomic resumes the task after the user saves or declines.",
|
|
456
|
-
inputSchema: {
|
|
457
|
-
type: "object",
|
|
458
|
-
properties: {
|
|
459
|
-
credential_id: {
|
|
460
|
-
type: "string",
|
|
461
|
-
enum: Object.keys(CREDENTIAL_TARGETS),
|
|
462
|
-
description: "The credential the current task requires.",
|
|
463
|
-
},
|
|
464
|
-
reason: {
|
|
465
|
-
type: "string",
|
|
466
|
-
description: "Optional short, non-sensitive explanation of why the task needs it.",
|
|
467
|
-
},
|
|
468
|
-
},
|
|
469
|
-
required: ["credential_id"],
|
|
470
|
-
},
|
|
471
|
-
},
|
|
453
|
+
credentialTool,
|
|
472
454
|
{
|
|
473
455
|
name: "marketplace_search",
|
|
474
456
|
description: "Find an app or service the person wants connected in Atomic's Marketplace: official servers Atomic connects, and official apps through their Claude account. Then call open_marketplace. Never offer to build a connection or a community server.",
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { CREDENTIAL_TARGETS, isCredentialTargetId } from "../../shared/credential-request.js";
|
|
2
|
+
export async function requestCredential(args, BOT_ID, THREAD_ID, api) {
|
|
3
|
+
const credentialId = args.credential_id;
|
|
4
|
+
if (!isCredentialTargetId(credentialId)) {
|
|
5
|
+
return { text: "request_credential needs a supported credential_id.", isError: true };
|
|
6
|
+
}
|
|
7
|
+
const reason = typeof args.reason === "string" ? args.reason.trim().slice(0, 240) : "";
|
|
8
|
+
const r = await api("/api/internal/request-credential", {
|
|
9
|
+
method: "POST",
|
|
10
|
+
body: JSON.stringify({
|
|
11
|
+
fromBotId: BOT_ID,
|
|
12
|
+
fromThreadId: THREAD_ID,
|
|
13
|
+
credentialId,
|
|
14
|
+
...(credentialId === "connectionSecret" ? { connection: args.connection } : {}),
|
|
15
|
+
...(reason ? { reason } : {}),
|
|
16
|
+
}),
|
|
17
|
+
});
|
|
18
|
+
if (r.error)
|
|
19
|
+
return { text: String(r.error), isError: true };
|
|
20
|
+
if (r.alreadyConfigured) {
|
|
21
|
+
return { text: `${r.label ?? CREDENTIAL_TARGETS[credentialId].label} is already configured. Continue the task.` };
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
text: `A secure ${r.label ?? CREDENTIAL_TARGETS[credentialId].label} request is ready. The desktop app and a freshly QR-paired mobile app show its secure entry card; older mobile pairings explain how to pair again or finish on the computer. End this turn; Atomic will resume the task after the user saves or declines. Never ask them to paste the key into chat.`,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
export const credentialTool = {
|
|
28
|
+
name: "request_credential",
|
|
29
|
+
description: "Ask for a password, API key or token through Atomic's secure credential flow. Use connectionSecret with connection details for any HTTPS MCP service, or a built-in credential_id for a provider. The desktop app and a freshly QR-paired mobile app show a secure entry card; older mobile pairings show how to pair again or finish on the computer. Never claim a secure field opened unless this request succeeds, and never ask the user to paste a secret into chat. The secret is saved by the desktop app and is never returned to you. After calling this tool, end the turn; Atomic resumes the task after the user saves or declines.",
|
|
30
|
+
inputSchema: {
|
|
31
|
+
type: "object",
|
|
32
|
+
properties: {
|
|
33
|
+
credential_id: {
|
|
34
|
+
type: "string",
|
|
35
|
+
enum: Object.keys(CREDENTIAL_TARGETS),
|
|
36
|
+
description: "The credential the current task requires.",
|
|
37
|
+
},
|
|
38
|
+
connection: {
|
|
39
|
+
type: "object",
|
|
40
|
+
description: "Required for connectionSecret. The person reviews this HTTPS MCP destination before entering a password, API key or token. Never include the secret here.",
|
|
41
|
+
properties: {
|
|
42
|
+
name: { type: "string", description: "MCP server name." },
|
|
43
|
+
url: { type: "string", description: "Official HTTPS MCP URL, without credentials or query parameters." },
|
|
44
|
+
header: { type: "string", description: "Credential header name, for example x-api-key or Authorization." },
|
|
45
|
+
prefix: { type: "string", enum: ["Bearer "], description: "Only when the service requires a Bearer token." },
|
|
46
|
+
},
|
|
47
|
+
required: ["name", "url", "header"],
|
|
48
|
+
additionalProperties: false,
|
|
49
|
+
},
|
|
50
|
+
reason: {
|
|
51
|
+
type: "string",
|
|
52
|
+
description: "Optional short, non-sensitive explanation of why the task needs it.",
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
required: ["credential_id"],
|
|
56
|
+
},
|
|
57
|
+
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { engineOnPlatform } from "../../shared/engine-platforms.js";
|
|
1
2
|
import { AntigravityDriver } from "./antigravity.js";
|
|
2
3
|
import { BoxAgentDriver } from "./boxagent.js";
|
|
3
4
|
import { ClaudeDriver } from "./claude.js";
|
|
@@ -20,7 +21,8 @@ import { MistralDriver } from "./mistral.js";
|
|
|
20
21
|
import { MinimaxDriver } from "./minimax.js";
|
|
21
22
|
import { OllamaCloudDriver } from "./ollama-cloud.js";
|
|
22
23
|
import { AppleIntelligenceDriver } from "./apple-intelligence.js";
|
|
23
|
-
|
|
24
|
+
/** Every built-in driver, on every OS. Most code wants BUILT_IN_DRIVERS. */
|
|
25
|
+
export const ALL_BUILT_IN_DRIVERS = [
|
|
24
26
|
GrokDriver,
|
|
25
27
|
GrokAgentDriver,
|
|
26
28
|
GeminiAgentDriver,
|
|
@@ -44,3 +46,9 @@ export const BUILT_IN_DRIVERS = [
|
|
|
44
46
|
OllamaCloudDriver,
|
|
45
47
|
AppleIntelligenceDriver,
|
|
46
48
|
];
|
|
49
|
+
/** The drivers registered on `platform`: all of them minus any engine switched
|
|
50
|
+
* off there (shared/engine-platforms.ts, e.g. Qwen on Windows for now). */
|
|
51
|
+
export function builtInDriversFor(platform) {
|
|
52
|
+
return ALL_BUILT_IN_DRIVERS.filter((driver) => engineOnPlatform(driver.driverKind, platform));
|
|
53
|
+
}
|
|
54
|
+
export const BUILT_IN_DRIVERS = builtInDriversFor(process.platform);
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// MCP elicitation for Claude bots. Claude Code (2.1.270, stream-json input)
|
|
2
|
+
// forwards a configured server's `elicitation/create` to its host as
|
|
3
|
+
//
|
|
4
|
+
// {"type":"control_request","request_id":"…","request":{"subtype":"elicitation",
|
|
5
|
+
// "mcp_server_name":"addigy","message":"Delete policy X?","requested_schema":{…}}}
|
|
6
|
+
//
|
|
7
|
+
// and waits for a control_response on stdin. Addigy's hosted server confirms
|
|
8
|
+
// every write this way. A plain confirmation becomes a permission card that
|
|
9
|
+
// always needs the person (its tool is an elicitation tool: no approval
|
|
10
|
+
// mode, remembered grant or reviewer answers it). Anything else is answered
|
|
11
|
+
// `cancel` and never filled in (server/mcp-elicitation.ts).
|
|
12
|
+
import { newId } from "../contracts.js";
|
|
13
|
+
import { elicitationAnswer, elicitationConfirm, elicitationSummary, elicitationTool } from "../mcp-elicitation.js";
|
|
14
|
+
import { claudeControlResponse } from "./claude-stdin.js";
|
|
15
|
+
/** How long a confirmation card waits before it goes back as `cancel`. */
|
|
16
|
+
export const ELICITATION_TIMEOUT_MS = 15 * 60_000;
|
|
17
|
+
export function createClaudeElicitations(deps) {
|
|
18
|
+
const open = new Map();
|
|
19
|
+
return {
|
|
20
|
+
/** A control_request frame from the CLI's stdout. True when it was an
|
|
21
|
+
* elicitation (answered now or once the person decides). */
|
|
22
|
+
handle(frame, ctx) {
|
|
23
|
+
const o = frame;
|
|
24
|
+
if (!o || o.request?.subtype !== "elicitation")
|
|
25
|
+
return false;
|
|
26
|
+
if (typeof o.request_id !== "string")
|
|
27
|
+
return true;
|
|
28
|
+
const controlId = o.request_id;
|
|
29
|
+
const reply = (result) => ctx.write(claudeControlResponse(controlId, result));
|
|
30
|
+
const confirm = elicitationConfirm(o.request);
|
|
31
|
+
if (!confirm || ctx.closing) {
|
|
32
|
+
reply({ action: "cancel" });
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
const server = typeof o.request.mcp_server_name === "string" ? o.request.mcp_server_name : undefined;
|
|
36
|
+
const requestId = newId();
|
|
37
|
+
const finish = (allowed, source) => {
|
|
38
|
+
if (!open.delete(requestId))
|
|
39
|
+
return;
|
|
40
|
+
clearTimeout(timer);
|
|
41
|
+
reply(allowed === null ? { action: "cancel" } : elicitationAnswer(confirm, allowed));
|
|
42
|
+
deps.emit({ ...deps.base(ctx.threadId, ctx.turnId), type: "request.resolved", requestId, behavior: allowed ? "allow" : "deny", source });
|
|
43
|
+
};
|
|
44
|
+
const timer = setTimeout(() => finish(null, "timeout"), deps.timeoutMs ?? ELICITATION_TIMEOUT_MS);
|
|
45
|
+
timer.unref?.();
|
|
46
|
+
open.set(requestId, { threadId: ctx.threadId, finish });
|
|
47
|
+
deps.emit({
|
|
48
|
+
...deps.base(ctx.threadId, ctx.turnId),
|
|
49
|
+
type: "request.opened",
|
|
50
|
+
requestId,
|
|
51
|
+
requestType: "permission",
|
|
52
|
+
tool: elicitationTool(server),
|
|
53
|
+
summary: elicitationSummary(server, confirm),
|
|
54
|
+
requiresExplicitApproval: true,
|
|
55
|
+
});
|
|
56
|
+
return true;
|
|
57
|
+
},
|
|
58
|
+
/** The person's answer to a card, or null when the card is not one of these. */
|
|
59
|
+
respond(threadId, requestId, decision) {
|
|
60
|
+
const entry = open.get(requestId);
|
|
61
|
+
if (!entry)
|
|
62
|
+
return null;
|
|
63
|
+
if (entry.threadId !== threadId)
|
|
64
|
+
return "unavailable";
|
|
65
|
+
const allowed = decision.behavior === "allow";
|
|
66
|
+
entry.finish(allowed, "user");
|
|
67
|
+
return allowed ? "allowed-once" : "rejected";
|
|
68
|
+
},
|
|
69
|
+
/** The session ended: every open question goes back as "no answer". */
|
|
70
|
+
endThread(threadId) {
|
|
71
|
+
for (const entry of Array.from(open.values()))
|
|
72
|
+
if (entry.threadId === threadId)
|
|
73
|
+
entry.finish(null, "system");
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// The stream-json frames Atomic writes to the Claude CLI's stdin: a user
|
|
2
|
+
// turn, its diagnostic copy for native traces, and the answer to a control
|
|
3
|
+
// request the CLI forwarded (an MCP server's elicitation).
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
/** Claude's stream-json input accepts the same image source blocks as the
|
|
6
|
+
* Anthropic Messages API. Keep the old string form for text-only turns so a
|
|
7
|
+
* CLI update cannot disturb the overwhelmingly common path. */
|
|
8
|
+
export function claudeUserMessage(text, images) {
|
|
9
|
+
if (!images?.length)
|
|
10
|
+
return { type: "user", message: { role: "user", content: text } };
|
|
11
|
+
const content = images.map((image) => ({
|
|
12
|
+
type: "image",
|
|
13
|
+
source: {
|
|
14
|
+
type: "base64",
|
|
15
|
+
media_type: image.mime,
|
|
16
|
+
data: readFileSync(image.path).toString("base64"),
|
|
17
|
+
},
|
|
18
|
+
}));
|
|
19
|
+
if (text)
|
|
20
|
+
content.push({ type: "text", text });
|
|
21
|
+
return { type: "user", message: { role: "user", content } };
|
|
22
|
+
}
|
|
23
|
+
/** Native traces are routinely attached to bug reports. Preserve the image
|
|
24
|
+
* block's shape and size for debugging, but never persist its base64 bytes. */
|
|
25
|
+
export function diagnosticClaudeUserMessage(message) {
|
|
26
|
+
if (!Array.isArray(message.message.content))
|
|
27
|
+
return message;
|
|
28
|
+
return {
|
|
29
|
+
...message,
|
|
30
|
+
message: {
|
|
31
|
+
...message.message,
|
|
32
|
+
content: message.message.content.map((block) => block.type === "image"
|
|
33
|
+
? {
|
|
34
|
+
...block,
|
|
35
|
+
source: {
|
|
36
|
+
...block.source,
|
|
37
|
+
data: `[image data: ${block.source.data.length} base64 chars]`,
|
|
38
|
+
},
|
|
39
|
+
}
|
|
40
|
+
: block),
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
/** The reply to one control_request the CLI sent on stdout. */
|
|
45
|
+
export function claudeControlResponse(requestId, response) {
|
|
46
|
+
return { type: "control_response", response: { subtype: "success", request_id: requestId, response } };
|
|
47
|
+
}
|