agent-control-plane 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/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # agent-control-plane
2
2
 
3
3
  <p>
4
- <a href="https://github.com/ducminhuyen0319/agent-control-plane/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/ducminhuyen0319/agent-control-plane/actions/workflows/ci.yml/badge.svg?branch=main"></a>
4
+ <a href="https://github.com/ducminhnguyen0319/agent-control-plane/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/ducminhnguyen0319/agent-control-plane/actions/workflows/ci.yml/badge.svg?branch=main"></a>
5
5
  <a href="https://www.npmjs.com/package/agent-control-plane"><img alt="npm version" src="https://img.shields.io/npm/v/agent-control-plane?style=flat-square"></a>
6
6
  <a href="https://www.npmjs.com/package/agent-control-plane"><img alt="node version" src="https://img.shields.io/node/v/agent-control-plane?style=flat-square"></a>
7
7
  <a href="./LICENSE"><img alt="license" src="https://img.shields.io/npm/l/agent-control-plane?style=flat-square"></a>
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "agent-control-plane",
3
- "version": "0.1.0",
3
+ "version": "0.1.2",
4
4
  "description": "Help a repo keep GitHub-driven coding agents running reliably without constant human babysitting",
5
- "homepage": "https://github.com/ducminhuyen0319/agent-control-plane",
5
+ "homepage": "https://github.com/ducminhnguyen0319/agent-control-plane",
6
6
  "bugs": {
7
- "url": "https://github.com/ducminhuyen0319/agent-control-plane/issues"
7
+ "url": "https://github.com/ducminhnguyen0319/agent-control-plane/issues"
8
8
  },
9
9
  "repository": {
10
10
  "type": "git",
11
- "url": "git+https://github.com/ducminhuyen0319/agent-control-plane.git"
11
+ "url": "git+https://github.com/ducminhnguyen0319/agent-control-plane.git"
12
12
  },
13
13
  "license": "MIT",
14
14
  "funding": [
@@ -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",
@@ -73,7 +73,7 @@ pr_review_template="${SKILL_ROOT}/tools/templates/pr-review-template.md"
73
73
  pr_fix_template="${SKILL_ROOT}/tools/templates/pr-fix-template.md"
74
74
  pr_merge_repair_template="${SKILL_ROOT}/tools/templates/pr-merge-repair-template.md"
75
75
  legacy_profile_repo_name="$(printf 'f-%s' 'losning')"
76
- legacy_profile_repo_slug="$(printf '%s/%s' 'ducminhuyen0319' "${legacy_profile_repo_name}")"
76
+ legacy_profile_repo_slug="$(printf '%s/%s' 'example-owner' "${legacy_profile_repo_name}")"
77
77
  legacy_profile_package_scope="@${legacy_profile_repo_name}"
78
78
  commands_map="${SKILL_ROOT}/references/commands.md"
79
79
  skill_doc="${SKILL_ROOT}/SKILL.md"
@@ -4,6 +4,7 @@ set -euo pipefail
4
4
  SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
5
5
  FLOW_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
6
6
  CLI_SCRIPT="${FLOW_ROOT}/npm/bin/agent-control-plane.js"
7
+ PACKAGE_VERSION="$(node -p "require('${FLOW_ROOT}/package.json').version")"
7
8
 
8
9
  tmpdir="$(mktemp -d)"
9
10
  trap 'rm -rf "${tmpdir}"' EXIT
@@ -74,7 +75,7 @@ version_output="$(
74
75
  node "${CLI_SCRIPT}" version
75
76
  )"
76
77
 
77
- grep -q '^0\.1\.0$' <<<"${version_output}"
78
+ grep -q "^${PACKAGE_VERSION}$" <<<"${version_output}"
78
79
 
79
80
  HOME="${home_dir}" \
80
81
  AGENT_PLATFORM_HOME="${platform_home}" \
@@ -11,13 +11,13 @@ const pkg = JSON.parse(fs.readFileSync(process.argv[1], "utf8"));
11
11
  if (!pkg.description || !pkg.description.includes("running reliably without constant human babysitting")) {
12
12
  process.exit(1);
13
13
  }
