@piagent/platform 1.46.2 → 1.47.0
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/CHANGELOG.md +40 -0
- package/docs/command-reference-vietnamese.md +2 -2
- package/docs/company-dev-commands.md +2 -1
- package/docs/distribution-standard.md +6 -6
- package/docs/mcp-and-tools.md +21 -1
- package/docs/operator-manual-vietnamese.md +7 -7
- package/docs/publishing-for-teams.md +3 -0
- package/docs/release-install-policy.md +2 -2
- package/docs/team-onboarding.md +2 -2
- package/docs-site/architecture.html +1 -1
- package/docs-site/assets/search-en.json +1 -1
- package/docs-site/assets/search-vi.json +1 -1
- package/docs-site/benchmark.html +1 -1
- package/docs-site/commands.html +1 -1
- package/docs-site/content/en/mcp.html +8 -0
- package/docs-site/content/mcp.html +8 -0
- package/docs-site/context-engine.html +1 -1
- package/docs-site/ecosystem.html +1 -1
- package/docs-site/en/architecture.html +1 -1
- package/docs-site/en/benchmark.html +1 -1
- package/docs-site/en/commands.html +1 -1
- package/docs-site/en/context-engine.html +1 -1
- package/docs-site/en/ecosystem.html +1 -1
- package/docs-site/en/index.html +15 -15
- package/docs-site/en/mcp-approval.html +1 -1
- package/docs-site/en/mcp-auth.html +1 -1
- package/docs-site/en/mcp-servers.html +1 -1
- package/docs-site/en/mcp.html +9 -1
- package/docs-site/en/permissions.html +1 -1
- package/docs-site/en/profiles.html +1 -1
- package/docs-site/en/quickstart.html +1 -1
- package/docs-site/en/runtime.html +1 -1
- package/docs-site/en/scope.html +1 -1
- package/docs-site/en/security.html +1 -1
- package/docs-site/en/team.html +1 -1
- package/docs-site/en/update.html +3 -3
- package/docs-site/en/whats-new.html +79 -47
- package/docs-site/en/windows.html +1 -1
- package/docs-site/en/workflows.html +1 -1
- package/docs-site/index.html +15 -15
- package/docs-site/mcp-approval.html +1 -1
- package/docs-site/mcp-auth.html +1 -1
- package/docs-site/mcp-servers.html +1 -1
- package/docs-site/mcp.html +9 -1
- package/docs-site/permissions.html +1 -1
- package/docs-site/profiles.html +1 -1
- package/docs-site/quickstart.html +1 -1
- package/docs-site/releases.json +98 -0
- package/docs-site/runtime.html +1 -1
- package/docs-site/scope.html +1 -1
- package/docs-site/security.html +1 -1
- package/docs-site/team.html +1 -1
- package/docs-site/update.html +3 -3
- package/docs-site/whats-new.html +79 -47
- package/docs-site/windows.html +1 -1
- package/docs-site/workflows.html +1 -1
- package/evals/private-holdout-v1/public-exposure.v2.json +2 -2
- package/package.json +1 -1
- package/packages/piagent-core/extensions/piagent-guard.ts +2 -146
- package/packages/piagent-core/managed/command-tools.mjs +17 -3
- package/packages/piagent-core/managed/copies.mjs +14 -4
- package/packages/piagent-core/managed/member-mcp.mjs +71 -0
- package/packages/piagent-core/managed/memory-lessons.mjs +1 -1
- package/packages/piagent-core/managed/resource-loader.mjs +19 -7
- package/packages/piagent-core/managed/sandbox-diagnostic.mjs +4 -0
- package/packages/piagent-core/managed/session.mjs +5 -5
- package/packages/piagent-core/managed/simulator-commands.mjs +117 -0
- package/packages/piagent-core/mcp/mcp-server-catalog.js +18 -1
- package/packages/piagent-core/package.json +1 -1
- package/packages/piagent-core/runtime/managed-failure.mjs +0 -1
- package/packages/piagent-core/runtime/mcp-repository-gate.d.mts +10 -0
- package/packages/piagent-core/runtime/mcp-repository-gate.mjs +150 -0
- package/scripts/claude-model-additions.mjs +8 -1
- package/scripts/configure-model-scope.sh +6 -1
- package/scripts/install-global.sh +10 -4
- package/scripts/personal-pi-config.mjs +5 -2
- package/scripts/pi-models-json.mjs +13 -0
- package/scripts/setup.sh +1 -1
|
@@ -4,6 +4,7 @@ import { engineAlive, engineLauncher } from './container-engine.mjs';
|
|
|
4
4
|
import { MEMBER_ONLY, memberOnlyReason, credentialReason, runHostCommand } from './host-command.mjs';
|
|
5
5
|
import { processTools } from './background-process.mjs';
|
|
6
6
|
import { checkpointsOf } from './checkpoints.mjs';
|
|
7
|
+
import { detachedEmulator, simulatorCommandReason } from './simulator-commands.mjs';
|
|
7
8
|
|
|
8
9
|
// Commands that leave the plain sandbox, each after the member approves that
|
|
9
10
|
// exact command: run_with_network (internet, a local server or browser),
|
|
@@ -40,6 +41,16 @@ const KINDS = {
|
|
|
40
41
|
// credentials: a prompt injection's "cat ~/.ssh/id_rsa" is never silent.
|
|
41
42
|
confirm: async (self, command) => asksNothing(self) ? credentialReason(command) : await networkConfirmation(self, command) ?? 'ask',
|
|
42
43
|
},
|
|
44
|
+
// The sandbox reaches neither CoreSimulator nor adb. Simulator and app build
|
|
45
|
+
// commands only (simulator-commands.mjs), asked about as network commands:
|
|
46
|
+
// Full access and Bypass run them, except what reaches credentials.
|
|
47
|
+
simulator: {
|
|
48
|
+
name: 'run_on_simulator', label: 'Run on simulator', scope: 'simulator-command-once', provider: 'host', riskClass: 'medium',
|
|
49
|
+
description: 'Run one iOS Simulator or Android emulator command on the member\'s machine, outside the sandbox. iOS: xcrun simctl (list, boot, bootstatus -b, install, launch, terminate, openurl, io <device> screenshot <file>, ui appearance, status_bar, spawn <device> log show --info --last 5m --predicate \'process == "<App>"\'), xcodebuild with a simulator destination (build, test: XCUITest is the reliable way to tap, type and answer system dialogs), xcodegen generate, axe (describe-ui reads the screen\'s elements; tap, type, swipe where this Xcode accepts them), open -a Simulator (some Xcode versions run simulators without that window; the device works without it). Android: adb (devices, install, shell am start, shell input tap/text/swipe, exec-out screencap -p > <file>, logcat -d), emulator -avd <name> (started in the background), avdmanager, ./gradlew installDebug. Also flutter, pod install, npx expo/react-native/cap, mkdir -p and cd inside the project. Parts may be joined by && or || and piped into grep, head, tail, xcbeautify or jq; files may be written only inside the project or to /dev/null. Read a screenshot you saved with read to see the screen. Other commands: run_on_host.',
|
|
50
|
+
allow: 'Run this exact simulator or app build command once on this machine, outside the sandbox: a build can run the project\'s build scripts as you.',
|
|
51
|
+
title: 'Chạy lệnh simulator', allowText: 'chạy đúng lệnh simulator/build app này một lần trên máy bạn, ngoài sandbox: build có thể chạy script build của project bằng quyền của bạn.',
|
|
52
|
+
confirm: async (self, command) => credentialReason(command) ?? await networkConfirmation(self, command),
|
|
53
|
+
},
|
|
43
54
|
};
|
|
44
55
|
|
|
45
56
|
// The engine answers, looked up again when it did not (started since).
|
|
@@ -66,6 +77,8 @@ function commandTool(self, kind) {
|
|
|
66
77
|
if (kind === 'docker' && !await engineReady(self.boundary)) throw Error(DOCKER_STOPPED);
|
|
67
78
|
const memberOnly = kind === 'host' ? memberOnlyReason(args.command) : null;
|
|
68
79
|
if (memberOnly) throw Error(MEMBER_ONLY(memberOnly));
|
|
80
|
+
const notSimulator = kind === 'simulator' ? simulatorCommandReason(args.command, self.cwd) : null;
|
|
81
|
+
if (notSimulator) throw Error(`managed-simulator-command: ${notSimulator}.`);
|
|
69
82
|
const confirm = await spec.confirm(self, args.command);
|
|
70
83
|
if (confirm !== null) await approve(self, id, signal, ctx, { toolName: spec.name, rawAction: { command: args.command }, commandPreview: String(args.command),
|
|
71
84
|
provider: spec.provider, requestedScope: spec.scope, reason: (confirm === 'ask' ? String(args.reason) : `Full access still asks: ${confirm}. ${args.reason}`).slice(0, 300), riskClass: spec.riskClass,
|
|
@@ -75,7 +88,8 @@ function commandTool(self, kind) {
|
|
|
75
88
|
if (self.run) await checkpointsOf(self).take();
|
|
76
89
|
const before = await self.changedFiles();
|
|
77
90
|
try {
|
|
78
|
-
const result = kind === '
|
|
91
|
+
const result = kind === 'simulator' && detachedEmulator(args.command) ? await runHostCommand({ command: `nohup ${args.command} >/dev/null 2>&1 & sleep 3; echo "Emulator started in the background; wait for it with: adb wait-for-device"`, cwd: self.cwd, userHome: self.boundary.userHome, timeout: 60, signal, onUpdate })
|
|
92
|
+
: kind === 'host' || kind === 'simulator' ? await runHostCommand({ command: args.command, cwd: self.cwd, userHome: self.boundary.userHome, timeout: args.timeout, signal, onUpdate })
|
|
79
93
|
: await self.boundary.invoke('bash', { command: args.command, ...(args.timeout ? { timeout: args.timeout } : {}) }, signal, onUpdate, ctx?.model, kind === 'docker' ? { docker: true } : { network: true });
|
|
80
94
|
ok = !result?.isError; return result;
|
|
81
95
|
} finally {
|
|
@@ -115,14 +129,14 @@ function startDockerTool(self, { wait = 180_000, every = 2000, launcher: found =
|
|
|
115
129
|
|
|
116
130
|
export function commandTools(self, options) {
|
|
117
131
|
const docker = self.boundary.engine || self.boundary.dockerCli;
|
|
118
|
-
return [commandTool(self, 'network'), ...(docker ? [commandTool(self, 'docker')] : []), startDockerTool(self, options), commandTool(self, 'host'), ...processTools(self)];
|
|
132
|
+
return [commandTool(self, 'network'), ...(docker ? [commandTool(self, 'docker')] : []), startDockerTool(self, options), commandTool(self, 'host'), commandTool(self, 'simulator'), ...processTools(self)];
|
|
119
133
|
}
|
|
120
134
|
|
|
121
135
|
// What the system prompt says about services and what the sandbox blocks: it
|
|
122
136
|
// is read for every request, so a Docker started since shows up.
|
|
123
137
|
const HOST_PROMPT = ' When the sandbox blocks something the task needs (a missing runtime such as a JDK or a Node version, the member\'s sign-in for git pull or git push or a private registry, a file outside the project), do not stop and do not tell the member to run it: request run_on_host with the exact command; it runs on the member\'s machine once they approve. Only what needs the member themselves (their password for sudo or an installer, an interactive sign-in, a window to click) goes to ask_user, with the exact steps and the options to continue or skip; after their answer, check and carry on.';
|
|
124
138
|
export function servicesPrompt(boundary) {
|
|
125
|
-
const simulator = ' The iOS Simulator
|
|
139
|
+
const simulator = ' The iOS Simulator and Android emulator are not reachable from bash: boot them, build and install the app, launch it, tap and type, and take screenshots with run_on_simulator (save the screenshot in the project, then read it to see the screen). Do not ask the member to run simulator steps you can run.';
|
|
126
140
|
if (boundary.engine) return ' Docker is available through run_with_docker (the member approves each command): a database or service for tests (docker compose up -d, then the tests through bash or run_with_network), docker build, or Testcontainers tests. If the engine stops answering, start_docker asks the member to start it. Stop what you started when done unless the member wants it kept.' + simulator + HOST_PROMPT;
|
|
127
141
|
if (boundary.dockerCli) return ' Docker is installed but not running: when the task needs it, call start_docker (the member approves), then run_with_docker.' + simulator + HOST_PROMPT;
|
|
128
142
|
return ' Docker is not installed on this machine: when the task needs it, ask the member with ask_user (install it, or check another way).' + simulator + HOST_PROMPT;
|
|
@@ -20,17 +20,27 @@ export function namedCount(words) {
|
|
|
20
20
|
}
|
|
21
21
|
// The copies a delegate call asks for, checked against the member's words;
|
|
22
22
|
// null for a plain call. `split`: one line per copy naming its share of the work.
|
|
23
|
+
// Copies the member did not ask for come back as `{ dropped }`: one helper runs
|
|
24
|
+
// and the main agent is told why. A refusal here sent agents round in retries,
|
|
25
|
+
// each quoting other words, until the member stopped the turn.
|
|
23
26
|
export function fanOut(manager, role, args, harness) {
|
|
24
27
|
const split = Array.isArray(args?.split) ? args.split.map(s => typeof s === 'string' ? s.trim().slice(0, 500) : '').filter(Boolean) : [];
|
|
25
28
|
const n = split.length || (Number.isInteger(args?.copies) ? args.copies : 1);
|
|
26
|
-
if (n <= 1
|
|
29
|
+
if (n <= 1) return null;
|
|
27
30
|
if (harness) throw Error('managed-copies-invalid: the harness runs one helper per call.');
|
|
28
|
-
if (n < 2 || n > MAX_COPIES) throw Error(`managed-copies-invalid: ask for 2 to ${MAX_COPIES} ${role} helpers (copies, or one split line per helper).`);
|
|
29
31
|
const words = normal(args?.member_words);
|
|
30
|
-
if (words.length < 3 || !memberRequest(memberMessages(manager)).some(text => normal(text).includes(words)) || namedCount(words) < n || !HELPER_WORDS.test(words))
|
|
31
|
-
|
|
32
|
+
if (words.length < 3 || !memberRequest(memberMessages(manager)).some(text => normal(text).includes(words)) || namedCount(words) < n || !HELPER_WORDS.test(words)) return { dropped: n };
|
|
33
|
+
if (n > MAX_COPIES) throw Error(`managed-copies-invalid: ask for 2 to ${MAX_COPIES} ${role} helpers (copies, or one split line per helper).`);
|
|
32
34
|
return { n, split: split.length ? split : null };
|
|
33
35
|
}
|
|
36
|
+
// The helper's answer, with what the main agent reads above it when it asked
|
|
37
|
+
// for helpers the member did not (`asked`: what fanOut returned).
|
|
38
|
+
export function withDroppedNote(result, role, asked) {
|
|
39
|
+
const n = asked?.dropped, first = result?.content?.[0];
|
|
40
|
+
if (!n || first?.type !== 'text') return result;
|
|
41
|
+
const note = `One ${role} helper ran, not ${n}: the member's own words did not ask for ${n} ${role} helpers. Go on with this answer; run several only when the member asks for that many.\n\n`;
|
|
42
|
+
return { ...result, content: [{ ...first, text: note + first.text }, ...result.content.slice(1)] };
|
|
43
|
+
}
|
|
34
44
|
// What one copy reads under its brief.
|
|
35
45
|
export function copyNote(role, i, fan) {
|
|
36
46
|
return `\n\nThe member asked for ${fan.n} ${role} helpers; you are ${role} helper ${i + 1} of ${fan.n}. ${fan.split ? `Your share of the work: ${fan.split[i]}. The others cover the rest.` : 'The others have the same brief: answer on your own judgement.'}`;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import { evaluateMcpApproval } from '../runtime/mcp-repository-gate.mjs';
|
|
5
|
+
import { userToolchains } from './toolchain.mjs';
|
|
6
|
+
import { fenced } from './untrusted.mjs';
|
|
7
|
+
|
|
8
|
+
// The member's MCP servers in company sessions. Company sessions load no Pi
|
|
9
|
+
// extension of the member's, with one exception: the MCP adapter Piagent
|
|
10
|
+
// installs (pi-mcp-adapter in Piagent's own Pi home). It reads the same
|
|
11
|
+
// configuration as personal Piagent (piagent-mcp add, ~/.config/mcp/mcp.json,
|
|
12
|
+
// a project's .mcp.json, /mcp) and the same logins (/mcp-auth), so a server the
|
|
13
|
+
// member added once works in both. The member decides which servers exist;
|
|
14
|
+
// company sessions keep the two protections personal Piagent has: a server a
|
|
15
|
+
// repository defines runs only once someone on this machine approved it, and
|
|
16
|
+
// what a server returns reaches the agent as data, not instructions.
|
|
17
|
+
|
|
18
|
+
const ADAPTER = 'pi-mcp-adapter';
|
|
19
|
+
const readJson = file => { try { return JSON.parse(fs.readFileSync(file, 'utf8')); } catch { return null; } };
|
|
20
|
+
const inside = (file, dir) => { const relative = path.relative(dir, file); return !!relative && !relative.startsWith('..') && !path.isAbsolute(relative); };
|
|
21
|
+
|
|
22
|
+
// The adapter the member installed in Piagent's Pi home and still lists among
|
|
23
|
+
// its packages, or null. `piagent studio` starts from a cleared environment, so
|
|
24
|
+
// the home is the default one (~/.piagent/agent) unless a caller names another.
|
|
25
|
+
export function memberMcpAdapter(home = os.homedir(), agentDir = path.join(home, '.piagent', 'agent')) {
|
|
26
|
+
const packages = readJson(path.join(agentDir, 'settings.json'))?.packages;
|
|
27
|
+
const listed = Array.isArray(packages) && packages.some(entry => new RegExp(`(^|[:/])${ADAPTER}(@|$)`).test(String(typeof entry === 'string' ? entry : entry?.source ?? '')));
|
|
28
|
+
const dir = path.join(agentDir, 'npm', 'node_modules', ADAPTER);
|
|
29
|
+
if (!listed || !fs.existsSync(path.join(dir, 'package.json'))) return null;
|
|
30
|
+
return { agentDir, dir: fs.realpathSync(dir) };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// What the adapter and the servers it starts need from a cleared environment:
|
|
34
|
+
// Piagent's Pi home (its mcp.json, logins and caches, shared with personal
|
|
35
|
+
// Piagent) and a PATH that finds npx, uvx, docker… as in the member's
|
|
36
|
+
// Terminal. Nothing else of the member's shell is passed, as for host commands.
|
|
37
|
+
export function memberMcpEnvironment(adapter, home = os.homedir(), env = process.env) {
|
|
38
|
+
env.PI_CODING_AGENT_DIR ||= adapter.agentDir;
|
|
39
|
+
const toolchains = userToolchains(home);
|
|
40
|
+
const dirs = [...toolchains.bins, path.dirname(process.execPath), '/opt/homebrew/bin', '/usr/local/bin', ...(env.PATH ?? '/usr/bin:/bin').split(':')];
|
|
41
|
+
env.PATH = [...new Set(dirs.filter(Boolean))].join(':');
|
|
42
|
+
for (const [name, value] of Object.entries(toolchains.env)) env[name] ||= value;
|
|
43
|
+
return env;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// The repository approval gate on every tool call (as personal Piagent's
|
|
47
|
+
// guard), and the adapter's results fenced as data.
|
|
48
|
+
export function memberMcpGuard(cwd, adapterDir) {
|
|
49
|
+
return pi => {
|
|
50
|
+
const fromAdapter = name => { const source = pi.getAllTools().find(tool => tool.name === name)?.sourceInfo?.path; return !!source && (source === adapterDir || inside(source, adapterDir)); };
|
|
51
|
+
pi.on('tool_call', event => {
|
|
52
|
+
const decision = evaluateMcpApproval(cwd, event.toolName, event.input ?? {});
|
|
53
|
+
return decision.block ? { block: true, reason: decision.reason } : undefined;
|
|
54
|
+
});
|
|
55
|
+
pi.on('tool_result', event => {
|
|
56
|
+
if (!fromAdapter(event.toolName)) return undefined;
|
|
57
|
+
const text = (event.content ?? []).filter(part => part?.type === 'text').map(part => part.text).join('\n');
|
|
58
|
+
const images = (event.content ?? []).filter(part => part?.type === 'image');
|
|
59
|
+
return { content: [{ type: 'text', text: fenced(`Result of MCP tool ${event.toolName}`, text) }, ...images] };
|
|
60
|
+
});
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// The company session's own extension factories, with the member's MCP added
|
|
65
|
+
// when the adapter is installed: [factories, extension paths] for inlineExtensions.
|
|
66
|
+
export function withMemberMcp(cwd, factories, home = os.homedir()) {
|
|
67
|
+
const adapter = memberMcpAdapter(home);
|
|
68
|
+
if (!adapter) return [factories, []];
|
|
69
|
+
memberMcpEnvironment(adapter, home);
|
|
70
|
+
return [[...factories, memberMcpGuard(cwd, adapter.dir)], [adapter.dir]];
|
|
71
|
+
}
|
|
@@ -8,7 +8,7 @@ import { createHash } from 'node:crypto';
|
|
|
8
8
|
import { redactedText } from '../runtime/managed-failure.mjs';
|
|
9
9
|
import { isCheckCommand } from './workflow.mjs';
|
|
10
10
|
|
|
11
|
-
const COMMAND_TOOLS = new Set(['bash','run_with_network','run_with_docker','run_on_host']);
|
|
11
|
+
const COMMAND_TOOLS = new Set(['bash','run_with_network','run_with_docker','run_on_host','run_on_simulator']);
|
|
12
12
|
const textOf = result => (result?.content ?? []).filter(p => p?.type === 'text').map(p => p.text ?? '').join('\n');
|
|
13
13
|
const clean = (value, max) => redactedText(String(value ?? '')).replace(/\bas_(?:live|run)_[A-Za-z0-9_-]{8,}/g,'[REDACTED_SECRET]').split(os.homedir()).join('~').replace(/\s+/g,' ').trim().slice(0,max);
|
|
14
14
|
const hash = s => createHash('sha256').update(s).digest('hex').slice(0,20);
|
|
@@ -26,10 +26,10 @@ export function projectInstructions(cwd, top = cwd) {
|
|
|
26
26
|
// discovered in a project. Skills and commands are text (agent-skills.mjs):
|
|
27
27
|
// what they ask for runs with the session's own tools.
|
|
28
28
|
// themes: Piagent's own (the company Terminal's /style), loaded by the host.
|
|
29
|
-
export function managedResourceLoader(api, { systemPrompt, extensions = [], runtime = api.createExtensionRuntime(), agentsFiles = [], skills = [], prompts = [], themes = [] } = {}) {
|
|
29
|
+
export function managedResourceLoader(api, { systemPrompt, extensions = [], runtime = api.createExtensionRuntime(), errors = [], agentsFiles = [], skills = [], prompts = [], themes = [] } = {}) {
|
|
30
30
|
const empty = Object.freeze([]);
|
|
31
31
|
return Object.freeze({
|
|
32
|
-
getExtensions: () => ({ extensions, errors:
|
|
32
|
+
getExtensions: () => ({ extensions, errors: [...errors], runtime }),
|
|
33
33
|
getSkills: () => ({ skills: [...skills], diagnostics: empty }),
|
|
34
34
|
getPrompts: () => ({ prompts: [...prompts], diagnostics: empty }),
|
|
35
35
|
getThemes: () => ({ themes: [...themes], diagnostics: empty }),
|
|
@@ -45,12 +45,24 @@ export function managedResourceLoader(api, { systemPrompt, extensions = [], runt
|
|
|
45
45
|
|
|
46
46
|
// Extensions the company Terminal adds itself (its commands and how Harness
|
|
47
47
|
// notes read), built the way Pi builds an inline extension. Never project code.
|
|
48
|
-
|
|
48
|
+
// `paths`: installed extensions loaded the way Pi loads a package's (only the
|
|
49
|
+
// member's MCP adapter, member-mcp.mjs); one that fails to load is reported in
|
|
50
|
+
// `errors` and the session opens without it.
|
|
51
|
+
export async function inlineExtensions(api, sdkRoot, cwd, factories = [], paths = []) {
|
|
49
52
|
const runtime = api.createExtensionRuntime();
|
|
50
|
-
if (!factories.length) return { extensions: [], runtime };
|
|
53
|
+
if (!factories.length && !paths.length) return { extensions: [], runtime };
|
|
51
54
|
const load = (file) => import(pathToFileURL(path.join(sdkRoot, 'dist', ...file.split('/'))).href);
|
|
52
|
-
const [{ loadExtensionFromFactory }, { createEventBus }] = await Promise.all([load('core/extensions/loader.js'), load('core/event-bus.js')]);
|
|
53
|
-
const bus = createEventBus(), extensions = [];
|
|
55
|
+
const [{ loadExtensionFromFactory, loadExtensions }, { createEventBus }] = await Promise.all([load('core/extensions/loader.js'), load('core/event-bus.js')]);
|
|
56
|
+
const bus = createEventBus(), extensions = [], errors = [];
|
|
54
57
|
for (const [index, factory] of factories.entries()) extensions.push(await loadExtensionFromFactory(factory, cwd, bus, runtime, `<inline:company-terminal-${index + 1}>`));
|
|
55
|
-
|
|
58
|
+
if (paths.length) {
|
|
59
|
+
// Pi's theme exists only once a terminal draws; the MCP adapter colours its
|
|
60
|
+
// status line with it in every mode, and without one (RPC, the WebUI) its
|
|
61
|
+
// start failed. The Terminal sets its own theme later.
|
|
62
|
+
const { theme, initTheme } = await load('modes/interactive/theme/theme.js');
|
|
63
|
+
try { void theme.fg; } catch { initTheme(); }
|
|
64
|
+
try { const loaded = await loadExtensions(paths, cwd, bus, runtime); extensions.push(...loaded.extensions); errors.push(...loaded.errors); }
|
|
65
|
+
catch (error) { errors.push({ path: paths.join(', '), error: error instanceof Error ? error.message : String(error) }); }
|
|
66
|
+
}
|
|
67
|
+
return { extensions, runtime, errors };
|
|
56
68
|
}
|
|
@@ -21,11 +21,15 @@ const DOCKER_DAEMON = /Cannot connect to the Docker daemon|connect to the docker
|
|
|
21
21
|
// A runtime or tool the command needs is not on this machine (or not the
|
|
22
22
|
// version the project asks for).
|
|
23
23
|
const TOOLCHAIN = /Unable to locate a Java Runtime|No Java runtime present|JAVA_HOME is not (?:set|defined correctly)|(?:^|\n)\S*?(?:ba|z)?sh: (?:line \d+: )?[\w.+-]+: command not found|zsh: command not found: [\w.+-]+|xcrun: error: (?:unable to find utility|invalid active developer path)|env: '?[\w.+-]+'?: No such file or directory|Unsupported class file major version|invalid target release: \d+|release version \d+ not supported|requires? (?:at least )?Java \d+|The engine "node" is incompatible|Unsupported engine/i;
|
|
24
|
+
// The iOS Simulator (CoreSimulator) and Android's adb/emulator, which the
|
|
25
|
+
// sandbox does not reach.
|
|
26
|
+
const SIMULATOR = /CoreSimulator|simctl|Unable to boot device|Unable to lookup in current state|Failed to start launchd_sim|adb: (?:cannot connect|failed to check server version|no devices|device offline)|cannot connect to daemon|\bemulator: (?:ERROR|PANIC)|Unable to find a destination matching|xcodebuild: error: Unable to find a destination/i;
|
|
24
27
|
// The member's sign-in, which the sandbox does not have.
|
|
25
28
|
const SIGN_IN = /could not read (?:Username|Password) for|terminal prompts disabled|Authentication failed for|Permission denied \(publickey|Host key verification failed|not logged into any GitHub hosts|please run:\s+gh auth login|Could not open a connection to your authentication agent|\bENEEDAUTH\b|\bE401\b|\b401 Unauthorized|requires authentication|denied: requested access to the resource is denied|pull access denied/i;
|
|
26
29
|
|
|
27
30
|
export function sandboxDiagnostic(message, { network = false, isolated = false, docker = false, engine = false } = {}) {
|
|
28
31
|
const text=String(message);
|
|
32
|
+
if(!network&&SIMULATOR.test(text))return 'managed-simulator-unreachable: Lệnh bash trong sandbox không tới được iOS Simulator hay Android emulator. Chạy lệnh simulator (xcrun simctl, xcodebuild cho simulator, adb, emulator, ./gradlew installDebug, flutter, npx expo) bằng run_on_simulator.\n'+text;
|
|
29
33
|
if(docker&&DOCKER_DAEMON.test(text))return 'managed-docker-unreachable: Lệnh chạy với Docker nhưng Docker engine không trả lời (Docker Desktop/Colima/OrbStack chưa chạy hoặc đang khởi động). Gọi start_docker để xin member duyệt mở Docker, rồi chạy lại đúng lệnh này bằng run_with_docker.\n'+text;
|
|
30
34
|
if(!docker&&DOCKER_DAEMON.test(text))return (engine?'managed-docker-blocked: Lệnh bash thường không dùng được Docker trong chế độ công ty. Lệnh docker, docker compose, hoặc test cần Docker (Testcontainers) chạy qua run_with_docker để người dùng duyệt đúng lệnh đó.\n':'managed-docker-unavailable: Docker engine trên máy chưa chạy. Gọi start_docker để xin member duyệt mở Docker, rồi chạy lệnh bằng run_with_docker.\n')+text;
|
|
31
35
|
if(TOOLCHAIN.test(text))return 'managed-toolchain-missing: Máy thiếu runtime/công cụ mà lệnh cần, hoặc sai version project yêu cầu. Đừng dừng ở đây: xem project cần gì (pom.xml, build.gradle, .nvmrc, .tool-versions, package.json engines…) rồi dùng run_on_host để xin member duyệt cài trên máy (ví dụ brew install openjdk@21, brew install node@22, sdk install java 21-tem), sau đó chạy lại lệnh. Nếu project build được trong Docker thì có thể đề xuất run_with_docker. Trình cài cần mật khẩu (sudo, file .pkg) thì hỏi member bằng ask_user.\n'+text;
|
|
@@ -24,7 +24,7 @@ import { readPatchDigest, readPatchSnapshot, readChangedFiles, readHead } from '
|
|
|
24
24
|
import { memberTurn } from './continuation.mjs';
|
|
25
25
|
import { workflowPolicy, repositoryChecks, sessionPlanTool, currentPlan, workflowPrompt, RunProcess, completionGate, pendingBaseline, pendingPaths, pendingHead, processEditTools } from './workflow.mjs';
|
|
26
26
|
import { workflowContextTool, workflowContextExtension } from './workflow-context.mjs';
|
|
27
|
-
import { beforeDelegate, resolveTool } from './objections.mjs'; import { fanOut } from './copies.mjs';
|
|
27
|
+
import { beforeDelegate, resolveTool } from './objections.mjs'; import { fanOut, withDroppedNote } from './copies.mjs'; import { withMemberMcp } from './member-mcp.mjs';
|
|
28
28
|
import { HELPER_CALLS, HELPER_ROLES, helperRoles, helperPrompt, webPrompt, delegateDescription, mainWebLookups, limitedWebTools } from './helper-roles.mjs'; import { delegateParameters, delegateArgs, SPEC_PROMPT } from './helper-settlement.mjs';
|
|
29
29
|
import { loadAgentResources } from './agent-skills.mjs';
|
|
30
30
|
import { loadPiagentThemes } from '../runtime/terminal/terminal-style.mjs';
|
|
@@ -137,8 +137,8 @@ export class ManagedSession {
|
|
|
137
137
|
self.checks = repositoryChecks(agentsFiles, self.cwd, self.boundary.repositoryTop);
|
|
138
138
|
// The Harness workflow (possibly changed on a later enrollment) adds its process to the prompt.
|
|
139
139
|
const loader = managedResourceLoader(api, { systemPrompt: () => BASE_PROMPT + referencePrompt(self.boundary.userHome) + TEST_PROMPT + servicesPrompt(self.boundary) + webPrompt(helperRoles(manifest), mainWebLookups(manifest)) + helperPrompt(helperRoles(self.manifest)) + workflowPrompt(workflowPolicy(self.manifest), self.checks), agentsFiles,
|
|
140
|
-
skills: self.resources.skills, prompts: self.resources.prompts, themes: terminalExtensions.length ? await loadPiagentThemes(sdk) : [], ...await inlineExtensions(api, sdk, self.cwd,
|
|
141
|
-
[...terminalExtensions, workflowContextExtension(self.cwd, () => self.session.sessionManager)]) });
|
|
140
|
+
skills: self.resources.skills, prompts: self.resources.prompts, themes: terminalExtensions.length ? await loadPiagentThemes(sdk) : [], ...await inlineExtensions(api, sdk, self.cwd, // and the member's MCP servers (member-mcp.mjs)
|
|
141
|
+
...withMemberMcp(self.cwd, [...terminalExtensions, workflowContextExtension(self.cwd, () => self.session.sessionManager)])) });
|
|
142
142
|
const settings = api.SettingsManager.inMemory({ retry: { enabled: false, provider: { maxRetries: 0 } }, cacheWarming: 'off', enableInstallTelemetry: false, enableAnalytics: false, enableSkillCommands: true });
|
|
143
143
|
const manager = sessionManager ?? api.SessionManager.inMemory(self.cwd); attachMemory(self, manager, agentDir, g => verifyGrant(g, self.manifest, 'main')); self.studioLink = new StudioLink({ origin: self.origin, agentDir, notify: event => self.notify(event), ui: () => self.session?.extensionRunner?.getUIContext?.() });
|
|
144
144
|
const scope = scopeOf(self.origin, manifest);
|
|
@@ -469,7 +469,7 @@ export class ManagedSession {
|
|
|
469
469
|
if (!enabled.includes(role)) throw Error(`managed-helper-not-configured: the company Harness has no ${role} subagent${enabled.length ? `; use ${enabled.join(', ')}` : ''}.`);
|
|
470
470
|
// Studio lets each helper role run HELPER_CALLS times per run (one user
|
|
471
471
|
// message), each helper the member asked for counted (copies.mjs). Say so at once.
|
|
472
|
-
const
|
|
472
|
+
const asked = fanOut(this.session?.sessionManager, role, more, harness), fan = asked?.dropped ? null : asked;
|
|
473
473
|
if ((this.helperCalls.get(role) ?? 0) + (fan?.n ?? 1) > HELPER_CALLS) throw Error(`managed-helper-limit: the ${role} subagent already ran ${HELPER_CALLS} times for this user message. Go on with what it returned, or use it again after the next user message.`);
|
|
474
474
|
beforeDelegate(this.run, role, harness, { since, review: this.review, changed: (this.ownPaths?.size ?? 0) > 0, asked: !!fan });
|
|
475
475
|
const job = this.runHelper(role, task, signal, harness, title, role === 'review' && typeof since === 'string' && since.trim() ? since.trim() : null, fan); this.helpers.set(role, job); this.publishHelpers();
|
|
@@ -477,7 +477,7 @@ export class ManagedSession {
|
|
|
477
477
|
// quiet: a beat each minute tells the dashboard's watchdog, which stops a
|
|
478
478
|
// turn after 15 minutes without an event, that the turn is working.
|
|
479
479
|
const beat = setInterval(() => this.notify({ type: 'managed_helper_progress', role }), this.helperBeatMs ?? HELPER_BEAT_MS); beat.unref?.();
|
|
480
|
-
try { return await job; } finally { clearInterval(beat); this.helpers.delete(role); this.publishHelpers(); }
|
|
480
|
+
try { return withDroppedNote(await job, role, asked); } finally { clearInterval(beat); this.helpers.delete(role); this.publishHelpers(); }
|
|
481
481
|
}
|
|
482
482
|
publishHelpers() {
|
|
483
483
|
// One of each enabled helper role can run at a time.
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
// What run_on_simulator runs on the member's machine: the iOS Simulator and
|
|
4
|
+
// Android emulator, and the builds that install an app on them (the sandbox
|
|
5
|
+
// reaches neither CoreSimulator nor adb). Only these programs, joined by &&,
|
|
6
|
+
// || or a pipe into an output filter; output may go to a file in the project
|
|
7
|
+
// (a screenshot), nothing else leaves it. Anything wider is run_on_host.
|
|
8
|
+
|
|
9
|
+
const XCRUN = new Set(['simctl', 'xcodebuild', 'devicectl', 'xcresulttool', 'xctrace']);
|
|
10
|
+
// `simctl spawn` starts a program of the simulator's runtime as the member:
|
|
11
|
+
// only `log` (the app's logs).
|
|
12
|
+
const simctl = words => words[1] !== 'simctl' || words[2] !== 'spawn' || words.slice(3).filter(word => !word.startsWith('-'))[1] === 'log';
|
|
13
|
+
const NPX = /^(expo|react-native|@react-native-community\/cli|cap|@capacitor\/cli)(@[\w.-]+)?$/;
|
|
14
|
+
// Program → whether its arguments are allowed (`words` without assignments,
|
|
15
|
+
// `inProject` a path check against the project folder).
|
|
16
|
+
const PROGRAMS = {
|
|
17
|
+
xcrun: words => XCRUN.has(words[1]) && simctl(words),
|
|
18
|
+
xcodebuild: () => true,
|
|
19
|
+
open: words => words.length === 3 && words[1] === '-a' && words[2] === 'Simulator',
|
|
20
|
+
adb: () => true, emulator: () => true, avdmanager: () => true, sdkmanager: () => true,
|
|
21
|
+
gradlew: (words, inProject) => inProject(words[0]),
|
|
22
|
+
// Taps, typing and the screen's accessibility tree; `axe init` writes into other tools' settings.
|
|
23
|
+
axe: words => !!words[1] && words[1] !== 'init',
|
|
24
|
+
xcodegen: words => words[1] === 'generate',
|
|
25
|
+
flutter: () => true,
|
|
26
|
+
pod: words => words[1] === 'install',
|
|
27
|
+
npx: words => NPX.test(words[1] === '--yes' || words[1] === '-y' ? words[2] ?? '' : words[1] ?? ''),
|
|
28
|
+
// Waiting for a boot, and reading what a build printed.
|
|
29
|
+
sleep: words => words.length === 2 && /^\d{1,3}$/.test(words[1]),
|
|
30
|
+
xcbeautify: () => true, xcpretty: () => true,
|
|
31
|
+
grep: () => true, head: () => true, tail: () => true, wc: () => true, sort: () => true, uniq: () => true, jq: () => true,
|
|
32
|
+
cd: (words, inProject) => words.length === 2 && inProject(words[1]),
|
|
33
|
+
mkdir: (words, inProject) => { const paths = words.slice(1).filter(word => word !== '-p'); return paths.length > 0 && paths.every(inProject); },
|
|
34
|
+
};
|
|
35
|
+
// Variables a command may set for itself; never what changes how programs load.
|
|
36
|
+
const ASSIGNMENT = /^([A-Z][A-Z0-9_]*)=/;
|
|
37
|
+
const UNSAFE_VARIABLE = /^(PATH|DYLD_\w*|LD_\w*|NODE_OPTIONS|BASH_ENV|ENV|ZDOTDIR|SHELL|HOME)$/;
|
|
38
|
+
|
|
39
|
+
// A path in the project folder: relative without climbing out of it, or
|
|
40
|
+
// absolute inside it (agents name files by their full path). Without the
|
|
41
|
+
// folder, only a relative one.
|
|
42
|
+
function insideProject(cwd) {
|
|
43
|
+
return target => {
|
|
44
|
+
if (typeof target !== 'string' || !target || target.startsWith('~') || target.includes('$') || target.split('/').includes('..')) return false;
|
|
45
|
+
if (!target.startsWith('/')) return true;
|
|
46
|
+
if (!cwd) return false;
|
|
47
|
+
const relative = path.relative(path.resolve(cwd), path.resolve(target));
|
|
48
|
+
return !relative.startsWith('..') && !path.isAbsolute(relative);
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Shell words with their quotes resolved, and the operators between them.
|
|
53
|
+
// null for what this reader does not follow (an unclosed quote).
|
|
54
|
+
function tokens(command) {
|
|
55
|
+
const out = []; let word = null, quote = null;
|
|
56
|
+
const push = () => { if (word !== null) { out.push({ word }); word = null; } };
|
|
57
|
+
for (let i = 0; i < command.length; i += 1) {
|
|
58
|
+
const c = command[i];
|
|
59
|
+
if (quote) { if (c === quote) quote = null; else if (c === '\\' && quote === '"' && i + 1 < command.length) word += command[++i]; else word += c; continue; }
|
|
60
|
+
if (c === "'" || c === '"') { quote = c; word ??= ''; continue; }
|
|
61
|
+
if (c === '\\' && i + 1 < command.length) { word = (word ?? '') + command[++i]; continue; }
|
|
62
|
+
if (/\s/.test(c)) { push(); continue; }
|
|
63
|
+
const two = command.slice(i, i + 2);
|
|
64
|
+
if (['&&', '||', '>>', '2>'].includes(two)) {
|
|
65
|
+
push();
|
|
66
|
+
if (two === '2>' && command[i + 2] === '&' && command[i + 3] === '1') { out.push({ op: '2>&1' }); i += 3; continue; }
|
|
67
|
+
out.push({ op: two }); i += 1; continue;
|
|
68
|
+
}
|
|
69
|
+
if (c === '|' || c === '>') { push(); out.push({ op: c }); continue; }
|
|
70
|
+
word = (word ?? '') + c;
|
|
71
|
+
}
|
|
72
|
+
if (quote) return null;
|
|
73
|
+
push();
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Why the command is not a simulator command, or null when it is. `cwd`:
|
|
78
|
+
// the project folder, where output files and `cd` may go.
|
|
79
|
+
export function simulatorCommandReason(command, cwd = null) {
|
|
80
|
+
const text = String(command ?? ''), inProject = insideProject(cwd);
|
|
81
|
+
if (!text.trim()) return 'the command is empty';
|
|
82
|
+
if (/[`;\n]|\$\(|<\(|>\(|</.test(text) || text.replaceAll('&&', '').replaceAll('2>&1', '').includes('&')) return 'only commands joined by &&, || or | are run (no ;, &, $( ), backticks or subshells)';
|
|
83
|
+
const list = tokens(text);
|
|
84
|
+
if (!list) return 'an unclosed quote';
|
|
85
|
+
let words = [];
|
|
86
|
+
const close = () => {
|
|
87
|
+
if (!words.length) return 'an empty part between operators';
|
|
88
|
+
while (words.length && ASSIGNMENT.test(words[0])) {
|
|
89
|
+
const name = ASSIGNMENT.exec(words[0])[1];
|
|
90
|
+
if (UNSAFE_VARIABLE.test(name)) return `${name} cannot be set here`;
|
|
91
|
+
words = words.slice(1);
|
|
92
|
+
}
|
|
93
|
+
const name = words[0]?.split('/').pop();
|
|
94
|
+
const allowed = Object.hasOwn(PROGRAMS, name) && PROGRAMS[name](words, inProject);
|
|
95
|
+
words = [];
|
|
96
|
+
return allowed ? null : `${name ?? 'this part'} is not a simulator or app build command (use run_on_host for other commands)`;
|
|
97
|
+
};
|
|
98
|
+
for (let i = 0; i < list.length; i += 1) {
|
|
99
|
+
const { word, op } = list[i];
|
|
100
|
+
if (word !== undefined) { words.push(word); continue; }
|
|
101
|
+
if (op === '2>&1') continue;
|
|
102
|
+
if (op === '>' || op === '>>' || op === '2>') {
|
|
103
|
+
const target = list[i + 1]?.word;
|
|
104
|
+
if (!(target === '/dev/null' || inProject(target))) return 'output may go only to a file inside the project or /dev/null';
|
|
105
|
+
i += 1; continue;
|
|
106
|
+
}
|
|
107
|
+
const reason = close(); if (reason) return reason;
|
|
108
|
+
}
|
|
109
|
+
return close();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// A lone `emulator …` keeps running while the virtual device is up: it is
|
|
113
|
+
// started detached and the tool returns once it is launched.
|
|
114
|
+
export function detachedEmulator(command) {
|
|
115
|
+
const list = tokens(String(command ?? ''));
|
|
116
|
+
return !!list && list.length > 0 && list.every(token => token.word !== undefined) && list[0].word.split('/').pop() === 'emulator';
|
|
117
|
+
}
|
|
@@ -151,6 +151,18 @@ export const CATALOG_SERVERS = {
|
|
|
151
151
|
directTools: false
|
|
152
152
|
}
|
|
153
153
|
},
|
|
154
|
+
// Atlassian's own remote server (Jira and Confluence Cloud). It registers its
|
|
155
|
+
// OAuth client itself (the metadata sits at the origin, not at the protected
|
|
156
|
+
// resource), so the member signs in with /mcp-auth and their own Jira access.
|
|
157
|
+
atlassian: {
|
|
158
|
+
description: "Atlassian Rovo MCP server: Jira and Confluence Cloud, with the member's own Atlassian login (OAuth).",
|
|
159
|
+
config: {
|
|
160
|
+
url: "https://mcp.atlassian.com/v1/mcp",
|
|
161
|
+
auth: "oauth",
|
|
162
|
+
lifecycle: "lazy",
|
|
163
|
+
directTools: false
|
|
164
|
+
}
|
|
165
|
+
},
|
|
154
166
|
// Converts .docx, .xlsx, .pptx and .pdf to Markdown locally. Local matters:
|
|
155
167
|
// the alternative to this is uploading a document somewhere to read it, and a
|
|
156
168
|
// document worth converting is usually one worth not uploading.
|
|
@@ -202,6 +214,10 @@ export const CATALOG_PREREQUISITES = {
|
|
|
202
214
|
"Developer Preview: no Pi OAuth end-to-end release proof exists yet",
|
|
203
215
|
"Google's supported client guides use an OAuth client id and secret; Pi's public-client/PKCE compatibility is unverified"
|
|
204
216
|
],
|
|
217
|
+
atlassian: [
|
|
218
|
+
"a Jira or Confluence Cloud site (Server and Data Center are not served by this endpoint)",
|
|
219
|
+
"sign in once with /mcp-auth atlassian; a site admin may have to allow the Atlassian MCP connection"
|
|
220
|
+
],
|
|
205
221
|
markitdown: [
|
|
206
222
|
"uv on PATH (https://docs.astral.sh/uv); the first run downloads a Python runtime and roughly 120 MB of dependencies",
|
|
207
223
|
"note: markitdown-mcp is published as an alpha, so pin changes deliberately"
|
|
@@ -244,11 +260,12 @@ export const CATALOG_PRESETS = {
|
|
|
244
260
|
"google-mail": ["gmail"],
|
|
245
261
|
google: ["google-drive", "google-docs", "google-sheets"],
|
|
246
262
|
"google-all": ["google-drive", "gmail", "google-docs", "google-sheets"],
|
|
263
|
+
atlassian: ["atlassian"],
|
|
247
264
|
core: ["context7", "chrome-devtools", "github"],
|
|
248
265
|
popular: ["context7", "chrome-devtools", "playwright", "github", "figma-desktop"],
|
|
249
266
|
all: [
|
|
250
267
|
"context7", "chrome-devtools", "playwright", "github", "figma-desktop", "figma",
|
|
251
|
-
"markitdown", "google-drive", "gmail", "google-docs", "google-sheets"
|
|
268
|
+
"markitdown", "atlassian", "google-drive", "gmail", "google-docs", "google-sheets"
|
|
252
269
|
]
|
|
253
270
|
};
|
|
254
271
|
|
|
@@ -96,7 +96,6 @@ const CODES = {
|
|
|
96
96
|
'managed-review-by-harness': ['tool', 'the harness reviews the final patch; the agent does not ask for one'],
|
|
97
97
|
'managed-review-once': ['tool', 'this change was reviewed already; ask again only after fixing blocking findings'],
|
|
98
98
|
'managed-copies-invalid': ['tool', 'several helpers of a role run 2 or 3 at a time, at the member\'s request'],
|
|
99
|
-
'managed-copies-not-asked': ['tool', 'more than one helper of a role runs only when the member asks for it'],
|
|
100
99
|
helper_run_limit_reached: ['tool', 'this subagent already ran 8 times for this message'],
|
|
101
100
|
'managed-helper-cancelled': ['tool', 'the subagent was stopped'],
|
|
102
101
|
'managed-helper-failed': ['failed', 'the subagent ended without an answer'],
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface RepositoryMcpGate {
|
|
2
|
+
blocked: Map<string, { state: string; origin: string }>;
|
|
3
|
+
serverNames: Set<string>;
|
|
4
|
+
unverifiable: string[];
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export const mcpApprovalCache: Map<string, { signature: string; gate: RepositoryMcpGate }>;
|
|
8
|
+
export function mcpServerFromToolCall(toolName: string, input: Record<string, unknown>, candidates: Iterable<string>): string | undefined;
|
|
9
|
+
export function repositoryMcpGate(cwd: string): RepositoryMcpGate;
|
|
10
|
+
export function evaluateMcpApproval(cwd: string, toolName: string, input: Record<string, unknown>): { block: boolean; reason?: string };
|