Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions apps/server/src/provider/Drivers/ClaudeDriver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -320,17 +320,21 @@ export const ClaudeDriver: ProviderDriver<ClaudeSettings, ClaudeDriverEnv, Path.
: undefined;
if (!grantId || !current.version) return "noCredit" as const;
const version = current.version;
// The grant was read for this organization; the claim refuses a login that has since moved.
const organization = current.auth.workspaceId;
return yield* resetCreditCoordinator.redeem(
configDir,
(requestId) =>
ClaudeResetCredits.consumeClaudeResetCredit({
configDir,
accountConfigPath,
version,
expectedOrganization: organization,
grantId,
requestId,
}),
ClaudeResetCredits.isSettledClaudeResetCreditFailure,
`${organization ?? ""}\n${grantId}`,
);
},
Effect.provideService(HttpClient.HttpClient, httpClient),
Expand Down
89 changes: 89 additions & 0 deletions apps/server/src/provider/ProviderInstanceRegistry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,9 @@ const layerTestHttpClient = Layer.succeed(

// The mock CLIs from makeTildeProviderFixtures are scripts Windows cannot execute.
const windowsHost = HostProcess.Platform.defaultValue() === "win32";
const decodeClaimRequest = Schema.decodeUnknownSync(
Schema.fromJsonString(Schema.Struct({ request_id: Schema.String })),
);

const TEST_EPOCH = DateTime.makeUnsafe("1970-01-01T00:00:00.000Z");

Expand Down Expand Up @@ -789,6 +792,92 @@ describe("ProviderInstanceRegistry — multi-instance codex slice", () => {
}),
);

it.live.skipIf(windowsHost)(
"claims a Claude reset only in the organization its grant was read under, even queued",
() =>
Effect.gen(function* () {
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const fixtures = yield* makeTildeProviderFixtures();
const signIn = (organizationUuid: string) =>
fs.writeFileString(
path.join(fixtures.claudeHomePath, ".claude.json"),
JSON.stringify({ oauthAccount: { organizationUuid } }),
);
yield* fs.writeFileString(
path.join(fixtures.claudeHomePath, ".credentials.json"),
'{"claudeAiOauth":{"accessToken":"fake-token"}}',
);
yield* signIn("org-a");
const posted = yield* Deferred.make<void>();
const answer = yield* Deferred.make<number>();
const claims: Array<{ url: string; requestId: string }> = [];
const client = HttpClient.make((request) =>
Effect.gen(function* () {
if (request.url.endsWith("/api/oauth/usage")) {
return HttpClientResponse.fromWeb(
request,
Response.json({
cedar_ember: {
eligible: true,
next_grant_id: "grant_a",
grants: [{ id: "grant_a", resets_left: 2, usable_now: true }],
},
}),
);
}
if (request.url.endsWith("/reset_rate_limits")) {
const body =
request.body._tag === "Uint8Array"
? new TextDecoder().decode(request.body.body)
: "";
claims.push({ url: request.url, requestId: decodeClaimRequest(body).request_id });
if (claims.length === 1) yield* Deferred.succeed(posted, undefined);
const status = claims.length === 1 ? yield* Deferred.await(answer) : 200;
return HttpClientResponse.fromWeb(
request,
Response.json({ result: "reset" }, { status }),
);
}
return HttpClientResponse.fromWeb(request, Response.json({ version: "0.0.0" }));
}),
);
const instanceId = ProviderInstanceId.make("claude_moved");
const { registry } = yield* makeProviderInstanceRegistry({
drivers: [ClaudeDriver],
configMap: {
[instanceId]: {
driver: ProviderDriverKind.make("claudeAgent"),
enabled: true,
config: makeClaudeConfig({
enabled: true,
binaryPath: fixtures.claudeBinaryPath,
homePath: fixtures.claudeHomePath,
}),
},
},
}).pipe(Effect.provideService(HttpClient.HttpClient, client));
const instance = (yield* registry.getInstance(instanceId))!;
expect((yield* instance.snapshot.refresh).auth.workspaceId).toBe("org-a");
const consume = instance.consumeResetCredit!(undefined).pipe(Effect.result);
const unanswered = yield* consume.pipe(Effect.forkChild);
yield* Deferred.await(posted);
// Claude signs in to another organization while the shown grant's claim waits.
yield* signIn("org-b");
const queued = yield* consume.pipe(Effect.forkChild);
yield* Deferred.succeed(answer, 503);
const results = [yield* Fiber.join(unanswered), yield* Fiber.join(queued)];
yield* signIn("org-a");
const retry = yield* consume;

const sentTo = "https://api.anthropic.com/api/organizations/org-a/reset_rate_limits";
const first = { url: sentTo, requestId: claims[0]?.requestId };
expect(claims).toEqual([first, first]);
expect(results.map((result) => result._tag)).toEqual(["Failure", "Failure"]);
expect(retry).toMatchObject({ _tag: "Success", success: "reset" });
}).pipe(Effect.provideService(HostProcess.Platform, "linux"), Effect.provide(layerTest)),
);