14
- if (pkg.homepage !== "https://github.com/ducminhuyen0319/agent-control-plane") {
14
+ if (pkg.homepage !== "https://github.com/ducminhnguyen0319/agent-control-plane") {
15
15
  process.exit(10);
16
16
  }
17
- if (!pkg.bugs || pkg.bugs.url !== "https://github.com/ducminhuyen0319/agent-control-plane/issues") {
17
+ if (!pkg.bugs || pkg.bugs.url !== "https://github.com/ducminhnguyen0319/agent-control-plane/issues") {
18
18
  process.exit(11);
19
19
  }
20
- if (!pkg.repository || pkg.repository.type !== "git" || pkg.repository.url !== "git+https://github.com/ducminhuyen0319/agent-control-plane.git") {
20
+ if (!pkg.repository || pkg.repository.type !== "git" || pkg.repository.url !== "git+https://github.com/ducminhnguyen0319/agent-control-plane.git") {
21
21
  process.exit(12);
22
22
  }
23
23
  if (pkg.license !== "MIT") {
@@ -25,6 +25,8 @@ 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"
29
+ grep -q '^## \[0.1.1\] - 2026-03-30$' "$ROOT_DIR/CHANGELOG.md"
28
30
  grep -q '^## \[0.1.0\] - 2026-03-27$' "$ROOT_DIR/CHANGELOG.md"
29
31
  grep -q '^# Roadmap$' "$ROOT_DIR/ROADMAP.md"
30
32
  grep -q '^## Platform Support$' "$ROOT_DIR/ROADMAP.md"
@@ -91,11 +93,11 @@ grep -q '^| `ollama` | local model runtime | Candidate local-model substrate beh
91
93
  grep -q '^| `nanoclaw` | adjacent agent shell | Ecosystem reference for containerized and WSL2-friendly workflows | exploratory interoperability target |$' "$ROOT_DIR/README.md"
92
94
  grep -q '^| `picoclaw` | adjacent agent shell | Ecosystem reference for lightweight Linux and edge-style agent runtimes | exploratory interoperability target |$' "$ROOT_DIR/README.md"
93
95
  grep -q 'If you are trying ACP on a real repo right now, start with `codex`, `claude`,' "$ROOT_DIR/README.md"
94
- grep -q 'github.com/ducminhuyen0319/agent-control-plane/actions/workflows/ci.yml/badge.svg?branch=main' "$ROOT_DIR/README.md"
96
+ grep -q 'github.com/ducminhnguyen0319/agent-control-plane/actions/workflows/ci.yml/badge.svg?branch=main' "$ROOT_DIR/README.md"
95
97
  grep -q 'img.shields.io/npm/v/agent-control-plane' "$ROOT_DIR/README.md"
96
98
  grep -q 'img.shields.io/node/v/agent-control-plane' "$ROOT_DIR/README.md"
97
99
  grep -q 'img.shields.io/npm/l/agent-control-plane' "$ROOT_DIR/README.md"
98
- grep -q 'https://github.com/ducminhuyen0319/agent-control-plane/actions/workflows/ci.yml' "$ROOT_DIR/README.md"
100
+ grep -q 'https://github.com/ducminhnguyen0319/agent-control-plane/actions/workflows/ci.yml' "$ROOT_DIR/README.md"
99
101
  grep -q 'https://www.npmjs.com/package/agent-control-plane' "$ROOT_DIR/README.md"
100
102
  grep -q '\./assets/readme/dashboard-demo.png' "$ROOT_DIR/README.md"
101
103
  grep -q '\./assets/readme/dashboard-demo.gif' "$ROOT_DIR/README.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
- `sessionKey` or OAuth tokens.
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 your local Claude session to call:
404
- - `https://claude.ai/api/organizations`
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. Browser cookies (Chromium/Chrome) to read `sessionKey` and `lastActiveOrg`
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
- - Only `label` plus one of `sessionKey` or `oauthToken` is required.
432
- - `cfClearance`, `orgId`, and `cookies` are optional.
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
- - On Linux, cookie access requires `sqlite3` and `secret-tool` (libsecret) to decrypt cookies.
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, session/OAuth resolution.
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(sessionKey || oauthToken);
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)) {