agy-mcp-bridge 0.1.22 → 0.1.24
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/README.md +13 -1
- package/config/workspaces.example.json +34 -1
- package/dist/src/agents/antigravity.js +8 -5
- package/dist/src/agents/codex.js +12 -5
- package/dist/src/agents/quota.js +82 -0
- package/dist/src/agents/registry.js +84 -2
- package/dist/src/antigravity/process.js +1 -1
- package/dist/src/config.js +32 -0
- package/dist/src/http.js +25 -0
- package/dist/src/runtime.js +2 -2
- package/dist/src/tasks/manager.js +97 -9
- package/dist/src/tools/register.js +15 -0
- package/dist/src/xerj/client.js +154 -0
- package/docs/AGENT-PROFILES-PLAN.md +146 -0
- package/docs/SMART-ROUTER-PLAN.md +112 -0
- package/docs/XERJ-INTEGRATION-PLAN.md +136 -0
- package/package.json +1 -1
- package/skills/antigravity-bridge/SKILL.md +11 -1
- package/skills/known-versions.json +8 -0
- package/skills/mission-orchestrator/SKILL.md +2 -0
package/README.md
CHANGED
|
@@ -60,7 +60,7 @@ Then call `antigravity_environment` from a fresh session — that is the proof;
|
|
|
60
60
|
|
|
61
61
|
| Field | Meaning |
|
|
62
62
|
|---|---|
|
|
63
|
-
| `agent` | id from `agents_list` (`antigravity`, `codex`). Omit for the highest-priority available agent; naming an unavailable agent errors rather than switching silently. |
|
|
63
|
+
| `agent` | id from `agents_list` (`antigravity`, `codex`), or a profile / pool id from [Agents, profiles and pools](#agents-profiles-and-pools). Omit for the highest-priority available agent; naming an unavailable agent errors rather than switching silently. |
|
|
64
64
|
| `isolation` | `none` (default — edits the workspace, globally serial) or `worktree` (branch `bridge/<task_id>` under `.bridge-state/worktrees/<task_id>`, parallel up to `max_parallel_worktrees`). Worktree tasks must declare `owns`, and the repo needs at least one commit. Uncommitted changes in the main checkout are **not** copied in. |
|
|
65
65
|
| `owns` | relative paths the agent may touch. After the run the bridge diffs the worktree against this list and reports anything else in `scope.out_of_scope`. It never auto-merges, never deletes the branch, and does not run repo hooks. |
|
|
66
66
|
| `autonomy` | `suggest` \| `auto` \| `full` — an instruction to the CLI agent, not a permission grant. Destructive actions stay hard-denied (see below). |
|
|
@@ -74,11 +74,23 @@ Then call `antigravity_environment` from a fresh session — that is the proof;
|
|
|
74
74
|
| `antigravity_task_wait` | blocks up to `wait_seconds` (default cap 55 s), returns early when the task ends |
|
|
75
75
|
| `antigravity_task_output` | final response, stdout/stderr, errors, `denied_actions`, truncation |
|
|
76
76
|
| `antigravity_cancel_task` | kills the task's process tree; does not revert file changes |
|
|
77
|
+
| `code_search` | XERJ passage/definition lookup; registered always but **refused while `xerj.enabled` is off** |
|
|
77
78
|
|
|
78
79
|
`completed` means the CLI exited with a parseable result — always `review_required: true`, `acceptance_verified: false`. A 0 exit code can still be reported as `failed` after a permission denial or a timeout. While a task runs, `wait`/`status` return a short `activity` line instead of making the client read logs.
|
|
79
80
|
|
|
80
81
|
Two switches control delegation itself: `agy-mcp-bridge delegation off` blocks new tasks immediately without a restart, and `BRIDGE_DELEGATION=off` pins it at startup. Tasks still refuse to be delegated by policy the skill describes (security, auth, secrets, data deletion, deploy, CI and public API changes are not delegated).
|
|
81
82
|
|
|
83
|
+
## Agents, profiles and pools
|
|
84
|
+
|
|
85
|
+
`agents.profiles.<id>` declares named agents that inherit an adapter and override its defaults (`env` such as `CODEX_HOME`, `model`, `executable`, `codex_sandbox`, `codex_ask_for_approval`), so one machine can run two logged-in accounts side by side. A profile id is a valid `agent` value and `agents_list` reports each one's availability and warnings. `priority` and `enabled` still order the adapters themselves.
|
|
86
|
+
|
|
87
|
+
`agents.pools.<id>` groups profiles for quota handling. Delegating to a pool id picks the first healthy member. When a member stops with something that reads like an exhausted quota or rate limit, the bridge marks it cooling for `quota_cooldown_seconds` and re-runs **the same task** on the next member — each member at most once per task, after which the task fails instead of looping. The replaced attempt is kept on the record as `attempts[]` (agent, error, exit, log tails) so a reviewer can still see why the first CLI stopped, and the retry keeps holding the execution lock rather than releasing and re-taking it, so no second ordinary task slips into the gap.
|
|
88
|
+
|
|
89
|
+
Two knobs are opt-in because they widen what the bridge can reach. Neither is reachable from a task, and neither stays invisible: the codex pair is reported in `agents_list` warnings, and the XERJ pair in `/api/xerj/status` plus the refusal `code_search` gives while it is off.
|
|
90
|
+
|
|
91
|
+
- `codex_sandbox: "danger-full-access"` / `codex_ask_for_approval: "never"` — readable from the bridge's own config file only. No task, tool argument or MCP client can ask for them, and `--ask-for-approval` is passed where codex accepts it (before `exec`).
|
|
92
|
+
- `xerj.enabled` / `xerj.auto_reference` — both default to `false`. Turning them on sends task text to another local service and can place a retrieved passage inside the prompt an agent follows, which is a decision for the machine's owner, not for a delegating client.
|
|
93
|
+
|
|
82
94
|
## Missions
|
|
83
95
|
|
|
84
96
|
A mission is a plan the bridge **records and gates but never runs**: up to 12 steps in one workspace, each carrying its own `delegate` arguments and a list of `depends_on`. An MCP client writes the plan and dispatches it one step at a time; the bridge refuses to start a step whose dependencies did not all complete, so no agent is sent at a step whose inputs do not exist yet. Nothing ever starts by itself — a restart restores every plan and re-dispatches nothing.
|
|
@@ -12,7 +12,40 @@
|
|
|
12
12
|
"default_autonomy": "auto",
|
|
13
13
|
"agents": {
|
|
14
14
|
"priority": ["antigravity", "codex"],
|
|
15
|
-
"enabled": ["antigravity", "codex"]
|
|
15
|
+
"enabled": ["antigravity", "codex"],
|
|
16
|
+
"codex_sandbox": "workspace-write",
|
|
17
|
+
"codex_ask_for_approval": "on-request",
|
|
18
|
+
"profiles": {
|
|
19
|
+
"codex-work": {
|
|
20
|
+
"adapter": "codex",
|
|
21
|
+
"label": "Codex (Work Account)",
|
|
22
|
+
"env": {
|
|
23
|
+
"CODEX_HOME": "C:/Users/name/.codex-work"
|
|
24
|
+
}
|
|
25
|
+
},
|
|
26
|
+
"codex-backup": {
|
|
27
|
+
"adapter": "codex",
|
|
28
|
+
"label": "Codex (Backup Account)",
|
|
29
|
+
"env": {
|
|
30
|
+
"CODEX_HOME": "C:/Users/name/.codex-backup"
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
},
|
|
34
|
+
"pools": {
|
|
35
|
+
"codex-smart-pool": {
|
|
36
|
+
"strategy": "quota-adaptive",
|
|
37
|
+
"members": ["codex-work", "codex-backup"],
|
|
38
|
+
"hard_task_profile": "codex-work",
|
|
39
|
+
"quota_cooldown_seconds": 900
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
"xerj": {
|
|
44
|
+
"enabled": false,
|
|
45
|
+
"url": "http://127.0.0.1:9200",
|
|
46
|
+
"default_index": "mcp-bridge-docs",
|
|
47
|
+
"auto_reference": false,
|
|
48
|
+
"max_passages": 3
|
|
16
49
|
},
|
|
17
50
|
"tree_watch": {
|
|
18
51
|
"enabled": true,
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { finalEnvelope, invocationFor } from '../antigravity/invocation.js';
|
|
2
2
|
import { extractStats } from '../usage.js';
|
|
3
|
+
import { isQuotaError } from './quota.js';
|
|
3
4
|
const CAPABILITIES = ['code', 'review', 'test', 'research', 'long-run'];
|
|
4
|
-
export function antigravityAdapter(executable, defaultTransport) {
|
|
5
|
+
export function antigravityAdapter(executable, defaultTransport, options = {}) {
|
|
5
6
|
return {
|
|
6
|
-
id: 'antigravity',
|
|
7
|
-
label: 'Antigravity',
|
|
7
|
+
id: options.id ?? 'antigravity',
|
|
8
|
+
label: options.label ?? 'Antigravity',
|
|
8
9
|
capabilities: CAPABILITIES,
|
|
9
10
|
buildInvocation(task) {
|
|
10
11
|
const transport = task.promptTransport ?? defaultTransport;
|
|
11
12
|
const invocation = invocationFor(task.prompt, task.timeoutSeconds, transport, task.model ?? null, task.extraArgs ?? []);
|
|
12
|
-
return { executable, args: invocation.args, stdin: invocation.stdin, transport };
|
|
13
|
+
return { executable, args: invocation.args, stdin: invocation.stdin, transport, customEnv: options.env };
|
|
13
14
|
},
|
|
14
15
|
async parseResult(raw) {
|
|
15
16
|
const transport = raw.transport ?? defaultTransport;
|
|
@@ -48,7 +49,9 @@ export function antigravityAdapter(executable, defaultTransport) {
|
|
|
48
49
|
}
|
|
49
50
|
// A stream result that is not the expected envelope is reported by field name, because that is what tells the owner to turn step progress off.
|
|
50
51
|
const fields = transport === 'argv' ? '' : ` (stream result fields: ${Object.keys(envelope).slice(0, 12).join(', ') || 'none'}; if step progress is on, turn it off in Settings to return to the JSON mode)`;
|
|
51
|
-
|
|
52
|
+
const finalErr = typeof envelope.error === 'string' ? envelope.error : `CLI did not provide a complete successful response${fields}`;
|
|
53
|
+
const quotaHit = isQuotaError(raw.stderr) || isQuotaError(finalErr) || isQuotaError(raw.stdout);
|
|
54
|
+
return { ...result, error: finalErr, isQuotaExceeded: quotaHit };
|
|
52
55
|
}
|
|
53
56
|
};
|
|
54
57
|
}
|
package/dist/src/agents/codex.js
CHANGED
|
@@ -1,24 +1,29 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
+
import { isQuotaError } from './quota.js';
|
|
3
4
|
import { startProcess, terminateOwned } from '../antigravity/process.js';
|
|
4
5
|
const CAPABILITIES = ['code', 'review', 'test', 'research', 'long-run'];
|
|
5
6
|
const REQUIRED_FLAGS = ['--json', '--sandbox', '--cd', '--output-last-message', '--ephemeral', '--skip-git-repo-check'];
|
|
6
7
|
const MAX_RESULT_BYTES = 1024 * 1024;
|
|
7
8
|
export function codexAdapter(command, options = {}) {
|
|
8
9
|
return {
|
|
9
|
-
id: 'codex',
|
|
10
|
-
label: 'Codex',
|
|
10
|
+
id: options.id ?? 'codex',
|
|
11
|
+
label: options.label ?? 'Codex',
|
|
11
12
|
capabilities: CAPABILITIES,
|
|
12
13
|
buildInvocation(task) {
|
|
13
14
|
const resultFile = path.join(task.stateDirectory, 'results', `${task.taskId}.txt`);
|
|
14
15
|
const args = [
|
|
15
|
-
'exec', '--json', '--sandbox', 'workspace-write', '--skip-git-repo-check', '--ephemeral',
|
|
16
|
+
'exec', '--json', '--sandbox', options.sandbox ?? 'workspace-write', '--skip-git-repo-check', '--ephemeral',
|
|
16
17
|
'--cd', task.workspace, '--color', 'never', '-o', resultFile
|
|
17
18
|
];
|
|
19
|
+
// A global codex flag, so it goes before the `exec` subcommand: `codex exec --ask-for-approval never` exits
|
|
20
|
+
// with "unexpected argument" on codex-cli 0.163.0 and the task never starts.
|
|
21
|
+
if (options.askForApproval)
|
|
22
|
+
args.unshift('--ask-for-approval', options.askForApproval);
|
|
18
23
|
if (options.model)
|
|
19
24
|
args.push('-m', options.model);
|
|
20
25
|
args.push('-');
|
|
21
|
-
return { executable: command.executable, args, stdin: `${task.prompt}\n`, resultFile };
|
|
26
|
+
return { executable: command.executable, args, stdin: `${task.prompt}\n`, resultFile, customEnv: options.env };
|
|
22
27
|
},
|
|
23
28
|
async parseResult(raw) {
|
|
24
29
|
const events = [];
|
|
@@ -71,7 +76,9 @@ export function codexAdapter(command, options = {}) {
|
|
|
71
76
|
if (raw.signal)
|
|
72
77
|
return { ...result, error: `Codex was terminated (${raw.signal})` };
|
|
73
78
|
const stderrTail = raw.stderr.trim().split(/\r?\n/).filter(Boolean).at(-1);
|
|
74
|
-
|
|
79
|
+
const finalErr = errors.at(-1) ?? stderrTail?.slice(0, 500) ?? 'Codex did not provide a final message';
|
|
80
|
+
const quotaHit = isQuotaError(raw.stderr) || isQuotaError(finalErr) || isQuotaError(raw.stdout);
|
|
81
|
+
return { ...result, error: finalErr, isQuotaExceeded: quotaHit };
|
|
75
82
|
}
|
|
76
83
|
};
|
|
77
84
|
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export const QUOTA_ERROR_PATTERNS = [
|
|
2
|
+
/insufficient_quota/i,
|
|
3
|
+
/rate[_\s-]?limit(?:ed|_exceeded)?/i,
|
|
4
|
+
/quota[_\s-]?exceeded/i,
|
|
5
|
+
/credits?[_\s-]?exhausted/i,
|
|
6
|
+
/check your plan and billing/i,
|
|
7
|
+
/billing details/i,
|
|
8
|
+
/too many requests/i,
|
|
9
|
+
/credit limit/i,
|
|
10
|
+
/model[_\s-]?capacity/i,
|
|
11
|
+
/\boverloaded\b/i,
|
|
12
|
+
/exceeded your current quota/i,
|
|
13
|
+
/resource_exhausted/i,
|
|
14
|
+
/\b429\b/
|
|
15
|
+
];
|
|
16
|
+
/** Check if stdout/stderr/error text indicates an account quota exhaustion or rate limit. */
|
|
17
|
+
export function isQuotaError(text) {
|
|
18
|
+
if (!text)
|
|
19
|
+
return false;
|
|
20
|
+
return QUOTA_ERROR_PATTERNS.some(pattern => pattern.test(text));
|
|
21
|
+
}
|
|
22
|
+
const HARD_KEYWORDS = /(?:refactor|architecture|concurrency|tái cấu trúc|bảo mật|tối ưu|redesign|deadlock|race condition|migration|distributed)/i;
|
|
23
|
+
/** Assess whether a task is considered 'hard' (high complexity, multi-file, or architectural). */
|
|
24
|
+
export function isHardTask(input) {
|
|
25
|
+
if ((input.owns?.length ?? 0) >= 3)
|
|
26
|
+
return true;
|
|
27
|
+
if ((input.acceptance_criteria?.length ?? 0) >= 4)
|
|
28
|
+
return true;
|
|
29
|
+
const fullText = `${input.task ?? ''} ${input.context ?? ''}`;
|
|
30
|
+
if (fullText.length >= 3000)
|
|
31
|
+
return true;
|
|
32
|
+
if (HARD_KEYWORDS.test(fullText))
|
|
33
|
+
return true;
|
|
34
|
+
if ((input.timeout_seconds ?? 0) >= 600)
|
|
35
|
+
return true;
|
|
36
|
+
if (input.effort === 'high')
|
|
37
|
+
return true;
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
export class ProfileHealthTracker {
|
|
41
|
+
cooldowns = new Map();
|
|
42
|
+
/** Mark a profile as exhausted / in cooldown until now + durationSeconds. */
|
|
43
|
+
markCooldown(profileId, durationSeconds = 900) {
|
|
44
|
+
const expiresAt = Date.now() + durationSeconds * 1000;
|
|
45
|
+
this.cooldowns.set(profileId, expiresAt);
|
|
46
|
+
}
|
|
47
|
+
/** Check if a profile is currently available (not cooled down). */
|
|
48
|
+
isAvailable(profileId) {
|
|
49
|
+
const expiresAt = this.cooldowns.get(profileId);
|
|
50
|
+
if (!expiresAt)
|
|
51
|
+
return true;
|
|
52
|
+
if (Date.now() >= expiresAt) {
|
|
53
|
+
this.cooldowns.delete(profileId);
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
/** Clear cooldown for testing or explicit manual recovery. */
|
|
59
|
+
clearCooldown(profileId) {
|
|
60
|
+
this.cooldowns.delete(profileId);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Intelligently select the best profile from a pool based on task difficulty and health status.
|
|
64
|
+
* Never returns a member listed in `exclude` (already tried this task) or currently in cooldown.
|
|
65
|
+
* Returns null when nothing healthy remains — callers must fail the task rather than loop.
|
|
66
|
+
*/
|
|
67
|
+
selectProfile(pool, isHard, isAgentAvailable, exclude = new Set()) {
|
|
68
|
+
const usableMembers = pool.members.filter(id => isAgentAvailable(id) && !exclude.has(id));
|
|
69
|
+
if (!usableMembers.length)
|
|
70
|
+
return null;
|
|
71
|
+
const healthyMembers = usableMembers.filter(id => this.isAvailable(id));
|
|
72
|
+
if (!healthyMembers.length)
|
|
73
|
+
return null;
|
|
74
|
+
if (isHard && pool.hard_task_profile && healthyMembers.includes(pool.hard_task_profile)) {
|
|
75
|
+
return pool.hard_task_profile;
|
|
76
|
+
}
|
|
77
|
+
// 'quota-adaptive', 'priority-failover', and the current round-robin stub: first healthy in pool order
|
|
78
|
+
return healthyMembers[0];
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
export const globalHealthTracker = new ProfileHealthTracker();
|
|
82
|
+
//# sourceMappingURL=quota.js.map
|
|
@@ -9,8 +9,9 @@ export async function createAgentRegistry(config, antigravity) {
|
|
|
9
9
|
const enabled = settings.enabled ?? DEFAULT_ENABLED;
|
|
10
10
|
const priority = settings.priority ?? DEFAULT_PRIORITY;
|
|
11
11
|
const warnings = [];
|
|
12
|
+
const known = new Set([...KNOWN, ...Object.keys(settings.profiles ?? {}), ...Object.keys(settings.pools ?? {})]);
|
|
12
13
|
for (const id of [...enabled, ...priority]) {
|
|
13
|
-
if (!
|
|
14
|
+
if (!known.has(id))
|
|
14
15
|
warnings.push(`Unknown agent "${id}" in the bridge config is ignored`);
|
|
15
16
|
}
|
|
16
17
|
const agents = [];
|
|
@@ -40,7 +41,88 @@ export async function createAgentRegistry(config, antigravity) {
|
|
|
40
41
|
codexStatus = { ...codexStatus, warnings: [`Codex CLI not found: ${error instanceof Error ? error.message : 'unknown error'}`] };
|
|
41
42
|
}
|
|
42
43
|
}
|
|
43
|
-
|
|
44
|
+
if (settings.codex_sandbox === 'danger-full-access' || settings.codex_ask_for_approval === 'never') {
|
|
45
|
+
const dangerWarn = `Agent "codex" is configured with dangerous permissions (sandbox: ${settings.codex_sandbox ?? 'workspace-write'}, approval: ${settings.codex_ask_for_approval ?? 'on-request'})`;
|
|
46
|
+
codexStatus.warnings.push(dangerWarn);
|
|
47
|
+
warnings.push(dangerWarn);
|
|
48
|
+
}
|
|
49
|
+
agents.push(codexAgent(codexCommand, codexStatus, {
|
|
50
|
+
model: settings.codex_model,
|
|
51
|
+
sandbox: settings.codex_sandbox,
|
|
52
|
+
askForApproval: settings.codex_ask_for_approval
|
|
53
|
+
}));
|
|
54
|
+
if (settings.profiles) {
|
|
55
|
+
for (const [profileId, profile] of Object.entries(settings.profiles)) {
|
|
56
|
+
const isProfileEnabled = enabled.includes(profileId) || (!settings.enabled && true);
|
|
57
|
+
const adapterType = profile.adapter;
|
|
58
|
+
const label = profile.label ?? (adapterType === 'codex' ? `Codex (${profileId})` : `Antigravity (${profileId})`);
|
|
59
|
+
const profileWarnings = [];
|
|
60
|
+
if (!isProfileEnabled)
|
|
61
|
+
profileWarnings.push('Disabled in the bridge config');
|
|
62
|
+
if (adapterType === 'antigravity') {
|
|
63
|
+
let cmd = antigravity.executable;
|
|
64
|
+
let prefixArgs = antigravity.prefixArgs;
|
|
65
|
+
let supported = antigravity.supported;
|
|
66
|
+
let version = antigravity.version;
|
|
67
|
+
if (profile.executable) {
|
|
68
|
+
try {
|
|
69
|
+
const resolved = await resolveCommand(profile.executable);
|
|
70
|
+
cmd = resolved.executable;
|
|
71
|
+
prefixArgs = resolved.prefixArgs;
|
|
72
|
+
}
|
|
73
|
+
catch (error) {
|
|
74
|
+
supported = false;
|
|
75
|
+
profileWarnings.push(`Executable not found: ${error instanceof Error ? error.message : 'unknown error'}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const status = {
|
|
79
|
+
id: profileId, label, available: isProfileEnabled && supported,
|
|
80
|
+
version: version || null, executable: cmd || null,
|
|
81
|
+
capabilities: ['code', 'review', 'test', 'research', 'long-run'],
|
|
82
|
+
warnings: isProfileEnabled ? [...antigravity.warnings, ...profileWarnings] : profileWarnings
|
|
83
|
+
};
|
|
84
|
+
agents.push({
|
|
85
|
+
adapter: antigravityAdapter(cmd, config.prompt_transport, { id: profileId, label, env: profile.env }),
|
|
86
|
+
status, enabled: isProfileEnabled, prefixArgs
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
else if (adapterType === 'codex') {
|
|
90
|
+
let cmd = codexCommand;
|
|
91
|
+
let statusAvailable = codexStatus.available;
|
|
92
|
+
let version = codexStatus.version;
|
|
93
|
+
if (profile.executable) {
|
|
94
|
+
try {
|
|
95
|
+
cmd = await resolveCommand(profile.executable);
|
|
96
|
+
const detected = await detectCodex(cmd, { cwd: process.cwd(), extraEnvPassthrough: config.extra_env_passthrough });
|
|
97
|
+
statusAvailable = detected.available;
|
|
98
|
+
version = detected.version;
|
|
99
|
+
profileWarnings.push(...detected.warnings);
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
statusAvailable = false;
|
|
103
|
+
profileWarnings.push(`Codex CLI not found: ${error instanceof Error ? error.message : 'unknown error'}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const sandbox = profile.codex_sandbox ?? settings.codex_sandbox;
|
|
107
|
+
const askForApproval = profile.codex_ask_for_approval ?? settings.codex_ask_for_approval;
|
|
108
|
+
if (sandbox === 'danger-full-access' || askForApproval === 'never') {
|
|
109
|
+
const dangerMsg = `Agent "${profileId}" is configured with dangerous permissions (sandbox: ${sandbox ?? 'workspace-write'}, approval: ${askForApproval ?? 'on-request'})`;
|
|
110
|
+
profileWarnings.push(dangerMsg);
|
|
111
|
+
warnings.push(dangerMsg);
|
|
112
|
+
}
|
|
113
|
+
const status = {
|
|
114
|
+
id: profileId, label, available: isProfileEnabled && statusAvailable,
|
|
115
|
+
version: version || null, executable: cmd.source || null,
|
|
116
|
+
capabilities: ['code', 'review', 'test', 'research', 'long-run'],
|
|
117
|
+
warnings: isProfileEnabled ? [...codexStatus.warnings.filter(w => !w.includes('Disabled') && !w.includes('dangerous permissions')), ...profileWarnings] : profileWarnings
|
|
118
|
+
};
|
|
119
|
+
agents.push(codexAgent(cmd, status, {
|
|
120
|
+
id: profileId, label, model: profile.model ?? settings.codex_model,
|
|
121
|
+
sandbox, askForApproval, env: profile.env
|
|
122
|
+
}));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
44
126
|
const usable = (agent) => agent.enabled && agent.status.available;
|
|
45
127
|
const defaultAgent = priority.find(id => agents.some(agent => agent.adapter.id === id && usable(agent)))
|
|
46
128
|
?? agents.find(usable)?.adapter.id ?? null;
|
|
@@ -71,7 +71,7 @@ export function childEnvironment(parent, extra = []) {
|
|
|
71
71
|
(explicit.has(key.toUpperCase()) || (base.test(key) && !/TOKEN|SECRET|PASSWORD|API_KEY/i.test(key)))));
|
|
72
72
|
}
|
|
73
73
|
export function startProcess(executable, args, cwd, options = {}) {
|
|
74
|
-
const env = childEnvironment(process.env, options.extraEnvPassthrough);
|
|
74
|
+
const env = { ...childEnvironment(process.env, options.extraEnvPassthrough), ...(options.customEnv ?? {}) };
|
|
75
75
|
const child = spawn(executable, args, {
|
|
76
76
|
cwd, env, shell: false, windowsHide: true, detached: process.platform !== 'win32',
|
|
77
77
|
stdio: ['pipe', 'pipe', 'pipe']
|
package/dist/src/config.js
CHANGED
|
@@ -2,7 +2,38 @@ import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { paths, packageRoot } from './paths.js';
|
|
4
4
|
import * as z from 'zod/v4';
|
|
5
|
+
export const codexSandboxSchema = z.enum(['workspace-write', 'danger-full-access', 'read-only']);
|
|
6
|
+
// The two policies codex-cli 0.163.0 accepts for --ask-for-approval; anything else makes codex exit before it reads the prompt.
|
|
7
|
+
export const codexApprovalSchema = z.enum(['on-request', 'never']);
|
|
8
|
+
export const agentProfileSchema = z.object({
|
|
9
|
+
adapter: z.enum(['codex', 'antigravity']),
|
|
10
|
+
label: z.string().min(1).max(100).optional(),
|
|
11
|
+
env: z.record(z.string().regex(/^[A-Za-z_][A-Za-z0-9_]*$/), z.string()).optional(),
|
|
12
|
+
codex_sandbox: codexSandboxSchema.optional(),
|
|
13
|
+
codex_ask_for_approval: codexApprovalSchema.optional(),
|
|
14
|
+
model: z.string().min(1).max(200).optional(),
|
|
15
|
+
executable: z.string().min(1).max(4096).optional()
|
|
16
|
+
}).strict();
|
|
17
|
+
export const poolSchema = z.object({
|
|
18
|
+
strategy: z.enum(['quota-adaptive', 'priority-failover', 'round-robin']).default('quota-adaptive'),
|
|
19
|
+
members: z.array(z.string().min(1).max(64)).min(1).max(50),
|
|
20
|
+
hard_task_profile: z.string().min(1).max(64).optional(),
|
|
21
|
+
quota_cooldown_seconds: z.number().int().min(10).max(86400).default(900)
|
|
22
|
+
}).strict();
|
|
23
|
+
export const xerjSchema = z.object({
|
|
24
|
+
// Off unless the owner turns it on: enabling it sends task text to another local service and prepends retrieved
|
|
25
|
+
// passages to the prompt an agent follows, so a search hit becomes an instruction the agent never got from the client.
|
|
26
|
+
enabled: z.boolean().default(false),
|
|
27
|
+
url: z.string().default('http://127.0.0.1:9200'),
|
|
28
|
+
default_index: z.string().default('mcp-bridge-docs'),
|
|
29
|
+
auto_reference: z.boolean().default(false),
|
|
30
|
+
max_passages: z.number().int().min(1).max(10).default(3)
|
|
31
|
+
}).strict();
|
|
5
32
|
const agentsSchema = z.object({
|
|
33
|
+
codex_sandbox: codexSandboxSchema.optional(),
|
|
34
|
+
codex_ask_for_approval: codexApprovalSchema.optional(),
|
|
35
|
+
profiles: z.record(z.string().min(1).max(64), agentProfileSchema).optional(),
|
|
36
|
+
pools: z.record(z.string().min(1).max(64), poolSchema).optional(),
|
|
6
37
|
priority: z.array(z.string().min(1).max(64)).max(50).optional(),
|
|
7
38
|
enabled: z.array(z.string().min(1).max(64)).max(50).optional(),
|
|
8
39
|
codex_executable: z.string().min(1).max(4096).optional(),
|
|
@@ -19,6 +50,7 @@ const schema = z.object({
|
|
|
19
50
|
max_parallel_worktrees: z.number().int().min(1).max(8).optional(),
|
|
20
51
|
default_autonomy: z.enum(['suggest', 'auto', 'full']).optional(),
|
|
21
52
|
agents: agentsSchema.optional(),
|
|
53
|
+
xerj: xerjSchema.optional(),
|
|
22
54
|
/**
|
|
23
55
|
* The process-tree watcher that runs while a task is live. One poll costs a full process-table query (about half a
|
|
24
56
|
* second on the reference machine), so the cadence is owner-tunable and the whole watch can be turned off; without
|
package/dist/src/http.js
CHANGED
|
@@ -52,6 +52,31 @@ export async function startHttpBridge(options) {
|
|
|
52
52
|
reply(200, { status: 'ok', transport: 'streamable-http', mcp_endpoint: `${baseUrl}/mcp`, ...options.manager.overview(), missions: options.missions.overview() });
|
|
53
53
|
return;
|
|
54
54
|
}
|
|
55
|
+
if (req.url === '/api/xerj/status' && req.method === 'GET') {
|
|
56
|
+
const xerj = options.manager.getXerjClient();
|
|
57
|
+
if (!options.manager.isXerjEnabled()) {
|
|
58
|
+
reply(200, { enabled: false, available: false, url: xerj.url, default_index: xerj.defaultIndex, indices: [] });
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
void xerj.isAvailable().then(async (available) => {
|
|
62
|
+
const indices = available ? await xerj.listIndices() : [];
|
|
63
|
+
reply(200, { available, url: xerj.url, default_index: xerj.defaultIndex, indices });
|
|
64
|
+
}).catch(() => reply(200, { available: false, url: xerj.url, default_index: xerj.defaultIndex, indices: [] }));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (req.url?.startsWith('/api/xerj/search') && req.method === 'GET') {
|
|
68
|
+
if (!options.manager.isXerjEnabled()) {
|
|
69
|
+
reply(404, { error: 'XERJ search is disabled: set "xerj.enabled" to true in workspaces.json' });
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const parsed = new URL(req.url, baseUrl || 'http://127.0.0.1:3000');
|
|
73
|
+
const q = parsed.searchParams.get('q') ?? '';
|
|
74
|
+
const limit = parseInt(parsed.searchParams.get('limit') ?? '5', 10);
|
|
75
|
+
const index = parsed.searchParams.get('index') ?? undefined;
|
|
76
|
+
const xerj = options.manager.getXerjClient();
|
|
77
|
+
void xerj.search(q, { limit, index }).then(result => reply(200, result)).catch(() => reply(500, { error: 'Search failed' }));
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
55
80
|
if (req.url !== '/mcp') {
|
|
56
81
|
reply(404, { error: 'Use /mcp for MCP or GET /health for health checks' });
|
|
57
82
|
return;
|
package/dist/src/runtime.js
CHANGED
|
@@ -81,7 +81,7 @@ export async function createRuntime() {
|
|
|
81
81
|
limits: { max_timeout_seconds: config.max_timeout_seconds, max_output_bytes_per_stream: config.max_output_bytes, max_retained_tasks: config.max_retained_tasks, max_mission_steps: MAX_STEPS_PER_MISSION, max_retained_missions: MAX_RETAINED_MISSIONS, concurrent_tasks: maxParallelWorktrees + 1, concurrent_normal_tasks: 1, max_parallel_worktrees: maxParallelWorktrees, max_queued_tasks: config.max_queued_tasks },
|
|
82
82
|
limitations: [
|
|
83
83
|
`Antigravity requires agy 1.4.x with all required flags; real execution was verified on ${VERIFIED_AGY_VERSIONS.join(' and ')}.`,
|
|
84
|
-
'Codex
|
|
84
|
+
'Codex defaults to sandbox workspace-write; the bridge owner may raise sandbox/approval in config (danger-full-access / ask-for-approval never), which is surfaced as agent warnings.',
|
|
85
85
|
'Ordinary tasks are serialized globally. Only tasks with isolation "worktree" run concurrently, each in its own git worktree and branch.',
|
|
86
86
|
'Mission plans are recorded and gated by the bridge, but the bridge never dispatches a step by itself: every step starts on a client call, and a canceled or completed plan grants nothing.',
|
|
87
87
|
'Worktree tasks declare owns; files outside that list are reported for review and are never merged automatically.',
|
|
@@ -92,7 +92,7 @@ export async function createRuntime() {
|
|
|
92
92
|
'Workspace preflight is not an OS sandbox and cannot prevent concurrent link swaps.',
|
|
93
93
|
'Existing agent permissions, hooks, MCP and credential access remain governed by each CLI configuration.',
|
|
94
94
|
'Antigravity permission flags are off unless the bridge owner raises the level in the web UI (a client cannot change it). At the default level, actions that need approval are denied (see denied_actions).',
|
|
95
|
-
'
|
|
95
|
+
'Antigravity has no client-side permission bypass. Codex sandbox/approval are owner-config only; dangerous settings emit warnings and still require the owner to opt in via workspaces.json.',
|
|
96
96
|
`After an agent process exits, the bridge releases the process trees it left behind (Windows: taskkill on every subtree still hanging below the agent or below a process recorded while it ran, after re-checking that each pid still names the same process; POSIX: the exited process group). The outcome is published as the task's cleanup field, and a graceful shutdown waits for it. A bridge killed abruptly, or tree_watch disabled in config, can still orphan processes.`,
|
|
97
97
|
'Worktree directories of finished tasks are kept for review. Only empty leftovers are removed at startup, a directory a live process still holds is reported instead of forced, and a repository left holding a record whose directory is gone is pruned.',
|
|
98
98
|
'Redaction recognizes known patterns only. Never include credentials in task data.'
|