@rex_koh/subagent-budget-guard 0.1.0 → 0.1.2
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/.claude-plugin/plugin.json +2 -18
- package/README.md +1 -1
- package/bin/verify.js +11 -2
- package/lib/guard.js +0 -20
- package/lib/verifier.js +56 -19
- package/package.json +1 -1
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "subagent-budget-guard",
|
|
3
3
|
"displayName": "Subagent Budget Guard",
|
|
4
4
|
"description": "Hard-deny subagent launches, record verified subagent usage, and enforce a session budget against Claude Code's 5-hour rate-limit percentage.",
|
|
5
|
-
"version": "0.1.
|
|
5
|
+
"version": "0.1.2",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "ClaudeSubAgentSuppressor"
|
|
8
8
|
},
|
|
@@ -15,14 +15,6 @@
|
|
|
15
15
|
"hooks"
|
|
16
16
|
],
|
|
17
17
|
"userConfig": {
|
|
18
|
-
"max_subagents_per_session": {
|
|
19
|
-
"type": "number",
|
|
20
|
-
"title": "Max subagents per session",
|
|
21
|
-
"description": "Maximum normal Agent tool subagents allowed in one Claude Code session. The default 0 blocks all new subagents.",
|
|
22
|
-
"default": 0,
|
|
23
|
-
"min": 0,
|
|
24
|
-
"required": true
|
|
25
|
-
},
|
|
26
18
|
"max_concurrent_subagents": {
|
|
27
19
|
"type": "number",
|
|
28
20
|
"title": "Max concurrent subagents",
|
|
@@ -31,14 +23,6 @@
|
|
|
31
23
|
"min": 0,
|
|
32
24
|
"required": true
|
|
33
25
|
},
|
|
34
|
-
"max_agent_team_tasks_per_session": {
|
|
35
|
-
"type": "number",
|
|
36
|
-
"title": "Max agent-team tasks per session",
|
|
37
|
-
"description": "Maximum agent-team tasks that may be created in one session. The default 0 suppresses agent-team task creation.",
|
|
38
|
-
"default": 0,
|
|
39
|
-
"min": 0,
|
|
40
|
-
"required": true
|
|
41
|
-
},
|
|
42
26
|
"max_subagent_tokens_per_session": {
|
|
43
27
|
"type": "number",
|
|
44
28
|
"title": "Max verified subagent tokens per session",
|
|
@@ -68,7 +52,7 @@
|
|
|
68
52
|
"enforcement_enabled": {
|
|
69
53
|
"type": "boolean",
|
|
70
54
|
"title": "Enable enforcement",
|
|
71
|
-
"description": "When true, hooks deny over-budget subagents
|
|
55
|
+
"description": "When true, hooks deny over-budget subagents and prompts. When false, the plugin records usage without blocking.",
|
|
72
56
|
"default": true,
|
|
73
57
|
"required": true
|
|
74
58
|
}
|
package/README.md
CHANGED
|
@@ -43,4 +43,4 @@ Offline verification:
|
|
|
43
43
|
node bin/verify.js --offline
|
|
44
44
|
```
|
|
45
45
|
|
|
46
|
-
The plugin is strict by default: `
|
|
46
|
+
The plugin is strict by default: `max_concurrent_subagents` defaults to `0`, so normal subagent launches are blocked unless raised.
|
package/bin/verify.js
CHANGED
|
@@ -4,6 +4,9 @@ import {
|
|
|
4
4
|
runLiveVerification,
|
|
5
5
|
runOfflineVerification
|
|
6
6
|
} from '../lib/verifier.js';
|
|
7
|
+
import { pathExists } from '../lib/guard.js';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import path from 'node:path';
|
|
7
10
|
|
|
8
11
|
function modeFromArgs() {
|
|
9
12
|
if (process.argv.includes('--live')) return 'live';
|
|
@@ -12,10 +15,16 @@ function modeFromArgs() {
|
|
|
12
15
|
|
|
13
16
|
async function main() {
|
|
14
17
|
const mode = modeFromArgs();
|
|
18
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
19
|
+
const cwd = process.cwd();
|
|
20
|
+
const cwdLooksLikeMarketplace =
|
|
21
|
+
(await pathExists(path.join(cwd, '.claude-plugin', 'marketplace.json'))) ||
|
|
22
|
+
(await pathExists(path.join(cwd, 'plugins', 'subagent-budget-guard', '.claude-plugin', 'plugin.json')));
|
|
23
|
+
const repoRoot = cwdLooksLikeMarketplace ? cwd : packageRoot;
|
|
15
24
|
const result =
|
|
16
25
|
mode === 'live'
|
|
17
|
-
? await runLiveVerification({ repoRoot
|
|
18
|
-
: await runOfflineVerification({ repoRoot
|
|
26
|
+
? await runLiveVerification({ repoRoot, env: process.env })
|
|
27
|
+
: await runOfflineVerification({ repoRoot, env: process.env });
|
|
19
28
|
|
|
20
29
|
process.stdout.write(`${formatVerificationResult(result)}\n`);
|
|
21
30
|
process.exitCode = result.ok ? 0 : 1;
|
package/lib/guard.js
CHANGED
|
@@ -16,9 +16,7 @@ import path from 'node:path';
|
|
|
16
16
|
export const PLUGIN_NAME = 'subagent-budget-guard';
|
|
17
17
|
|
|
18
18
|
export const DEFAULT_CONFIG = Object.freeze({
|
|
19
|
-
max_subagents_per_session: 0,
|
|
20
19
|
max_concurrent_subagents: 0,
|
|
21
|
-
max_agent_team_tasks_per_session: 0,
|
|
22
20
|
max_subagent_tokens_per_session: 0,
|
|
23
21
|
session_five_hour_budget_percent: 25,
|
|
24
22
|
absolute_five_hour_ceiling_percent: 95,
|
|
@@ -306,14 +304,6 @@ function agentDenyReason(state, config) {
|
|
|
306
304
|
const budgetReason = fiveHourBudgetDecision(state, config);
|
|
307
305
|
if (budgetReason) return budgetReason;
|
|
308
306
|
|
|
309
|
-
if (config.max_subagents_per_session === 0) {
|
|
310
|
-
return 'Subagent launch denied: max_subagents_per_session is 0.';
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
if (state.subagents.allowed >= config.max_subagents_per_session) {
|
|
314
|
-
return `Subagent launch denied: max_subagents_per_session ${config.max_subagents_per_session} already reached.`;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
307
|
if (config.max_concurrent_subagents === 0) {
|
|
318
308
|
return 'Subagent launch denied: max_concurrent_subagents is 0.';
|
|
319
309
|
}
|
|
@@ -474,14 +464,6 @@ function taskDenyReason(state, config) {
|
|
|
474
464
|
const budgetReason = fiveHourBudgetDecision(state, config);
|
|
475
465
|
if (budgetReason) return budgetReason;
|
|
476
466
|
|
|
477
|
-
if (config.max_agent_team_tasks_per_session === 0) {
|
|
478
|
-
return 'Agent-team task denied: max_agent_team_tasks_per_session is 0.';
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
if (state.agentTeam.created >= config.max_agent_team_tasks_per_session) {
|
|
482
|
-
return `Agent-team task denied: max_agent_team_tasks_per_session ${config.max_agent_team_tasks_per_session} already reached.`;
|
|
483
|
-
}
|
|
484
|
-
|
|
485
467
|
return null;
|
|
486
468
|
}
|
|
487
469
|
|
|
@@ -614,9 +596,7 @@ export async function buildReport(sessionId, env = process.env) {
|
|
|
614
596
|
state,
|
|
615
597
|
summary: {
|
|
616
598
|
verifiedTokenLabel: `${state.subagents.verifiedTokens.toLocaleString('en-US')} verified tokens`,
|
|
617
|
-
subagentLaunches: `${state.subagents.allowed}/${config.max_subagents_per_session}`,
|
|
618
599
|
activeSubagents: `${state.subagents.active}/${config.max_concurrent_subagents}`,
|
|
619
|
-
agentTeamTasks: `${state.agentTeam.created}/${config.max_agent_team_tasks_per_session}`,
|
|
620
600
|
fiveHourBudget:
|
|
621
601
|
consumed === null
|
|
622
602
|
? '5-hour usage unavailable'
|
package/lib/verifier.js
CHANGED
|
@@ -20,8 +20,31 @@ async function readJson(filePath) {
|
|
|
20
20
|
return JSON.parse(text.replace(/^\uFEFF/, ''));
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
function
|
|
24
|
-
|
|
23
|
+
async function resolveLayout(repoRoot) {
|
|
24
|
+
const packagePluginRoot = repoRoot;
|
|
25
|
+
const marketplacePluginRoot = path.join(repoRoot, 'plugins', 'subagent-budget-guard');
|
|
26
|
+
|
|
27
|
+
if (await pathExists(path.join(repoRoot, '.claude-plugin', 'marketplace.json'))) {
|
|
28
|
+
return {
|
|
29
|
+
repoRoot,
|
|
30
|
+
pluginRoot: marketplacePluginRoot,
|
|
31
|
+
hasMarketplace: true
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (await pathExists(path.join(packagePluginRoot, '.claude-plugin', 'plugin.json'))) {
|
|
36
|
+
return {
|
|
37
|
+
repoRoot,
|
|
38
|
+
pluginRoot: packagePluginRoot,
|
|
39
|
+
hasMarketplace: false
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
repoRoot,
|
|
45
|
+
pluginRoot: marketplacePluginRoot,
|
|
46
|
+
hasMarketplace: false
|
|
47
|
+
};
|
|
25
48
|
}
|
|
26
49
|
|
|
27
50
|
async function withCheck(result, name, fn) {
|
|
@@ -48,23 +71,36 @@ export async function runOfflineVerification({
|
|
|
48
71
|
checks: [],
|
|
49
72
|
failures: []
|
|
50
73
|
};
|
|
51
|
-
const
|
|
74
|
+
const layout = await resolveLayout(repoRoot);
|
|
75
|
+
const root = layout.pluginRoot;
|
|
52
76
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
77
|
+
if (layout.hasMarketplace) {
|
|
78
|
+
await withCheck(result, 'marketplace-manifest', async () => {
|
|
79
|
+
const marketplacePath = path.join(repoRoot, '.claude-plugin', 'marketplace.json');
|
|
80
|
+
const marketplace = await readJson(marketplacePath);
|
|
81
|
+
assert(marketplace.name === 'subagent-budget-tools', 'marketplace name mismatch');
|
|
82
|
+
assert(Array.isArray(marketplace.plugins), 'marketplace.plugins must be an array');
|
|
83
|
+
const entry = marketplace.plugins.find((plugin) => plugin.name === 'subagent-budget-guard');
|
|
84
|
+
assert(entry, 'subagent-budget-guard entry missing');
|
|
85
|
+
assert(entry.source?.source === 'npm', 'marketplace source must use npm');
|
|
86
|
+
assert(
|
|
87
|
+
entry.source?.package === '@rex_koh/subagent-budget-guard',
|
|
88
|
+
'marketplace npm package mismatch'
|
|
89
|
+
);
|
|
90
|
+
assert(entry.source?.version === '0.1.2', 'marketplace npm version mismatch');
|
|
91
|
+
return marketplacePath;
|
|
92
|
+
});
|
|
93
|
+
} else {
|
|
94
|
+
await withCheck(result, 'package-root', async () => {
|
|
95
|
+
const packageJsonPath = path.join(root, 'package.json');
|
|
96
|
+
const packageJson = await readJson(packageJsonPath);
|
|
97
|
+
assert(
|
|
98
|
+
packageJson.name === '@rex_koh/subagent-budget-guard',
|
|
99
|
+
'package root name mismatch'
|
|
100
|
+
);
|
|
101
|
+
return packageJsonPath;
|
|
102
|
+
});
|
|
103
|
+
}
|
|
68
104
|
|
|
69
105
|
await withCheck(result, 'plugin-manifest-user-config', async () => {
|
|
70
106
|
const manifestPath = path.join(root, '.claude-plugin', 'plugin.json');
|
|
@@ -303,7 +339,8 @@ export async function runLiveVerification({
|
|
|
303
339
|
result.checks.push(...offline.checks);
|
|
304
340
|
result.failures.push(...offline.failures);
|
|
305
341
|
|
|
306
|
-
const
|
|
342
|
+
const layout = await resolveLayout(repoRoot);
|
|
343
|
+
const root = layout.pluginRoot;
|
|
307
344
|
const hasClaude = await commandExists('claude');
|
|
308
345
|
if (!hasClaude) {
|
|
309
346
|
result.warnings.push('claude executable was not found on PATH; skipped claude plugin validate and install-state checks.');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rex_koh/subagent-budget-guard",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Claude Code plugin that blocks subagents by default, records verified subagent usage, and enforces 5-hour usage budgets.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "ClaudeSubAgentSuppressor",
|