agent-lyceum 0.27.0 → 0.29.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/README.md +1 -1
- package/README.zh-TW.md +1 -1
- package/dist/adapters/claude.js +11 -0
- package/dist/adapters/codex.js +2 -0
- package/dist/call.js +3 -2
- package/dist/cli.js +51 -1
- package/dist/config.d.ts +23 -1
- package/dist/config.js +38 -2
- package/dist/fallback.d.ts +10 -0
- package/dist/fallback.js +23 -2
- package/dist/mailbox.js +19 -2
- package/dist/policy.d.ts +33 -0
- package/dist/policy.js +80 -0
- package/dist/project-lock.d.ts +2 -0
- package/dist/project-lock.js +27 -2
- package/dist/quota.d.ts +25 -0
- package/dist/quota.js +141 -0
- package/dist/run-session.js +17 -3
- package/dist/sandbox-invocation.d.ts +35 -0
- package/dist/sandbox-invocation.js +186 -0
- package/dist/schema.d.ts +27 -1
- package/dist/schema.js +14 -1
- package/dist/status-watch.d.ts +5 -1
- package/dist/status-watch.js +5 -3
- package/dist/validate.js +5 -0
- package/package.json +5 -3
- package/web/dist/public/assets/index-B1Ld6Pl5.css +2 -0
- package/web/dist/public/assets/index-D2JeeQpl.js +9 -0
- package/web/dist/public/index.html +13 -0
- package/web/dist/server/http.js +94 -0
- package/web/dist/server/hub.js +97 -0
- package/web/dist/server/open.js +9 -0
- package/web/dist/start.js +58 -0
- package/web/dist/types.js +1 -0
package/README.md
CHANGED
|
@@ -49,7 +49,7 @@ Its own data (config, mailboxes, logs, memory, worktrees) lives in `~/agent-lyce
|
|
|
49
49
|
| `init` | Create the home and the global agent library. [details](docs/commands.md#init) |
|
|
50
50
|
| `project add` / `project` / `project remove` | Register, list or remove projects. [details](docs/commands.md#project-add) |
|
|
51
51
|
| `doctor` | Check config, git, lock and runtime CLIs without running agents. [details](docs/commands.md#doctor) |
|
|
52
|
-
| `status` | Show agents, mail, the current wake-up and past runs. [details](docs/commands.md#status) |
|
|
52
|
+
| `status` | Show agents, mail, the current wake-up and past runs. `status --web` opens every registered project in a browser. [details](docs/commands.md#status) |
|
|
53
53
|
| `run` | Give the task to the lead and run until done. [details](docs/commands.md#run) |
|
|
54
54
|
| `resume` | Continue an interrupted run, or reopen a finished one with a follow-up. [details](docs/commands.md#resume) |
|
|
55
55
|
| `clear` | Delete a run, or remove a stale lock. [details](docs/commands.md#clear) |
|
package/README.zh-TW.md
CHANGED
|
@@ -49,7 +49,7 @@ npx agent-lyceum status
|
|
|
49
49
|
| `init` | 建立 home 與全域 agent 庫。 [詳細](docs/commands.zh-TW.md#init) |
|
|
50
50
|
| `project add` / `project` / `project remove` | 註冊、列出或移除專案。 [詳細](docs/commands.zh-TW.md#project-add) |
|
|
51
51
|
| `doctor` | 不執行 agent,檢查設定、git、鎖與 runtime CLI。 [詳細](docs/commands.zh-TW.md#doctor) |
|
|
52
|
-
| `status` | 顯示 agent、信件、目前的喚醒與過去的 run
|
|
52
|
+
| `status` | 顯示 agent、信件、目前的喚醒與過去的 run。`status --web` 在瀏覽器看全部已註冊專案。 [詳細](docs/commands.zh-TW.md#status) |
|
|
53
53
|
| `run` | 把任務交給 lead,執行到完成。 [詳細](docs/commands.zh-TW.md#run) |
|
|
54
54
|
| `resume` | 繼續中斷的 run,或用追加需求重開已完成的 run。 [詳細](docs/commands.zh-TW.md#resume) |
|
|
55
55
|
| `clear` | 刪除 run,或移除殘留的鎖。 [詳細](docs/commands.zh-TW.md#clear) |
|
package/dist/adapters/claude.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { repoDirFor, scratchCwd, writePolicy } from "../policy.js";
|
|
4
|
+
import { canonicalPath } from "../paths.js";
|
|
4
5
|
/** Claude Code path rules: `//abs/path` — a leading "/" plus the absolute path. */
|
|
5
6
|
const rule = (tool, p) => `${tool}(/${p})`;
|
|
6
7
|
function denyRules(p) {
|
|
@@ -34,6 +35,15 @@ export function buildClaudeSettings(project, agent, override) {
|
|
|
34
35
|
for (const f of pol.allowFiles)
|
|
35
36
|
allow.push(rule("Edit", f));
|
|
36
37
|
const deny = pol.deny.flatMap(denyRules);
|
|
38
|
+
// Spec 25: network allowances; never with a policy override (a minimal-authority wake).
|
|
39
|
+
const sb = override ? undefined : agent.sandbox;
|
|
40
|
+
const network = {};
|
|
41
|
+
if (sb?.network.length)
|
|
42
|
+
network.allowedDomains = sb.network;
|
|
43
|
+
if (sb?.localBinding)
|
|
44
|
+
network.allowLocalBinding = true;
|
|
45
|
+
if (sb?.unixSockets.length)
|
|
46
|
+
network.allowUnixSockets = sb.unixSockets.map(canonicalPath);
|
|
37
47
|
return {
|
|
38
48
|
// The user's own and plugin hooks (e.g. a hook that asks the user a question) would block every tool call of a headless wake-up.
|
|
39
49
|
disableAllHooks: true,
|
|
@@ -45,6 +55,7 @@ export function buildClaudeSettings(project, agent, override) {
|
|
|
45
55
|
allowWrite: [override?.cwd ?? ws?.root ?? project.dir, ...pol.allowDirs, ...pol.allowFiles],
|
|
46
56
|
denyWrite: pol.deny,
|
|
47
57
|
},
|
|
58
|
+
...(Object.keys(network).length ? { network } : {}),
|
|
48
59
|
},
|
|
49
60
|
};
|
|
50
61
|
}
|
package/dist/adapters/codex.js
CHANGED
|
@@ -63,6 +63,8 @@ export function buildCodexInvocation(input) {
|
|
|
63
63
|
'approval_policy="never"',
|
|
64
64
|
"-c",
|
|
65
65
|
`sandbox_workspace_write.writable_roots=${JSON.stringify(roots)}`,
|
|
66
|
+
// Codex has one switch for all network access; spec 25 never uses it under a policy override.
|
|
67
|
+
...(!input.policy && agent.sandbox?.network.length ? ["-c", "sandbox_workspace_write.network_access=true"] : []),
|
|
66
68
|
"--json",
|
|
67
69
|
"-o",
|
|
68
70
|
lastFile,
|
package/dist/call.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { invokeWithSandboxChecks } from "./sandbox-invocation.js";
|
|
3
4
|
import { realInvoker } from "./adapters/index.js";
|
|
4
5
|
import { must } from "./assert.js";
|
|
5
6
|
import { ConfigError, loadGlobal, resolveProjectFrom } from "./config.js";
|
|
@@ -157,7 +158,7 @@ export async function callAgent(o) {
|
|
|
157
158
|
return;
|
|
158
159
|
say(` ${agent.name} falling back to ${to.model ? `${to.runtime}/${to.model}` : to.runtime} (${kind ?? "unknown error"})`);
|
|
159
160
|
},
|
|
160
|
-
invoke: (c) => invoke
|
|
161
|
+
invoke: (c, ctx) => invokeWithSandboxChecks(invoke, {
|
|
161
162
|
project,
|
|
162
163
|
agent: agentForCandidate(agent, c),
|
|
163
164
|
workDir: callDir,
|
|
@@ -166,7 +167,7 @@ export async function callAgent(o) {
|
|
|
166
167
|
timeoutSec: project.dispatcher.wake_timeout_sec,
|
|
167
168
|
signal: o.signal,
|
|
168
169
|
logDir: logDirFor(c),
|
|
169
|
-
}).catch((e) => ({ ok: false, text: "", exitCode: null, timedOut: false, error: e.message })),
|
|
170
|
+
}, agent, { event: (name, fields) => say(JSON.stringify({ event: name, ...fields })) }, ctx.attempt).catch((e) => ({ ok: false, text: "", exitCode: null, timedOut: false, error: e.message })),
|
|
170
171
|
});
|
|
171
172
|
const result = must(outcome.result, "the call's result");
|
|
172
173
|
const tried = chain ? outcome.tried : undefined;
|
package/dist/cli.js
CHANGED
|
@@ -3,6 +3,7 @@ import fs from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import readline from "node:readline/promises";
|
|
5
5
|
import { spawnSync } from "node:child_process";
|
|
6
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
7
|
import { Command } from "commander";
|
|
7
8
|
import { ConfigError, findProjectForCwd, flattenResolved, listProjects, loadGlobal, resolveProject, resolveProjectWithSources } from "./config.js";
|
|
8
9
|
import { openInEditor, prepareAnswers } from "./ask-answer.js";
|
|
@@ -27,6 +28,7 @@ import { createStatusWatcher } from "./status-watch.js";
|
|
|
27
28
|
import { LAUNCH_PPID, startedByWrapper, watchParent } from "./parent-watch.js";
|
|
28
29
|
import { onlyAgent, soloProject } from "./solo.js";
|
|
29
30
|
import { DEMO_PROJECT, DEMO_TASK, defaultDemoDir, detectRuntime, prepareDemo } from "./demo.js";
|
|
31
|
+
import { claudeQuota, codexQuota } from "./quota.js";
|
|
30
32
|
const program = new Command();
|
|
31
33
|
program
|
|
32
34
|
.name("agent-lyceum")
|
|
@@ -764,9 +766,57 @@ program
|
|
|
764
766
|
.option("--interval <sec>", "refresh interval for --monitor", "2")
|
|
765
767
|
.option("--json", "print the report as JSON (no colour); works with --task-list and --task-id")
|
|
766
768
|
.option("--watch", "with --json: keep running and print one JSON line (the project report plus one per running task) whenever it changes (Ctrl-C to quit)")
|
|
767
|
-
.option("--heartbeat <sec>", "with --watch: how often to re-check when no file event came", "5")
|
|
769
|
+
.option("--heartbeat <sec>", "with --watch or --web: how often to re-check when no file event came", "5")
|
|
770
|
+
.option("--web", "open a local page for every registered project")
|
|
771
|
+
.option("--port <n>", "with --web: TCP port", "8787")
|
|
772
|
+
.option("--host <addr>", "with --web: bind address", "127.0.0.1")
|
|
773
|
+
.option("--no-open", "with --web: do not open a browser")
|
|
768
774
|
.action(async (opts) => {
|
|
769
775
|
try {
|
|
776
|
+
if (opts.web) {
|
|
777
|
+
if (opts.json || opts.watch || opts.monitor || opts.taskList || opts.taskId !== undefined)
|
|
778
|
+
fail("--web cannot be combined with --json, --watch, --monitor, --task-list or --task-id.");
|
|
779
|
+
if (opts.project)
|
|
780
|
+
fail("--web shows every registered project; it does not take --project.");
|
|
781
|
+
const port = Number(opts.port);
|
|
782
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535)
|
|
783
|
+
fail("--port must be a TCP port.");
|
|
784
|
+
const beat = Number(opts.heartbeat);
|
|
785
|
+
if (!(beat > 0))
|
|
786
|
+
fail("--heartbeat must be a positive number of seconds.");
|
|
787
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
788
|
+
const entry = path.join(root, "web", "dist", "start.js");
|
|
789
|
+
const staticDir = path.join(root, "web", "dist", "public");
|
|
790
|
+
if (!fs.existsSync(entry) || !fs.existsSync(path.join(staticDir, "index.html"))) {
|
|
791
|
+
fail(`The web monitor is not built.\nFrom the repo: pnpm --dir web install && pnpm --dir web build\nExpected ${entry}`);
|
|
792
|
+
}
|
|
793
|
+
const mod = (await import(pathToFileURL(entry).href));
|
|
794
|
+
const page = await mod.startWebServer({
|
|
795
|
+
port,
|
|
796
|
+
host: opts.host,
|
|
797
|
+
open: opts.open !== false,
|
|
798
|
+
staticDir,
|
|
799
|
+
heartbeatMs: beat * 1000,
|
|
800
|
+
factory: {
|
|
801
|
+
list: () => listProjects(home()).map((item) => item.name),
|
|
802
|
+
watch: (name, onFrame) => createStatusWatcher({
|
|
803
|
+
load: () => resolveProject(home(), name),
|
|
804
|
+
dir: (project) => project.paths.runs,
|
|
805
|
+
write: (line) => onFrame(JSON.parse(line)),
|
|
806
|
+
debounceMs: 100,
|
|
807
|
+
allTasks: true,
|
|
808
|
+
}),
|
|
809
|
+
quota: () => Promise.all([claudeQuota(), codexQuota()]),
|
|
810
|
+
},
|
|
811
|
+
});
|
|
812
|
+
console.log(page.url);
|
|
813
|
+
const stop = (code) => {
|
|
814
|
+
void page.close().finally(() => process.exit(code));
|
|
815
|
+
};
|
|
816
|
+
process.on("SIGINT", () => stop(130));
|
|
817
|
+
process.on("SIGTERM", () => stop(143));
|
|
818
|
+
await new Promise(() => { });
|
|
819
|
+
}
|
|
770
820
|
if (opts.watch) {
|
|
771
821
|
if (!opts.json)
|
|
772
822
|
fail("--watch needs --json.");
|
package/dist/config.d.ts
CHANGED
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
import { type ProjectPaths } from "./paths.js";
|
|
2
2
|
import type { AgentWorkspace } from "./worktree.js";
|
|
3
|
-
import { type DispatcherSettings, type Effort, type FallbackEntry, type Runtime, type WebLevel } from "./schema.js";
|
|
3
|
+
import { type DispatcherSettings, type Effort, type FallbackEntry, type SandboxPartialT, type Runtime, type WebLevel } from "./schema.js";
|
|
4
4
|
export declare class ConfigError extends Error {
|
|
5
5
|
}
|
|
6
6
|
export type Source = "global" | "project" | "default" | "model";
|
|
7
|
+
export interface SandboxAllowances {
|
|
8
|
+
write: string[];
|
|
9
|
+
network: string[];
|
|
10
|
+
localBinding: boolean;
|
|
11
|
+
unixSockets: string[];
|
|
12
|
+
}
|
|
7
13
|
export interface ResolvedAgent {
|
|
8
14
|
name: string;
|
|
9
15
|
runtime?: Runtime;
|
|
@@ -22,6 +28,8 @@ export interface ResolvedAgent {
|
|
|
22
28
|
canAskUser: boolean;
|
|
23
29
|
/** Web access for this agent; undefined = not configured, so the runtime keeps its own default. */
|
|
24
30
|
allowWeb?: WebLevel;
|
|
31
|
+
/** Spec 25 allowances; unset when none is requested (the only switch that keeps output identical). Paths are absolute, `~` expanded, not yet canonical. */
|
|
32
|
+
sandbox?: SandboxAllowances;
|
|
25
33
|
/** Ordered failover chain (runtime and model filled in); empty = no fallback. */
|
|
26
34
|
fallback: FallbackEntry[];
|
|
27
35
|
/** True when the chain came from `dispatcher.fallback` rather than the agent's own setting. */
|
|
@@ -102,6 +110,12 @@ export declare function loadGlobal(home: string): {
|
|
|
102
110
|
model?: string | undefined;
|
|
103
111
|
effort?: "high" | "low" | "max" | "medium" | "minimal" | "xhigh" | undefined;
|
|
104
112
|
}[] | undefined;
|
|
113
|
+
sandbox?: {
|
|
114
|
+
write?: string[] | undefined;
|
|
115
|
+
network?: string[] | undefined;
|
|
116
|
+
local_binding?: boolean | undefined;
|
|
117
|
+
unix_sockets?: string[] | undefined;
|
|
118
|
+
} | undefined;
|
|
105
119
|
owns?: unknown;
|
|
106
120
|
}>;
|
|
107
121
|
};
|
|
@@ -150,6 +164,12 @@ export declare function loadProjectRaw(home: string, name: string): {
|
|
|
150
164
|
model?: string | undefined;
|
|
151
165
|
effort?: "high" | "low" | "max" | "medium" | "minimal" | "xhigh" | undefined;
|
|
152
166
|
}[] | undefined;
|
|
167
|
+
sandbox?: {
|
|
168
|
+
write?: string[] | undefined;
|
|
169
|
+
network?: string[] | undefined;
|
|
170
|
+
local_binding?: boolean | undefined;
|
|
171
|
+
unix_sockets?: string[] | undefined;
|
|
172
|
+
} | undefined;
|
|
153
173
|
owns?: unknown;
|
|
154
174
|
}>;
|
|
155
175
|
};
|
|
@@ -177,6 +197,8 @@ export type SourceInfo = {
|
|
|
177
197
|
} | {
|
|
178
198
|
default: true;
|
|
179
199
|
};
|
|
200
|
+
/** Spec 25.4.2: validated, deduplicated allowances; undefined when nothing is requested. */
|
|
201
|
+
export declare function resolveSandbox(agent: string, s: SandboxPartialT | undefined): SandboxAllowances | undefined;
|
|
180
202
|
/** The resolved project plus, for every setting, the file and key it was taken from (or "default"). */
|
|
181
203
|
export declare function resolveProjectWithSources(home: string, name: string): {
|
|
182
204
|
project: ResolvedProject;
|
package/dist/config.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import YAML from "yaml";
|
|
4
|
-
import { absPath, escapesParent, homePaths, projectPaths, } from "./paths.js";
|
|
4
|
+
import { absPath, canonicalPath, escapesParent, expandHome, homePaths, projectPaths, } from "./paths.js";
|
|
5
5
|
import { availableTiers } from "./tiers.js";
|
|
6
6
|
import { DISPATCHER_DEFAULTS, GlobalConfig, REMOVED_AGENT_KEYS, REMOVED_DISPATCHER_KEYS, ProjectConfig, inferRuntime, RUNTIMES, } from "./schema.js";
|
|
7
7
|
export class ConfigError extends Error {
|
|
@@ -162,6 +162,7 @@ export function resolveProjectFrom(home, name, g, p) {
|
|
|
162
162
|
const canEdit = pick("can_edit_agent_md");
|
|
163
163
|
const canAsk = pick("can_ask_user");
|
|
164
164
|
const allowWeb = pick("allow_web");
|
|
165
|
+
const sandbox = resolveSandbox(agentName, pick("sandbox"));
|
|
165
166
|
for (const [k, v] of [
|
|
166
167
|
["resume", resume],
|
|
167
168
|
["can_message", canMessage],
|
|
@@ -183,6 +184,7 @@ export function resolveProjectFrom(home, name, g, p) {
|
|
|
183
184
|
canEditAgentMd: canEdit ?? isLead,
|
|
184
185
|
canAskUser: canAsk ?? isLead,
|
|
185
186
|
allowWeb,
|
|
187
|
+
...(sandbox ? { sandbox } : {}),
|
|
186
188
|
fallback,
|
|
187
189
|
fallbackShared,
|
|
188
190
|
fallbackInvalid,
|
|
@@ -274,7 +276,39 @@ export function findProjectForCwd(home, cwd) {
|
|
|
274
276
|
}
|
|
275
277
|
return best?.name;
|
|
276
278
|
}
|
|
277
|
-
const AGENT_FIELDS = ["runtime", "model", "effort", "agent_md", "memory.global", "memory.project", "resume", "can_message", "can_edit_agent_md", "can_ask_user", "allow_web", "fallback"];
|
|
279
|
+
const AGENT_FIELDS = ["runtime", "model", "effort", "agent_md", "memory.global", "memory.project", "resume", "can_message", "can_edit_agent_md", "can_ask_user", "allow_web", "fallback", "sandbox"];
|
|
280
|
+
/** Claude pattern metacharacters and control characters: refused in sandbox paths instead of escaped (spec 25.4.1). */
|
|
281
|
+
const PATH_CHARS = /[*?[\]{}()!\\]/;
|
|
282
|
+
const PATH_META = { test: (p) => PATH_CHARS.test(p) || [...p].some((c) => c.charCodeAt(0) < 32 || c.charCodeAt(0) === 127) };
|
|
283
|
+
function sandboxPath(agent, key, i, raw) {
|
|
284
|
+
const at = `agents.${agent}.sandbox.${key}.${i}`;
|
|
285
|
+
if (PATH_META.test(raw))
|
|
286
|
+
throw new ConfigError(`${at}: must not contain pattern characters (* ? [ ] { } ( ) !), backslashes or control characters (got ${JSON.stringify(raw)})`);
|
|
287
|
+
if (/^\$|\$\{?[A-Za-z_]/.test(raw))
|
|
288
|
+
throw new ConfigError(`${at}: environment variables are not expanded; write the absolute path (got "${raw}")`);
|
|
289
|
+
const expanded = expandHome(raw);
|
|
290
|
+
if (!path.isAbsolute(expanded))
|
|
291
|
+
throw new ConfigError(`${at}: must be an absolute path or start with ~/ (got "${raw}")`);
|
|
292
|
+
const resolved = path.resolve(expanded);
|
|
293
|
+
const canon = canonicalPath(resolved);
|
|
294
|
+
for (const p of [resolved, canon])
|
|
295
|
+
if (PATH_META.test(p))
|
|
296
|
+
throw new ConfigError(`${at}: resolved path contains pattern characters (${JSON.stringify(p)})`);
|
|
297
|
+
return resolved;
|
|
298
|
+
}
|
|
299
|
+
/** Spec 25.4.2: validated, deduplicated allowances; undefined when nothing is requested. */
|
|
300
|
+
export function resolveSandbox(agent, s) {
|
|
301
|
+
if (!s)
|
|
302
|
+
return undefined;
|
|
303
|
+
const uniq = (xs) => [...new Set(xs)];
|
|
304
|
+
const write = uniq((s.write ?? []).map((p, i) => sandboxPath(agent, "write", i, p)));
|
|
305
|
+
const unixSockets = uniq((s.unix_sockets ?? []).map((p, i) => sandboxPath(agent, "unix_sockets", i, p)));
|
|
306
|
+
const network = uniq(s.network ?? []);
|
|
307
|
+
const localBinding = s.local_binding === true;
|
|
308
|
+
if (!write.length && !unixSockets.length && !network.length && !localBinding)
|
|
309
|
+
return undefined;
|
|
310
|
+
return { write, network, localBinding, unixSockets };
|
|
311
|
+
}
|
|
278
312
|
/** The resolved project plus, for every setting, the file and key it was taken from (or "default"). */
|
|
279
313
|
export function resolveProjectWithSources(home, name) {
|
|
280
314
|
const project = resolveProject(home, name);
|
|
@@ -361,6 +395,8 @@ export function flattenResolved(project) {
|
|
|
361
395
|
v[`${p}.allow_web`] = a.allowWeb;
|
|
362
396
|
if (a.fallback.length)
|
|
363
397
|
v[`${p}.fallback`] = a.fallback;
|
|
398
|
+
if (a.sandbox)
|
|
399
|
+
v[`${p}.sandbox`] = { write: a.sandbox.write, network: a.sandbox.network, local_binding: a.sandbox.localBinding, unix_sockets: a.sandbox.unixSockets };
|
|
364
400
|
if (a.tiers)
|
|
365
401
|
v[`${p}.model_tiers`] = a.tiers;
|
|
366
402
|
}
|
package/dist/fallback.d.ts
CHANGED
|
@@ -42,6 +42,12 @@ export declare class Cooldowns {
|
|
|
42
42
|
}
|
|
43
43
|
/** The candidates for one wake: the chain minus those cooling down, or the whole chain when every one is (something must be tried). */
|
|
44
44
|
export declare function planAttempts(all: readonly Candidate[], cooldowns: Cooldowns, now: number): Candidate[];
|
|
45
|
+
/**
|
|
46
|
+
* The candidates `planAttempts` left out because they are cooling down, soonest to recover first. They are tried last, after
|
|
47
|
+
* waiting for their cooldown to end, so a wake-up whose warm models all fail is not lost to a cooldown that ends moments later.
|
|
48
|
+
* A tier's cheaper model is not one of them: stepping back down to it after the ceiling failed would be wrong.
|
|
49
|
+
*/
|
|
50
|
+
export declare function deferredAttempts(all: readonly Candidate[], cooldowns: Cooldowns, now: number): Candidate[];
|
|
45
51
|
export type Step = "retry-same" | "next-candidate" | "stop";
|
|
46
52
|
/**
|
|
47
53
|
* What to do after a failed call (spec 20.2 decisions 2 and 5). `attemptsOnCurrent` counts the calls made on the current
|
|
@@ -80,6 +86,8 @@ export interface Tried {
|
|
|
80
86
|
export interface RunWithFallbackOptions<R extends AttemptResult> {
|
|
81
87
|
/** From `planAttempts`; tried in order. */
|
|
82
88
|
candidates: Candidate[];
|
|
89
|
+
/** From `deferredAttempts`: tried after `candidates`, each one only once its cooldown has ended. */
|
|
90
|
+
deferred?: Candidate[];
|
|
83
91
|
/** The agent has no fallback chain (see `nextStep`): no backoff, no cooldowns. */
|
|
84
92
|
legacy: boolean;
|
|
85
93
|
/** Retries per candidate (`dispatcher.retry`). */
|
|
@@ -93,6 +101,8 @@ export interface RunWithFallbackOptions<R extends AttemptResult> {
|
|
|
93
101
|
/** Runs after each call, awaited before the next decision. */
|
|
94
102
|
afterAttempt?(candidate: Candidate, result: R, ctx: AttemptCtx<R>): void | Promise<void>;
|
|
95
103
|
onSwitch?(from: Candidate, to: Candidate, kind: ErrorKind | undefined, result: R): void;
|
|
104
|
+
/** Runs before the loop sleeps `ms` for `candidate`'s cooldown to end. */
|
|
105
|
+
onCooldownWait?(candidate: Candidate, ms: number): void;
|
|
96
106
|
sleep?(ms: number, signal?: AbortSignal): Promise<void>;
|
|
97
107
|
now?(): number;
|
|
98
108
|
signal?: AbortSignal;
|
package/dist/fallback.js
CHANGED
|
@@ -75,12 +75,25 @@ export function planAttempts(all, cooldowns, now) {
|
|
|
75
75
|
const warm = all.filter((c) => cooldowns.active(c.key, now) === 0);
|
|
76
76
|
return warm.length ? warm : [...all];
|
|
77
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* The candidates `planAttempts` left out because they are cooling down, soonest to recover first. They are tried last, after
|
|
80
|
+
* waiting for their cooldown to end, so a wake-up whose warm models all fail is not lost to a cooldown that ends moments later.
|
|
81
|
+
* A tier's cheaper model is not one of them: stepping back down to it after the ceiling failed would be wrong.
|
|
82
|
+
*/
|
|
83
|
+
export function deferredAttempts(all, cooldowns, now) {
|
|
84
|
+
return all
|
|
85
|
+
.filter((c) => !c.tiered && cooldowns.active(c.key, now) > 0)
|
|
86
|
+
.sort((a, b) => cooldowns.active(a.key, now) - cooldowns.active(b.key, now));
|
|
87
|
+
}
|
|
78
88
|
/**
|
|
79
89
|
* What to do after a failed call (spec 20.2 decisions 2 and 5). `attemptsOnCurrent` counts the calls made on the current
|
|
80
90
|
* candidate, the failed one included; each candidate gets `retry` retries. `legacy` (the agent has no fallback chain)
|
|
81
91
|
* keeps the old loop: every kind is retried `retry` times, then the wake fails.
|
|
82
92
|
*/
|
|
83
93
|
export function nextStep(kind, attemptsOnCurrent, retry, hasNext, legacy) {
|
|
94
|
+
// A refused configuration cannot get better by trying again or on another model (spec 25.19.1).
|
|
95
|
+
if (kind === "config")
|
|
96
|
+
return "stop";
|
|
84
97
|
const retriesLeft = attemptsOnCurrent <= retry;
|
|
85
98
|
if (legacy)
|
|
86
99
|
return retriesLeft ? "retry-same" : "stop";
|
|
@@ -122,7 +135,8 @@ export function defaultSleep(ms, signal) {
|
|
|
122
135
|
* retry it or move on as `nextStep` says, back off between retries. Success and cancellation end the loop as they are.
|
|
123
136
|
*/
|
|
124
137
|
export async function runWithFallback(o) {
|
|
125
|
-
const
|
|
138
|
+
const list = [...o.candidates, ...(o.deferred ?? [])];
|
|
139
|
+
const first = list[0];
|
|
126
140
|
if (!first)
|
|
127
141
|
throw new Error("runWithFallback needs at least one candidate");
|
|
128
142
|
const sleep = o.sleep ?? defaultSleep;
|
|
@@ -142,7 +156,7 @@ export async function runWithFallback(o) {
|
|
|
142
156
|
tried.push({ runtime: used.runtime, model: used.model, kind: result.kind, error: result.error });
|
|
143
157
|
if (!o.legacy && (result.kind === "rate_limit" || result.kind === "quota"))
|
|
144
158
|
o.cooldowns.mark(used.key, result.kind, now(), o.cooldownSec);
|
|
145
|
-
const next =
|
|
159
|
+
const next = list[index + 1];
|
|
146
160
|
const step = nextStep(result.kind, ctx.candidateAttempt + 1, o.retry, !!next, o.legacy);
|
|
147
161
|
if (step === "stop")
|
|
148
162
|
return { result, tried, used };
|
|
@@ -157,6 +171,13 @@ export async function runWithFallback(o) {
|
|
|
157
171
|
}
|
|
158
172
|
const to = next;
|
|
159
173
|
o.onSwitch?.(used, to, result.kind, result);
|
|
174
|
+
const wait = o.legacy ? 0 : o.cooldowns.active(to.key, now());
|
|
175
|
+
if (wait > 0) {
|
|
176
|
+
o.onCooldownWait?.(to, wait);
|
|
177
|
+
await sleep(wait, o.signal);
|
|
178
|
+
if (o.signal?.aborted)
|
|
179
|
+
return { result, tried, used, stopped: "aborted" };
|
|
180
|
+
}
|
|
160
181
|
ctx = { attempt: ctx.attempt + 1, candidateAttempt: 0, previous: result, switchedFrom: used };
|
|
161
182
|
index++;
|
|
162
183
|
used = to;
|
package/dist/mailbox.js
CHANGED
|
@@ -27,14 +27,31 @@ export function serialize(meta, body) {
|
|
|
27
27
|
const fm = YAML.stringify(meta).trimEnd();
|
|
28
28
|
return `---\n${fm}\n---\n\n${body.trim()}\n`;
|
|
29
29
|
}
|
|
30
|
+
const ID_KEYS = new Set(["id", "reply_to", "thread", "about", "run_id"]);
|
|
30
31
|
export function parseRaw(text) {
|
|
31
32
|
const m = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/.exec(text);
|
|
32
33
|
if (!m)
|
|
33
34
|
throw new Error("missing frontmatter (--- ... ---)");
|
|
34
|
-
const
|
|
35
|
+
const fm = m[1] ?? "";
|
|
36
|
+
const data = YAML.parse(fm);
|
|
35
37
|
if (!data || typeof data !== "object" || Array.isArray(data))
|
|
36
38
|
throw new Error("frontmatter is not a mapping");
|
|
37
|
-
|
|
39
|
+
const out = data;
|
|
40
|
+
// Ids are 8 hex characters: `79e76911` parses as Infinity and `01234567` as 1234567.
|
|
41
|
+
// Only an id key that came out as a number is re-read from its source text; everything else stays as YAML.parse returned it.
|
|
42
|
+
if (Object.entries(out).some(([k, v]) => ID_KEYS.has(k) && typeof v === "number")) {
|
|
43
|
+
const doc = YAML.parseDocument(fm);
|
|
44
|
+
if (YAML.isMap(doc.contents)) {
|
|
45
|
+
for (const pair of doc.contents.items) {
|
|
46
|
+
const key = YAML.isScalar(pair.key) ? String(pair.key.value) : "";
|
|
47
|
+
if (!ID_KEYS.has(key) || typeof out[key] !== "number" || !YAML.isScalar(pair.value))
|
|
48
|
+
continue;
|
|
49
|
+
if (pair.value.type === "PLAIN" && typeof pair.value.source === "string")
|
|
50
|
+
out[key] = pair.value.source;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return { data: out, body: (m[2] ?? "").trim() };
|
|
38
55
|
}
|
|
39
56
|
export function ensureProjectDirs(project) {
|
|
40
57
|
fs.mkdirSync(path.join(project.paths.shared, "common"), { recursive: true });
|
package/dist/policy.d.ts
CHANGED
|
@@ -50,3 +50,36 @@ export declare function setOsSandboxForTest(available: boolean | undefined): voi
|
|
|
50
50
|
export declare function enforcementFor(project: ResolvedProject, agent: ResolvedAgent): AgentEnforcement;
|
|
51
51
|
/** Can `agent` legitimately change this protected file? */
|
|
52
52
|
export declare function mayEditProtected(project: ResolvedProject, agent: ResolvedAgent, file: string): boolean;
|
|
53
|
+
export type SandboxRelation = "equal" | "inside" | "contains";
|
|
54
|
+
export interface SandboxPathConflict {
|
|
55
|
+
key: "write" | "unix_sockets";
|
|
56
|
+
index: number;
|
|
57
|
+
path: string;
|
|
58
|
+
protectedPath: string;
|
|
59
|
+
reason: string;
|
|
60
|
+
relation: SandboxRelation;
|
|
61
|
+
}
|
|
62
|
+
/** Pure (spec 25.4.3): which canonical sandbox paths equal, sit inside or contain a protected path. Callers canonicalize. */
|
|
63
|
+
export declare function sandboxPathConflicts(entries: {
|
|
64
|
+
key: "write" | "unix_sockets";
|
|
65
|
+
index: number;
|
|
66
|
+
path: string;
|
|
67
|
+
}[], protectedPaths: {
|
|
68
|
+
path: string;
|
|
69
|
+
reason: string;
|
|
70
|
+
}[]): SandboxPathConflict[];
|
|
71
|
+
/** What a runtime really applies of an agent's allowances (spec 25.9): the tuple a fallback must not exceed. */
|
|
72
|
+
export interface SandboxCaps {
|
|
73
|
+
network: "none" | "domains" | "all";
|
|
74
|
+
localBinding: boolean;
|
|
75
|
+
unixSockets: boolean;
|
|
76
|
+
}
|
|
77
|
+
export declare const NETWORK_RANK: {
|
|
78
|
+
readonly none: 0;
|
|
79
|
+
readonly domains: 1;
|
|
80
|
+
readonly all: 2;
|
|
81
|
+
};
|
|
82
|
+
/** Without an OS sandbox nothing is applied, so every runtime is equal there. */
|
|
83
|
+
export declare function sandboxCaps(agent: ResolvedAgent, runtime?: string | undefined): SandboxCaps;
|
|
84
|
+
/** Components in which `backup` applies more than `primary`, as `name: primary -> backup`. */
|
|
85
|
+
export declare function sandboxCapExcess(primary: SandboxCaps, backup: SandboxCaps): string[];
|
package/dist/policy.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { findOnPath } from "./fs-util.js";
|
|
4
|
+
import { canonicalPath } from "./paths.js";
|
|
4
5
|
export function commonFile(project) {
|
|
5
6
|
return path.join(project.paths.shared, "common", "COMMON.md");
|
|
6
7
|
}
|
|
@@ -88,6 +89,8 @@ export function writePolicy(project, agent) {
|
|
|
88
89
|
...(ws ? [ws.gitDir] : []),
|
|
89
90
|
...(shared && mayWriteShared ? [shared] : []),
|
|
90
91
|
...(isLead ? [recordsDir(project)] : []),
|
|
92
|
+
// Spec 25: canonical, because the OS sandbox compares real paths (`/tmp` is `/private/tmp` on macOS).
|
|
93
|
+
...(agent.sandbox?.write ?? []).map(canonicalPath),
|
|
91
94
|
];
|
|
92
95
|
const allowFiles = [];
|
|
93
96
|
if (isLead)
|
|
@@ -180,6 +183,8 @@ export function enforcementFor(project, agent) {
|
|
|
180
183
|
if (agent.canEditAgentMd)
|
|
181
184
|
notes.push("can_edit_agent_md widens Codex writable roots to whole agent directories.");
|
|
182
185
|
}
|
|
186
|
+
if (agent.sandbox)
|
|
187
|
+
notes.push(...sandboxNotes(agent, os));
|
|
183
188
|
return {
|
|
184
189
|
agent: agent.name,
|
|
185
190
|
runtime: agent.runtime ?? "?",
|
|
@@ -188,7 +193,82 @@ export function enforcementFor(project, agent) {
|
|
|
188
193
|
notes,
|
|
189
194
|
};
|
|
190
195
|
}
|
|
196
|
+
/** Doctor lines for an agent's sandbox allowances (spec 25.7, 25.10): what is requested and what its runtime really applies. */
|
|
197
|
+
function sandboxNotes(agent, os) {
|
|
198
|
+
const s = agent.sandbox;
|
|
199
|
+
if (!s)
|
|
200
|
+
return [];
|
|
201
|
+
const claude = agent.runtime === "claude-code";
|
|
202
|
+
const notes = [];
|
|
203
|
+
if (!os) {
|
|
204
|
+
notes.push(claude
|
|
205
|
+
? "no OS sandbox; sandbox.write changes Edit tool rules only; network/local_binding/unix_sockets have no effect."
|
|
206
|
+
: "no OS sandbox; enforcement is prompt-only; sandbox allowances provide no OS enforcement.");
|
|
207
|
+
}
|
|
208
|
+
if (s.network.length) {
|
|
209
|
+
notes.push(`network: ${s.network.join(", ")} (${claude ? "claude-code: per-domain" : "codex: all hosts"})`);
|
|
210
|
+
if (!claude)
|
|
211
|
+
notes.push(`warn: sandbox.network is all-or-nothing on Codex: this agent may reach any host, not only ${s.network.join(", ")}.`);
|
|
212
|
+
}
|
|
213
|
+
if (s.localBinding) {
|
|
214
|
+
notes.push(claude ? (process.platform === "darwin" ? "local_binding: on" : "local_binding: requested, no effect on this platform (macOS only)") : "local_binding: requested, not supported by codex");
|
|
215
|
+
if (!claude)
|
|
216
|
+
notes.push("warn: sandbox.local_binding has no Codex setting; whether a workspace-write sandbox lets it bind ports is unverified. sandbox.network turns on all network access, not only binding.");
|
|
217
|
+
}
|
|
218
|
+
if (s.unixSockets.length) {
|
|
219
|
+
notes.push(`unix_sockets: ${s.unixSockets.map(canonicalPath).join(", ")} (${claude ? "applied" : "not applied by codex"})`);
|
|
220
|
+
if (!claude)
|
|
221
|
+
notes.push("warn: sandbox.unix_sockets has no per-socket Codex control; no setting emitted.");
|
|
222
|
+
}
|
|
223
|
+
for (const [key, list] of [["write", s.write], ["unix_sockets", s.unixSockets]]) {
|
|
224
|
+
list.forEach((p, i) => {
|
|
225
|
+
if (!fs.existsSync(p))
|
|
226
|
+
notes.push(`info: sandbox.${key}[${i}] ${p} does not exist yet`);
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
return notes;
|
|
230
|
+
}
|
|
191
231
|
/** Can `agent` legitimately change this protected file? */
|
|
192
232
|
export function mayEditProtected(project, agent, file) {
|
|
193
233
|
return writePolicy(project, agent).allowFiles.includes(file);
|
|
194
234
|
}
|
|
235
|
+
const within = (parent, p) => p === parent || p.startsWith(parent.endsWith(path.sep) ? parent : parent + path.sep);
|
|
236
|
+
/** Pure (spec 25.4.3): which canonical sandbox paths equal, sit inside or contain a protected path. Callers canonicalize. */
|
|
237
|
+
export function sandboxPathConflicts(entries, protectedPaths) {
|
|
238
|
+
const out = [];
|
|
239
|
+
for (const e of entries) {
|
|
240
|
+
for (const pp of protectedPaths) {
|
|
241
|
+
const relation = e.path === pp.path ? "equal" : within(pp.path, e.path) ? "inside" : within(e.path, pp.path) ? "contains" : undefined;
|
|
242
|
+
if (relation)
|
|
243
|
+
out.push({ ...e, protectedPath: pp.path, reason: pp.reason, relation });
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
return out;
|
|
247
|
+
}
|
|
248
|
+
export const NETWORK_RANK = { none: 0, domains: 1, all: 2 };
|
|
249
|
+
/** Without an OS sandbox nothing is applied, so every runtime is equal there. */
|
|
250
|
+
export function sandboxCaps(agent, runtime = agent.runtime) {
|
|
251
|
+
const s = agent.sandbox;
|
|
252
|
+
const none = { network: "none", localBinding: false, unixSockets: false };
|
|
253
|
+
if (!s || !osSandboxAvailable())
|
|
254
|
+
return none;
|
|
255
|
+
if (runtime === "claude-code") {
|
|
256
|
+
return {
|
|
257
|
+
network: s.network.length ? "domains" : "none",
|
|
258
|
+
localBinding: s.localBinding && process.platform === "darwin",
|
|
259
|
+
unixSockets: s.unixSockets.length > 0,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
return { ...none, network: s.network.length ? "all" : "none" };
|
|
263
|
+
}
|
|
264
|
+
/** Components in which `backup` applies more than `primary`, as `name: primary -> backup`. */
|
|
265
|
+
export function sandboxCapExcess(primary, backup) {
|
|
266
|
+
const out = [];
|
|
267
|
+
if (NETWORK_RANK[backup.network] > NETWORK_RANK[primary.network])
|
|
268
|
+
out.push(`network: ${primary.network} -> ${backup.network}`);
|
|
269
|
+
if (backup.localBinding && !primary.localBinding)
|
|
270
|
+
out.push("local_binding: off -> on");
|
|
271
|
+
if (backup.unixSockets && !primary.unixSockets)
|
|
272
|
+
out.push("unix_sockets: off -> on");
|
|
273
|
+
return out;
|
|
274
|
+
}
|
package/dist/project-lock.d.ts
CHANGED
|
@@ -44,6 +44,8 @@ export interface PidStartIo {
|
|
|
44
44
|
}
|
|
45
45
|
/** A start time that identifies one process: `proc:<ticks>` on Linux, the `ps` text elsewhere or when /proc cannot be read. */
|
|
46
46
|
export declare function pidStartFrom(pid: number, io: PidStartIo): string | undefined;
|
|
47
|
+
/** True when both strings name the same process start. Locale-specific `ps` text for one instant matches. */
|
|
48
|
+
export declare function samePidStart(a: string, b: string): boolean;
|
|
47
49
|
/**
|
|
48
50
|
* Is the process that wrote this lock still the same running process? Judged by pid and its start time,
|
|
49
51
|
* never by hostname (macOS changes it with the network) or by heartbeat age (a busy holder may be late).
|
package/dist/project-lock.js
CHANGED
|
@@ -50,7 +50,8 @@ const realIo = {
|
|
|
50
50
|
}
|
|
51
51
|
},
|
|
52
52
|
ps: (pid) => {
|
|
53
|
-
|
|
53
|
+
// C locale: `ps` start times stay in one language. A lock written from a Chinese terminal would otherwise look like a different process.
|
|
54
|
+
const r = spawnSync("ps", ["-o", "lstart=", "-p", String(pid)], { encoding: "utf8", env: { ...process.env, LANG: "C", LC_ALL: "C" } });
|
|
54
55
|
const out = r.status === 0 ? r.stdout.trim() : "";
|
|
55
56
|
return out || undefined;
|
|
56
57
|
},
|
|
@@ -60,6 +61,30 @@ function pidStart(pid) {
|
|
|
60
61
|
}
|
|
61
62
|
/** Two start times can only be compared when they were taken the same way. */
|
|
62
63
|
const sameKind = (a, b) => a.startsWith("proc:") === b.startsWith("proc:");
|
|
64
|
+
const MONTHS = { Jan: "01", Feb: "02", Mar: "03", Apr: "04", May: "05", Jun: "06", Jul: "07", Aug: "08", Sep: "09", Oct: "10", Nov: "11", Dec: "12" };
|
|
65
|
+
/** `ps -o lstart` in the C locale, or the same instant from a Chinese macOS locale, as `YYYY-MM-DDTHH:mm:ss`. */
|
|
66
|
+
function canonicalLstart(text) {
|
|
67
|
+
const s = text.trim();
|
|
68
|
+
const en = /^[A-Za-z]{3}\s+([A-Za-z]{3})\s+(\d{1,2})\s+(\d{2}):(\d{2}):(\d{2})\s+(\d{4})$/.exec(s);
|
|
69
|
+
if (en) {
|
|
70
|
+
const month = MONTHS[en[1] ?? ""];
|
|
71
|
+
if (!month)
|
|
72
|
+
return undefined;
|
|
73
|
+
return `${en[6]}-${month}-${(en[2] ?? "").padStart(2, "0")}T${en[3]}:${en[4]}:${en[5]}`;
|
|
74
|
+
}
|
|
75
|
+
const zh = /^\S+\s+(\d{1,2})月\/\s*(\d{1,2})\s+(\d{2}):(\d{2}):(\d{2})\s+(\d{4})$/.exec(s);
|
|
76
|
+
if (zh)
|
|
77
|
+
return `${zh[6]}-${(zh[1] ?? "").padStart(2, "0")}-${(zh[2] ?? "").padStart(2, "0")}T${zh[3]}:${zh[4]}:${zh[5]}`;
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
/** True when both strings name the same process start. Locale-specific `ps` text for one instant matches. */
|
|
81
|
+
export function samePidStart(a, b) {
|
|
82
|
+
if (a === b)
|
|
83
|
+
return true;
|
|
84
|
+
const left = canonicalLstart(a);
|
|
85
|
+
const right = canonicalLstart(b);
|
|
86
|
+
return !!left && left === right;
|
|
87
|
+
}
|
|
63
88
|
/**
|
|
64
89
|
* Is the process that wrote this lock still the same running process? Judged by pid and its start time,
|
|
65
90
|
* never by hostname (macOS changes it with the network) or by heartbeat age (a busy holder may be late).
|
|
@@ -74,7 +99,7 @@ export function lockHolderAlive(info) {
|
|
|
74
99
|
}
|
|
75
100
|
if (info.pid_started) {
|
|
76
101
|
const now = pidStart(info.pid);
|
|
77
|
-
if (now && sameKind(now, info.pid_started) &&
|
|
102
|
+
if (now && sameKind(now, info.pid_started) && !samePidStart(info.pid_started, now))
|
|
78
103
|
return false;
|
|
79
104
|
}
|
|
80
105
|
return true;
|
package/dist/quota.d.ts
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type QuotaWindow = {
|
|
2
|
+
label: string;
|
|
3
|
+
used_percent: number;
|
|
4
|
+
remaining_percent: number;
|
|
5
|
+
resets: string;
|
|
6
|
+
};
|
|
7
|
+
export type QuotaReport = {
|
|
8
|
+
runtime: "claude" | "codex";
|
|
9
|
+
windows: QuotaWindow[];
|
|
10
|
+
error?: string;
|
|
11
|
+
};
|
|
12
|
+
export declare function parseClaudeUsage(text: string): QuotaWindow[];
|
|
13
|
+
type LimitWindow = {
|
|
14
|
+
usedPercent?: number;
|
|
15
|
+
windowDurationMins?: number;
|
|
16
|
+
resetsAt?: number;
|
|
17
|
+
};
|
|
18
|
+
type RateLimits = {
|
|
19
|
+
primary?: LimitWindow | null;
|
|
20
|
+
secondary?: LimitWindow | null;
|
|
21
|
+
};
|
|
22
|
+
export declare function codexWindows(limits: RateLimits): QuotaWindow[];
|
|
23
|
+
export declare function claudeQuota(): Promise<QuotaReport>;
|
|
24
|
+
export declare function codexQuota(): Promise<QuotaReport>;
|
|
25
|
+
export {};
|