agent-control-plane 0.1.1 → 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/package.json +2 -2
- package/tools/tests/test-public-repo-docs.sh +1 -0
- package/tools/tests/test-vendored-codex-quota-claude-oauth-only.sh +38 -0
- package/tools/vendor/codex-quota/README.md +8 -16
- package/tools/vendor/codex-quota/lib/claude-accounts.js +2 -68
- package/tools/vendor/codex-quota/lib/claude-usage.js +76 -667
- package/tools/vendor/codex-quota/lib/display.js +6 -14
- package/tools/vendor/codex-quota/lib/handlers.js +32 -117
- package/tools/vendor/codex-quota/lib/sync.js +9 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-control-plane",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Help a repo keep GitHub-driven coding agents running reliably without constant human babysitting",
|
|
5
5
|
"homepage": "https://github.com/ducminhnguyen0319/agent-control-plane",
|
|
6
6
|
"bugs": {
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"scripts": {
|
|
42
42
|
"doctor": "node ./npm/bin/agent-control-plane.js doctor",
|
|
43
43
|
"smoke": "node ./npm/bin/agent-control-plane.js smoke",
|
|
44
|
-
"test": "bash tools/tests/test-agent-control-plane-npm-cli.sh"
|
|
44
|
+
"test": "bash tools/tests/test-agent-control-plane-npm-cli.sh && bash tools/tests/test-vendored-codex-quota-claude-oauth-only.sh"
|
|
45
45
|
},
|
|
46
46
|
"keywords": [
|
|
47
47
|
"agents",
|
|
@@ -25,6 +25,7 @@ test -f "$ROOT_DIR/assets/architecture/state-dashboard-infographic.png"
|
|
|
25
25
|
grep -q 'Use GitHub private vulnerability reporting or a GitHub security advisory' "$ROOT_DIR/SECURITY.md"
|
|
26
26
|
grep -q 'For undisclosed security issues, use \[SECURITY.md\](\./SECURITY.md)' "$ROOT_DIR/CODE_OF_CONDUCT.md"
|
|
27
27
|
grep -q '^## \[Unreleased\]$' "$ROOT_DIR/CHANGELOG.md"
|
|
28
|
+
grep -q '^## \[0.1.2\] - 2026-03-30$' "$ROOT_DIR/CHANGELOG.md"
|
|
28
29
|
grep -q '^## \[0.1.1\] - 2026-03-30$' "$ROOT_DIR/CHANGELOG.md"
|
|
29
30
|
grep -q '^## \[0.1.0\] - 2026-03-27$' "$ROOT_DIR/CHANGELOG.md"
|
|
30
31
|
grep -q '^# Roadmap$' "$ROOT_DIR/ROADMAP.md"
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
5
|
+
ROOT_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
|
6
|
+
|
|
7
|
+
CLAUDE_USAGE_FILE="${ROOT_DIR}/tools/vendor/codex-quota/lib/claude-usage.js"
|
|
8
|
+
CLAUDE_README_FILE="${ROOT_DIR}/tools/vendor/codex-quota/README.md"
|
|
9
|
+
CLAUDE_DISPLAY_FILE="${ROOT_DIR}/tools/vendor/codex-quota/lib/display.js"
|
|
10
|
+
|
|
11
|
+
if rg -n "CLAUDE_COOKIE_DB_PATH|secret-tool|sqlite3|decryptChromeCookie|readClaudeCookiesFromDb|Browser cookies" \
|
|
12
|
+
"${CLAUDE_USAGE_FILE}" "${CLAUDE_README_FILE}" "${CLAUDE_DISPLAY_FILE}" >/dev/null 2>&1; then
|
|
13
|
+
echo "vendored codex-quota still contains browser-cookie Claude usage logic" >&2
|
|
14
|
+
exit 1
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
grep -q "Uses OAuth credentials only" "${CLAUDE_DISPLAY_FILE}"
|
|
18
|
+
grep -q "Claude usage in the bundled public package is OAuth-only." "${CLAUDE_README_FILE}"
|
|
19
|
+
|
|
20
|
+
node --input-type=module <<'EOF'
|
|
21
|
+
import { fetchClaudeUsageForCredentials, fetchClaudeUsage } from "./tools/vendor/codex-quota/lib/claude-usage.js";
|
|
22
|
+
|
|
23
|
+
const missingAccount = await fetchClaudeUsageForCredentials({ label: "demo", source: "test" });
|
|
24
|
+
if (missingAccount.success || missingAccount.error !== "Claude OAuth token required") {
|
|
25
|
+
throw new Error(`unexpected credential fallback result: ${JSON.stringify(missingAccount)}`);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const missingOauth = await fetchClaudeUsage();
|
|
29
|
+
const oauthError = String(missingOauth.error || "").toLowerCase();
|
|
30
|
+
if (
|
|
31
|
+
missingOauth.success ||
|
|
32
|
+
(!oauthError.includes("oauth") && !oauthError.includes("credentials not found"))
|
|
33
|
+
) {
|
|
34
|
+
throw new Error(`unexpected Claude usage fallback result: ${JSON.stringify(missingOauth)}`);
|
|
35
|
+
}
|
|
36
|
+
EOF
|
|
37
|
+
|
|
38
|
+
echo "vendored codex-quota Claude OAuth-only test passed"
|
|
@@ -280,7 +280,7 @@ Root-level fields are preserved on write; unknown root fields are kept intact.
|
|
|
280
280
|
|
|
281
281
|
Claude multi-account files (`~/.claude-accounts.json`) use the same root fields
|
|
282
282
|
(`schemaVersion`, `activeLabel`) and store account entries that include a
|
|
283
|
-
|
|
283
|
+
Claude OAuth token and refresh metadata.
|
|
284
284
|
|
|
285
285
|
## OAuth Flow
|
|
286
286
|
|
|
@@ -400,17 +400,13 @@ To add a Claude credential interactively:
|
|
|
400
400
|
codex-quota claude add
|
|
401
401
|
```
|
|
402
402
|
|
|
403
|
-
This uses
|
|
404
|
-
- `https://
|
|
405
|
-
- `https://claude.ai/api/organizations/{orgId}/usage`
|
|
406
|
-
- `https://claude.ai/api/organizations/{orgId}/overage_spend_limit`
|
|
407
|
-
- `https://claude.ai/api/account`
|
|
403
|
+
This uses OAuth credentials to call:
|
|
404
|
+
- `https://api.anthropic.com/api/oauth/usage`
|
|
408
405
|
|
|
409
406
|
Authentication sources (in order):
|
|
410
407
|
1. `CLAUDE_ACCOUNTS` env var (JSON array or `{ accounts: [...] }`)
|
|
411
|
-
2. `~/.claude-accounts.json` (multi-account format)
|
|
412
|
-
3.
|
|
413
|
-
4. `~/.claude/.credentials.json` OAuth `accessToken`
|
|
408
|
+
2. `~/.claude-accounts.json` (multi-account format with `oauthToken`)
|
|
409
|
+
3. `~/.claude/.credentials.json` OAuth `accessToken`
|
|
414
410
|
|
|
415
411
|
Multi-account format (Claude):
|
|
416
412
|
```json
|
|
@@ -418,8 +414,6 @@ Multi-account format (Claude):
|
|
|
418
414
|
"accounts": [
|
|
419
415
|
{
|
|
420
416
|
"label": "personal",
|
|
421
|
-
"sessionKey": "sk-ant-oat...",
|
|
422
|
-
"cfClearance": "cf_clearance...",
|
|
423
417
|
"oauthToken": "claude-ai-access-token",
|
|
424
418
|
"orgId": "org_uuid_optional"
|
|
425
419
|
}
|
|
@@ -428,13 +422,12 @@ Multi-account format (Claude):
|
|
|
428
422
|
```
|
|
429
423
|
|
|
430
424
|
Notes:
|
|
431
|
-
-
|
|
432
|
-
- `
|
|
425
|
+
- `label` plus `oauthToken` is required.
|
|
426
|
+
- `orgId` is optional.
|
|
433
427
|
|
|
434
428
|
Environment overrides:
|
|
435
429
|
- `CLAUDE_ACCOUNTS` to supply multi-account JSON directly
|
|
436
430
|
- `CLAUDE_CREDENTIALS_PATH` to point to a different credentials file
|
|
437
|
-
- `CLAUDE_COOKIE_DB_PATH` to point to a specific Chromium/Chrome Cookies DB
|
|
438
431
|
|
|
439
432
|
Codex overrides:
|
|
440
433
|
- `CODEX_ACCOUNTS` to supply multi-account JSON directly (read-only)
|
|
@@ -443,8 +436,7 @@ Codex overrides:
|
|
|
443
436
|
- `PI_AUTH_PATH` to point to a different pi auth file
|
|
444
437
|
|
|
445
438
|
Notes:
|
|
446
|
-
-
|
|
447
|
-
- For best results, keep `claude.ai` logged in within your Chromium/Chrome profile.
|
|
439
|
+
- Claude usage in the bundled public package is OAuth-only.
|
|
448
440
|
|
|
449
441
|
## Releasing
|
|
450
442
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Claude account loading
|
|
2
|
+
* Claude account loading and OAuth resolution.
|
|
3
3
|
* Depends on: lib/constants.js, lib/container.js, lib/paths.js
|
|
4
4
|
*/
|
|
5
5
|
|
|
@@ -8,52 +8,18 @@ import { CLAUDE_CREDENTIALS_PATH, CLAUDE_MULTI_ACCOUNT_PATHS } from "./constants
|
|
|
8
8
|
import { readMultiAccountContainer, writeMultiAccountContainer } from "./container.js";
|
|
9
9
|
import { getOpencodeAuthPath } from "./paths.js";
|
|
10
10
|
|
|
11
|
-
export function isClaudeSessionKey(value) {
|
|
12
|
-
return typeof value === "string" && value.startsWith("sk-ant-");
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export function findClaudeSessionKey(value) {
|
|
16
|
-
if (isClaudeSessionKey(value)) return value;
|
|
17
|
-
if (typeof value === "string") {
|
|
18
|
-
const match = value.match(/sk-ant-[a-z0-9_-]+/i);
|
|
19
|
-
if (match) return match[0];
|
|
20
|
-
}
|
|
21
|
-
if (!value || typeof value !== "object") return null;
|
|
22
|
-
|
|
23
|
-
const direct = value.sessionKey
|
|
24
|
-
?? value.session_key
|
|
25
|
-
?? value.token
|
|
26
|
-
?? value.sessionToken
|
|
27
|
-
?? value.accessToken
|
|
28
|
-
?? value.access_token
|
|
29
|
-
?? value.oauthAccessToken;
|
|
30
|
-
if (isClaudeSessionKey(direct)) return direct;
|
|
31
|
-
|
|
32
|
-
for (const child of Object.values(value)) {
|
|
33
|
-
const found = findClaudeSessionKey(child);
|
|
34
|
-
if (found) return found;
|
|
35
|
-
}
|
|
36
|
-
return null;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
11
|
export function normalizeClaudeAccount(raw, source) {
|
|
40
12
|
if (!raw || typeof raw !== "object") return null;
|
|
41
13
|
const label = raw.label ?? null;
|
|
42
|
-
const sessionKey = raw.sessionKey ?? raw.session_key ?? null;
|
|
43
14
|
const oauthToken = raw.oauthToken ?? raw.oauth_token ?? raw.accessToken ?? raw.access_token ?? null;
|
|
44
|
-
const cfClearance = raw.cfClearance ?? raw.cf_clearance ?? null;
|
|
45
15
|
const orgId = raw.orgId ?? raw.org_id ?? null;
|
|
46
|
-
const cookies = raw.cookies && typeof raw.cookies === "object" ? raw.cookies : null;
|
|
47
16
|
const oauthRefreshToken = raw.oauthRefreshToken ?? raw.oauth_refresh_token ?? null;
|
|
48
17
|
const oauthExpiresAt = raw.oauthExpiresAt ?? raw.oauth_expires_at ?? null;
|
|
49
18
|
const oauthScopes = raw.oauthScopes ?? raw.oauth_scopes ?? null;
|
|
50
19
|
return {
|
|
51
20
|
label,
|
|
52
|
-
sessionKey,
|
|
53
21
|
oauthToken,
|
|
54
|
-
cfClearance,
|
|
55
22
|
orgId,
|
|
56
|
-
cookies,
|
|
57
23
|
oauthRefreshToken,
|
|
58
24
|
oauthExpiresAt,
|
|
59
25
|
oauthScopes,
|
|
@@ -63,9 +29,8 @@ export function normalizeClaudeAccount(raw, source) {
|
|
|
63
29
|
|
|
64
30
|
export function isValidClaudeAccount(account) {
|
|
65
31
|
if (!account?.label) return false;
|
|
66
|
-
const sessionKey = account.sessionKey ?? findClaudeSessionKey(account.cookies);
|
|
67
32
|
const oauthToken = account.oauthToken ?? null;
|
|
68
|
-
return Boolean(
|
|
33
|
+
return Boolean(oauthToken);
|
|
69
34
|
}
|
|
70
35
|
|
|
71
36
|
export function loadClaudeAccountsFromEnv() {
|
|
@@ -147,37 +112,6 @@ export function saveClaudeAccounts(accounts) {
|
|
|
147
112
|
return result.path;
|
|
148
113
|
}
|
|
149
114
|
|
|
150
|
-
export function loadClaudeSessionFromCredentials() {
|
|
151
|
-
const credentialsPath = process.env.CLAUDE_CREDENTIALS_PATH || CLAUDE_CREDENTIALS_PATH;
|
|
152
|
-
if (!existsSync(credentialsPath)) {
|
|
153
|
-
return {
|
|
154
|
-
sessionKey: null,
|
|
155
|
-
source: credentialsPath,
|
|
156
|
-
error: `Claude credentials not found at ${credentialsPath}`,
|
|
157
|
-
};
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
try {
|
|
161
|
-
const raw = readFileSync(credentialsPath, "utf-8");
|
|
162
|
-
const parsed = JSON.parse(raw);
|
|
163
|
-
const sessionKey = findClaudeSessionKey(parsed);
|
|
164
|
-
if (!sessionKey) {
|
|
165
|
-
return {
|
|
166
|
-
sessionKey: null,
|
|
167
|
-
source: credentialsPath,
|
|
168
|
-
error: "No Claude sessionKey found in credentials file",
|
|
169
|
-
};
|
|
170
|
-
}
|
|
171
|
-
return { sessionKey, source: credentialsPath };
|
|
172
|
-
} catch (err) {
|
|
173
|
-
return {
|
|
174
|
-
sessionKey: null,
|
|
175
|
-
source: credentialsPath,
|
|
176
|
-
error: `Failed to read Claude credentials: ${err?.message ?? String(err)}`,
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
115
|
export function loadClaudeOAuthToken() {
|
|
182
116
|
const credentialsPath = process.env.CLAUDE_CREDENTIALS_PATH || CLAUDE_CREDENTIALS_PATH;
|
|
183
117
|
if (!existsSync(credentialsPath)) {
|