const makeClaudeCacheFixture = Effect.fnUntraced(function* (
state: "ready" | "signed-out" | "usage-failed",
) {
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/provider/claudeResetCredits.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,7 @@ const consume = (client: HttpClient.HttpClient, ids = { grantId: "grant_a", requ
return yield* ClaudeResetCredits.consumeClaudeResetCredit({
...login,
version: "2.1.0",
expectedOrganization: "org-1",
...ids,
}).pipe(
Effect.provideService(HostProcess.Platform, "linux"),
Expand Down
132 changes: 97 additions & 35 deletions apps/server/src/provider/claudeResetCredits.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,15 @@ const ClaimResponse = Schema.Struct({
"unavailable",
]),
});
const decodeClaimResponse = Schema.decodeUnknownEffect(Schema.fromJsonString(ClaimResponse));

const RESET_CREDIT_FAILURES = {
malformedCredit: "Claude returned a malformed reset credit.",
loginUnreadable: "Claude could not read its login.",
accountUnreadable: "Claude could not read its account.",
signedOut: "Sign in to Claude again to redeem resets.",
accountChanged:
"The Claude login changed since this reset was shown. Refresh Limits and try again.",
rateLimited: "Claude is rate limiting resets. Try again soon.",
coolingDown: "Claude resets are cooling down. Try again later.",
unconfirmed:
Expand Down Expand Up @@ -256,65 +259,124 @@ const CLAIM_OUTCOMES = {
ineligible: "noCredit",
} as const satisfies Record<string, ProviderConsumeResetCreditOutcome>;

/** Claude Code's claim of one grant for an organization; requestId is its idempotency key. */
export const claudeResetClaim = Effect.fn("claudeResetClaim")(function* (input: {
/** The login's organization, read under the account's claim lock. */
readonly organization: string | undefined;
readonly expectedOrganization: string | undefined;
readonly grantId: string;
readonly requestId: string;
}) {
if (!GRANT_ID.test(input.grantId) || !REQUEST_ID.test(input.requestId)) {
return yield* new ClaudeResetCreditError({ reason: "malformedCredit" });
}
const organization = input.organization?.trim();
if (!organization) return yield* new ClaudeResetCreditError({ reason: "signedOut" });
if (organization !== input.expectedOrganization) {
return yield* new ClaudeResetCreditError({ reason: "accountChanged" });
}
return {
url: new URL(
`/api/organizations/${encodeURIComponent(organization)}/reset_rate_limits`,
API_BASE,
).toString(),
body: { program: PROGRAM, grant_id: input.grantId, request_id: input.requestId },
};
});

const ORGANIZATION_HEADER = "anthropic-organization-id";
const ORGANIZATION_ID = /^[A-Za-z0-9-]{1,128}$/;

/**
* The organization a Claude response names in its headers, matched by any case and as a string
* or an array of values: undefined when it names none, null when it names one unreadably.
*/
export function claudeOrganizationHeader(header: unknown): string | null | undefined {
if (typeof header !== "object" || header === null) return undefined;
const values = Object.entries(header)
.filter(([name]) => name.toLowerCase() === ORGANIZATION_HEADER)
.flatMap(([, value]) => (Array.isArray(value) ? value : [value]));
if (values.length === 0) return undefined;
const [first] = values;
return typeof first === "string" &&
ORGANIZATION_ID.test(first) &&
values.every((value) => value === first)
? first
: null;
}

/**
* Claude's answer to a claim, given its HTTP status and body text. An answer naming another
* organization than the claim's confirms nothing, so its request ID stays for a retry.
*/
export const claudeResetClaimOutcome = Effect.fn("claudeResetClaimOutcome")(function* (
status: number,
body: string,
receipt?: {
readonly expected: string | undefined;
readonly answeredFor: string | null | undefined;
},
) {
if (status === 429) return yield* new ClaudeResetCreditError({ reason: "rateLimited" });
if (status === 401 || status === 403) {
return yield* new ClaudeResetCreditError({ reason: "signedOut" });
}
if (status < 200 || status >= 300) {
return yield* new ClaudeResetCreditError({ reason: "requestFailed" });
}
if (receipt && receipt.answeredFor !== undefined && receipt.answeredFor !== receipt.expected) {
return yield* new ClaudeResetCreditError({ reason: "unconfirmed" });
}
const answer = yield* decodeClaimResponse(body).pipe(
Effect.mapError((cause) => new ClaudeResetCreditError({ reason: "requestFailed", cause })),
);
if (answer.result === "cooldown") {
return yield* new ClaudeResetCreditError({ reason: "coolingDown" });
}
// Unknown outcomes retain the request ID so retries address the same claim.
if (answer.result === "unavailable") {
return yield* new ClaudeResetCreditError({ reason: "unconfirmed" });
}
return CLAIM_OUTCOMES[answer.result];
});

/** requestId is the claim's idempotency key. */
export const consumeClaudeResetCredit = Effect.fn("consumeClaudeResetCredit")(function* (input: {
readonly configDir: string;
readonly accountConfigPath: string;
readonly version: string;
readonly expectedOrganization: string | undefined;
readonly grantId: string;
readonly requestId: string;
}) {
if (!GRANT_ID.test(input.grantId) || !REQUEST_ID.test(input.requestId)) {
return yield* new ClaudeResetCreditError({ reason: "malformedCredit" });
}
const token = yield* readAccessToken(input.configDir).pipe(
Effect.mapError((cause) => new ClaudeResetCreditError({ reason: "loginUnreadable", cause })),
);
const config = yield* readJson(Config, input.accountConfigPath).pipe(
Effect.mapError((cause) => new ClaudeResetCreditError({ reason: "accountUnreadable", cause })),
);
const organization = config.oauthAccount?.organizationUuid?.trim();
if (!token || !organization) {
return yield* new ClaudeResetCreditError({ reason: "signedOut" });
}
const claim = yield* claudeResetClaim({
organization: config.oauthAccount?.organizationUuid,
expectedOrganization: input.expectedOrganization,
grantId: input.grantId,
requestId: input.requestId,
});
if (!token) return yield* new ClaudeResetCreditError({ reason: "signedOut" });
const client = yield* HttpClient.HttpClient;
const response = yield* client
.execute(
HttpClientRequest.post(
new URL(
`/api/organizations/${encodeURIComponent(organization)}/reset_rate_limits`,
API_BASE,
),
).pipe(
HttpClientRequest.post(claim.url).pipe(
withClaudeHeaders(token, input.version),
HttpClientRequest.bodyJsonUnsafe({
program: PROGRAM,
grant_id: input.grantId,
request_id: input.requestId,
}),
HttpClientRequest.bodyJsonUnsafe(claim.body),
),
)
.pipe(
Effect.timeout("25 seconds"),
Effect.mapError((cause) => new ClaudeResetCreditError({ reason: "requestFailed", cause })),
);
if (response.status === 429) {
return yield* new ClaudeResetCreditError({ reason: "rateLimited" });
}
if (response.status === 401 || response.status === 403) {
return yield* new ClaudeResetCreditError({ reason: "signedOut" });
}
const body = yield* HttpClientResponse.filterStatusOk(response).pipe(
Effect.flatMap(HttpClientResponse.schemaBodyJson(ClaimResponse)),
const body = yield* response.text.pipe(
Effect.timeout("25 seconds"),
Effect.mapError((cause) => new ClaudeResetCreditError({ reason: "requestFailed", cause })),
Effect.orElseSucceed(() => ""),
);
if (body.result === "cooldown") {
return yield* new ClaudeResetCreditError({ reason: "coolingDown" });
}
// Unknown outcomes retain the request ID so retries address the same claim.
if (body.result === "unavailable") {
return yield* new ClaudeResetCreditError({ reason: "unconfirmed" });
}
return CLAIM_OUTCOMES[body.result];
return yield* claudeResetClaimOutcome(response.status, body);
});
50 changes: 50 additions & 0 deletions apps/server/src/provider/resetCreditCoordinator.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,56 @@ describe("ResetCreditCoordinator", () => {
}).pipe(Effect.provide(ResetCreditCoordinator.layerTest)),
);

it.effect("keeps an unanswered claim's key when its retry is refused, until an answer", () =>
Effect.gen(function* () {
const { redeem } = yield* ResetCreditCoordinator.ResetCreditCoordinator;
const keys = yield* Ref.make<ReadonlyArray<string>>([]);
const answers = yield* Ref.make<ReadonlyArray<"lost" | "rateLimited" | "reset">>([
"lost",
"rateLimited",
"reset",
"reset",
]);
const consume = (key: string) =>
Effect.gen(function* () {
yield* Ref.update(keys, (seen) => [...seen, key]);
const [answer] = yield* Ref.getAndUpdate(answers, ([, ...rest]) => rest);
return answer === "reset" ? answer : yield* Effect.fail(answer!);
});
const isSettled = (error: "lost" | "rateLimited") => error === "rateLimited";

for (let attempt = 0; attempt < 4; attempt += 1) {
yield* redeem("acct", consume, isSettled).pipe(Effect.result);
}

const [lost, refused, answered, next] = yield* Ref.get(keys);
assert.strictEqual(refused, lost);
assert.strictEqual(answered, lost);
assert.notStrictEqual(next, lost);
}).pipe(Effect.provide(ResetCreditCoordinator.layerTest)),
);

it.effect("never gives one action's unanswered key to another action on the account", () =>
Effect.gen(function* () {
const { redeem } = yield* ResetCreditCoordinator.ResetCreditCoordinator;
const keys = yield* Ref.make<ReadonlyArray<string>>([]);
const consume = (outcome: "lost" | "reset") => (key: string) =>
Effect.gen(function* () {
yield* Ref.update(keys, (seen) => [...seen, key]);
if (outcome === "lost") return yield* Effect.fail(outcome);
return outcome;
});

yield* redeem("acct", consume("lost"), undefined, "weekly").pipe(Effect.result);
yield* redeem("acct", consume("reset"), undefined, "fable");
yield* redeem("acct", consume("reset"), undefined, "weekly");

const [weekly, fable, weeklyRetry] = yield* Ref.get(keys);
assert.notStrictEqual(fable, weekly);
assert.strictEqual(weeklyRetry, weekly);
}).pipe(Effect.provide(ResetCreditCoordinator.layerTest)),
);

it.effect("serialises concurrent redemptions on the same account, not per caller", () =>
Effect.gen(function* () {
const { redeem } = yield* ResetCreditCoordinator.ResetCreditCoordinator;
Expand Down
Loading
Loading