@centerforagenticai/pi-multi-account 0.1.1
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/LICENSE +21 -0
- package/NOTICE +29 -0
- package/README.md +999 -0
- package/config/models/pi-multi-account.v1.json +32 -0
- package/config/subscription-plans.v1.json +122 -0
- package/package.json +76 -0
- package/packages/pi-anthropic-oauth/LICENSE +21 -0
- package/packages/pi-anthropic-oauth/package.json +54 -0
- package/packages/pi-anthropic-oauth/src/auth.ts +396 -0
- package/packages/pi-anthropic-oauth/src/context.ts +116 -0
- package/packages/pi-anthropic-oauth/src/convert.ts +303 -0
- package/packages/pi-anthropic-oauth/src/index.ts +37 -0
- package/packages/pi-anthropic-oauth/src/prompt.ts +137 -0
- package/packages/pi-anthropic-oauth/src/stream.ts +476 -0
- package/packages/pi-antigravity/LICENSE +21 -0
- package/packages/pi-antigravity/package.json +77 -0
- package/packages/pi-antigravity/src/auth/index.ts +14 -0
- package/packages/pi-antigravity/src/auth/oauth.ts +442 -0
- package/packages/pi-antigravity/src/client/client.ts +561 -0
- package/packages/pi-antigravity/src/client/index.ts +1 -0
- package/packages/pi-antigravity/src/context.ts +110 -0
- package/packages/pi-antigravity/src/diagnostics/diagnostics.ts +96 -0
- package/packages/pi-antigravity/src/diagnostics/index.ts +1 -0
- package/packages/pi-antigravity/src/image/image.ts +336 -0
- package/packages/pi-antigravity/src/image/index.ts +1 -0
- package/packages/pi-antigravity/src/index.ts +280 -0
- package/packages/pi-antigravity/src/models/discovery.ts +154 -0
- package/packages/pi-antigravity/src/models/grouping.ts +424 -0
- package/packages/pi-antigravity/src/models/index.ts +3 -0
- package/packages/pi-antigravity/src/models/models.ts +500 -0
- package/packages/pi-antigravity/src/stream/index.ts +1 -0
- package/packages/pi-antigravity/src/stream/stream.ts +1478 -0
- package/packages/pi-antigravity/src/types/enums.ts +42 -0
- package/packages/pi-antigravity/src/types/index.ts +2 -0
- package/packages/pi-antigravity/src/types/types.ts +292 -0
- package/packages/pi-antigravity/src/usage/index.ts +1 -0
- package/packages/pi-antigravity/src/usage/usage.ts +416 -0
- package/packages/pi-antigravity/src/utils/http.ts +91 -0
- package/packages/pi-antigravity/src/utils/index.ts +3 -0
- package/packages/pi-antigravity/src/utils/security.ts +73 -0
- package/packages/pi-antigravity/src/utils/util.ts +132 -0
- package/scripts/multi-account.mjs +44 -0
- package/src/account-labels.ts +223 -0
- package/src/account-plan-assignment.ts +340 -0
- package/src/account-rate-history.ts +372 -0
- package/src/anthropic-adaptive-stream.ts +531 -0
- package/src/anthropic-alias-stream.ts +140 -0
- package/src/anthropic-context-compat.ts +80 -0
- package/src/api-pricing.ts +579 -0
- package/src/bounded-file-lines.ts +97 -0
- package/src/catalog-rebinding.ts +177 -0
- package/src/catalog-registration-probe.ts +111 -0
- package/src/codex-adapter.ts +345 -0
- package/src/codex-model-defaults.ts +785 -0
- package/src/command-completions.ts +404 -0
- package/src/commands.ts +2000 -0
- package/src/compaction.ts +14 -0
- package/src/config.ts +1317 -0
- package/src/continuation.ts +569 -0
- package/src/cooldowns.ts +110 -0
- package/src/cost-digest-store.ts +332 -0
- package/src/cost-digest.ts +1044 -0
- package/src/cost-history.ts +251 -0
- package/src/cost-period-closer.ts +160 -0
- package/src/cost-report-json.ts +318 -0
- package/src/cost-report-reader.ts +368 -0
- package/src/cost-report-render.ts +207 -0
- package/src/cost-report.ts +1104 -0
- package/src/coverage-attestation.ts +397 -0
- package/src/credential-lifecycle.ts +169 -0
- package/src/credential-refresh.ts +248 -0
- package/src/declaration-notice-marker.ts +238 -0
- package/src/diagnostic-store.ts +276 -0
- package/src/diagnostics.ts +309 -0
- package/src/discovery.ts +471 -0
- package/src/duration.ts +13 -0
- package/src/error-classification.ts +256 -0
- package/src/fuzzy.ts +15 -0
- package/src/group-policy.ts +81 -0
- package/src/history-store.ts +897 -0
- package/src/index.ts +5572 -0
- package/src/lifecycle.ts +378 -0
- package/src/logical-dispatch.ts +279 -0
- package/src/logical-model-selector.ts +254 -0
- package/src/logical-model-switcher.ts +430 -0
- package/src/logical-provider-attribution.ts +544 -0
- package/src/logical-provider.ts +1237 -0
- package/src/logical-route-indicator.ts +215 -0
- package/src/machine-lease.ts +445 -0
- package/src/model-support.ts +66 -0
- package/src/models-declaration.ts +1091 -0
- package/src/openai-adapter.ts +117 -0
- package/src/openrouter-budget.ts +304 -0
- package/src/openrouter-fallback.ts +146 -0
- package/src/period-boundaries.ts +376 -0
- package/src/pi-anthropic-oauth.d.ts +6 -0
- package/src/preflight.ts +253 -0
- package/src/pricing-cache.ts +235 -0
- package/src/project-identity.ts +100 -0
- package/src/provider-registration.ts +942 -0
- package/src/rate-formula.ts +163 -0
- package/src/recovery-engine.ts +853 -0
- package/src/recovery-output.ts +837 -0
- package/src/recovery-plan.ts +239 -0
- package/src/report-range.ts +203 -0
- package/src/route-resolver.ts +789 -0
- package/src/routing-config-transaction.ts +232 -0
- package/src/routing.ts +1163 -0
- package/src/runtime-state.ts +630 -0
- package/src/session-account-groups.ts +284 -0
- package/src/session-restore.ts +287 -0
- package/src/shared-usage.ts +1392 -0
- package/src/standalone-cli.ts +720 -0
- package/src/status-view.ts +578 -0
- package/src/subscription-plan-catalog.ts +346 -0
- package/src/tier-model-resolver.ts +46 -0
- package/src/upstream-anthropic.ts +315 -0
- package/src/upstream-antigravity.ts +327 -0
- package/src/usage-fetch.ts +1634 -0
- package/src/usage.ts +1026 -0
- package/src/vendor.ts +87 -0
- package/src/warmer.ts +231 -0
- package/src/watchdog.ts +219 -0
- package/src/window-history.ts +270 -0
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import type { CooldownReason, NumericServerHint } from "./runtime-state.js";
|
|
2
|
+
|
|
3
|
+
export const PROVIDER_ERROR_CODES = [
|
|
4
|
+
"rate_limit",
|
|
5
|
+
"quota_exhausted",
|
|
6
|
+
"invalid_api_key",
|
|
7
|
+
"invalid_token",
|
|
8
|
+
"token_expired",
|
|
9
|
+
"token_revoked",
|
|
10
|
+
"oauth_refresh_rejected",
|
|
11
|
+
"oauth_service_unavailable",
|
|
12
|
+
"insufficient_permissions",
|
|
13
|
+
"model_not_found",
|
|
14
|
+
"unsupported_api_version",
|
|
15
|
+
"invalid_request",
|
|
16
|
+
] as const;
|
|
17
|
+
export type ProviderErrorCode = (typeof PROVIDER_ERROR_CODES)[number];
|
|
18
|
+
|
|
19
|
+
export const TRANSPORT_FAILURE_KINDS = [
|
|
20
|
+
"connection-timeout",
|
|
21
|
+
"dns-failure",
|
|
22
|
+
"socket-reset",
|
|
23
|
+
"incomplete-response",
|
|
24
|
+
] as const;
|
|
25
|
+
export type TransportFailureKind = (typeof TRANSPORT_FAILURE_KINDS)[number];
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* A deliberately narrow, structured projection of an upstream failure. Raw
|
|
29
|
+
* errors, messages, headers, request/response bodies, and credentials must be
|
|
30
|
+
* sanitized/parsed before crossing this boundary.
|
|
31
|
+
*/
|
|
32
|
+
export interface ProviderFailureSignal {
|
|
33
|
+
readonly httpStatus?: number;
|
|
34
|
+
readonly code?: ProviderErrorCode;
|
|
35
|
+
/** Bounded model id from the current request, when the host exposes it. */
|
|
36
|
+
readonly modelId?: string;
|
|
37
|
+
readonly transportKind?: TransportFailureKind;
|
|
38
|
+
readonly retryAfterSeconds?: number;
|
|
39
|
+
readonly resetAtMs?: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export type FailureCategory =
|
|
43
|
+
| "quota-rate-limit"
|
|
44
|
+
| "terminal-auth"
|
|
45
|
+
| "transient-auth"
|
|
46
|
+
| "permission"
|
|
47
|
+
| "config"
|
|
48
|
+
| "transport"
|
|
49
|
+
| "unknown";
|
|
50
|
+
|
|
51
|
+
export interface FailureClassification {
|
|
52
|
+
readonly category: FailureCategory;
|
|
53
|
+
readonly accountAction:
|
|
54
|
+
| "cooldown-and-route"
|
|
55
|
+
| "invalidate-and-route"
|
|
56
|
+
| "route-without-retry";
|
|
57
|
+
readonly cooldownReason?: CooldownReason;
|
|
58
|
+
readonly serverHint?: NumericServerHint;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const RATE_LIMIT_CODES: ReadonlySet<ProviderErrorCode> = new Set([
|
|
62
|
+
"rate_limit",
|
|
63
|
+
"quota_exhausted",
|
|
64
|
+
]);
|
|
65
|
+
const TERMINAL_AUTH_CODES: ReadonlySet<ProviderErrorCode> = new Set([
|
|
66
|
+
"invalid_api_key",
|
|
67
|
+
"invalid_token",
|
|
68
|
+
"token_expired",
|
|
69
|
+
"token_revoked",
|
|
70
|
+
"oauth_refresh_rejected",
|
|
71
|
+
]);
|
|
72
|
+
const CONFIG_CODES: ReadonlySet<ProviderErrorCode> = new Set([
|
|
73
|
+
"model_not_found",
|
|
74
|
+
"unsupported_api_version",
|
|
75
|
+
"invalid_request",
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
const STRUCTURED_PROVIDER_ERROR_CODES: Readonly<
|
|
79
|
+
Record<string, ProviderErrorCode>
|
|
80
|
+
> = Object.freeze({
|
|
81
|
+
rate_limit_error: "rate_limit",
|
|
82
|
+
overloaded_error: "rate_limit",
|
|
83
|
+
quota_exhausted: "quota_exhausted",
|
|
84
|
+
authentication_error: "invalid_token",
|
|
85
|
+
permission_error: "insufficient_permissions",
|
|
86
|
+
not_found_error: "model_not_found",
|
|
87
|
+
invalid_request_error: "invalid_request",
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Fixed, bounded messages emitted by pi-ai's Codex wrapper when the upstream
|
|
92
|
+
* SSE error has no HTTP response event for extensions to observe. Keep this an
|
|
93
|
+
* exact allowlist: arbitrary provider prose must never become routing state.
|
|
94
|
+
* Sarrius independently treats `usage limit` as exhaustion; the narrower exact
|
|
95
|
+
* projection here preserves that behavior without retaining the raw message.
|
|
96
|
+
*/
|
|
97
|
+
const CODEX_QUOTA_EXHAUSTED_MESSAGES: ReadonlySet<string> = new Set([
|
|
98
|
+
"Codex error: The usage limit has been reached",
|
|
99
|
+
"Codex error: The usage limit has been reached.",
|
|
100
|
+
]);
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Extracts a bounded, allow-listed provider error code from a structured SSE
|
|
104
|
+
* error payload. Anthropic can open a stream with HTTP 200 and later emit
|
|
105
|
+
* `event: error` whose JSON becomes `AssistantMessage.errorMessage`; without
|
|
106
|
+
* this projection a real `rate_limit_error` is misclassified as unknown.
|
|
107
|
+
*
|
|
108
|
+
* JSON objects shaped as `{ type: "error", error: { type } }` are accepted.
|
|
109
|
+
* The exact fixed Codex wrapper message for subscription exhaustion is also
|
|
110
|
+
* projected because that SSE failure exposes no response status to extensions.
|
|
111
|
+
* Raw messages, request ids, details, and credentials never cross the
|
|
112
|
+
* classification boundary.
|
|
113
|
+
*
|
|
114
|
+
* The envelope is NOT always the whole string. Pi prefixes the HTTP status and
|
|
115
|
+
* may wrap the result again after exhausting its retries, so the same upstream
|
|
116
|
+
* rate limit arrives as any of:
|
|
117
|
+
*
|
|
118
|
+
* {"type":"error",...} (bare envelope)
|
|
119
|
+
* 429 {"type":"error",...} (status-prefixed)
|
|
120
|
+
* Retry failed after 3 attempts: 429 {"type":"error",…} (retry-wrapped)
|
|
121
|
+
*
|
|
122
|
+
* Parsing only the bare form silently lost the other two. Observed live on
|
|
123
|
+
* 2026-08-05: four `429 {"type":"error","error":{"type":"rate_limit_error"}}`
|
|
124
|
+
* messages classified as "transient or unknown", so `settleTurn` logged
|
|
125
|
+
* `routing.advisory` and never switched account -- the exact failure this
|
|
126
|
+
* projection exists to prevent, reintroduced by a leading `429 `.
|
|
127
|
+
*
|
|
128
|
+
* The scan therefore starts at the first `{`. Everything before it is Pi's own
|
|
129
|
+
* framing, never provider content.
|
|
130
|
+
*/
|
|
131
|
+
export function providerErrorCodeFromMessage(
|
|
132
|
+
errorMessage: unknown,
|
|
133
|
+
): ProviderErrorCode | undefined {
|
|
134
|
+
if (typeof errorMessage !== "string" || errorMessage.length === 0) {
|
|
135
|
+
return undefined;
|
|
136
|
+
}
|
|
137
|
+
// Provider error envelopes are small. Refuse oversized/unbounded diagnostic
|
|
138
|
+
// strings rather than parsing arbitrary assistant output.
|
|
139
|
+
if (errorMessage.length > 4_096) return undefined;
|
|
140
|
+
|
|
141
|
+
if (CODEX_QUOTA_EXHAUSTED_MESSAGES.has(errorMessage.trim())) {
|
|
142
|
+
return "quota_exhausted";
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const envelopeStart = errorMessage.indexOf("{");
|
|
146
|
+
if (envelopeStart < 0) return undefined;
|
|
147
|
+
let parsed: unknown;
|
|
148
|
+
try {
|
|
149
|
+
parsed = JSON.parse(errorMessage.slice(envelopeStart));
|
|
150
|
+
} catch {
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
if (typeof parsed !== "object" || parsed === null) return undefined;
|
|
154
|
+
const envelope = parsed as Record<string, unknown>;
|
|
155
|
+
if (envelope.type !== "error") return undefined;
|
|
156
|
+
if (typeof envelope.error !== "object" || envelope.error === null) {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
const providerType = (envelope.error as Record<string, unknown>).type;
|
|
160
|
+
if (typeof providerType !== "string") return undefined;
|
|
161
|
+
return STRUCTURED_PROVIDER_ERROR_CODES[providerType];
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function numericServerHint(
|
|
165
|
+
signal: ProviderFailureSignal,
|
|
166
|
+
): NumericServerHint | undefined {
|
|
167
|
+
const retryAfterSeconds =
|
|
168
|
+
typeof signal.retryAfterSeconds === "number" &&
|
|
169
|
+
Number.isSafeInteger(signal.retryAfterSeconds) &&
|
|
170
|
+
signal.retryAfterSeconds >= 0
|
|
171
|
+
? signal.retryAfterSeconds
|
|
172
|
+
: undefined;
|
|
173
|
+
const resetAtMs =
|
|
174
|
+
typeof signal.resetAtMs === "number" &&
|
|
175
|
+
Number.isFinite(signal.resetAtMs) &&
|
|
176
|
+
signal.resetAtMs >= 0
|
|
177
|
+
? signal.resetAtMs
|
|
178
|
+
: undefined;
|
|
179
|
+
if (retryAfterSeconds === undefined && resetAtMs === undefined)
|
|
180
|
+
return undefined;
|
|
181
|
+
return {
|
|
182
|
+
...(retryAfterSeconds === undefined ? {} : { retryAfterSeconds }),
|
|
183
|
+
...(resetAtMs === undefined ? {} : { resetAtMs }),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function hasCode(
|
|
188
|
+
signal: ProviderFailureSignal,
|
|
189
|
+
codes: ReadonlySet<ProviderErrorCode>,
|
|
190
|
+
): boolean {
|
|
191
|
+
return signal.code !== undefined && codes.has(signal.code);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function isRateLimited(signal: ProviderFailureSignal): boolean {
|
|
195
|
+
return signal.httpStatus === 429 || hasCode(signal, RATE_LIMIT_CODES);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function isTerminalAuth(signal: ProviderFailureSignal): boolean {
|
|
199
|
+
return signal.httpStatus === 401 || hasCode(signal, TERMINAL_AUTH_CODES);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function isPermissionFailure(signal: ProviderFailureSignal): boolean {
|
|
203
|
+
return (
|
|
204
|
+
signal.code === "insufficient_permissions" || signal.httpStatus === 403
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function isTransportFailure(signal: ProviderFailureSignal): boolean {
|
|
209
|
+
if (signal.transportKind !== undefined) return true;
|
|
210
|
+
return signal.httpStatus !== undefined && signal.httpStatus >= 500;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function cooldown(
|
|
214
|
+
category: FailureCategory,
|
|
215
|
+
cooldownReason: CooldownReason,
|
|
216
|
+
signal: ProviderFailureSignal,
|
|
217
|
+
): FailureClassification {
|
|
218
|
+
const serverHint = numericServerHint(signal);
|
|
219
|
+
return {
|
|
220
|
+
category,
|
|
221
|
+
accountAction: "cooldown-and-route",
|
|
222
|
+
cooldownReason,
|
|
223
|
+
...(serverHint === undefined ? {} : { serverHint }),
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Classifies from structured status/code/type only; error.message is never inspected. */
|
|
228
|
+
export function classifyFailure(
|
|
229
|
+
signal: ProviderFailureSignal,
|
|
230
|
+
): FailureClassification {
|
|
231
|
+
if (isRateLimited(signal)) {
|
|
232
|
+
return cooldown("quota-rate-limit", "rate-limit", signal);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (isTerminalAuth(signal)) {
|
|
236
|
+
return { category: "terminal-auth", accountAction: "invalidate-and-route" };
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (signal.code === "oauth_service_unavailable") {
|
|
240
|
+
return cooldown("transient-auth", "auth-transient", signal);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (isPermissionFailure(signal)) {
|
|
244
|
+
return cooldown("permission", "permission", signal);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
if (hasCode(signal, CONFIG_CODES)) {
|
|
248
|
+
return { category: "config", accountAction: "route-without-retry" };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (isTransportFailure(signal)) {
|
|
252
|
+
return cooldown("transport", "transport", signal);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return cooldown("unknown", "unknown", signal);
|
|
256
|
+
}
|
package/src/fuzzy.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thin project-owned re-export of the host `fuzzyMatch` matcher.
|
|
3
|
+
*
|
|
4
|
+
* The completion policy imports `fuzzyMatch` through this module rather than
|
|
5
|
+
* directly from `@earendil-works/pi-tui` for one reason: the pi-tui namespace
|
|
6
|
+
* export is non-configurable and its module namespace is frozen, so a test
|
|
7
|
+
* cannot spy the matcher at the pi-tui boundary. Routing the single logical
|
|
8
|
+
* haystack call through this project module lets `M-COMP-FUZZY-ID` inspect the
|
|
9
|
+
* exact matcher argument — the only way to prove the standalone bare-id
|
|
10
|
+
* haystack field survives, because a bare id is always a subsequence of both
|
|
11
|
+
* prefixed fields and so cannot be distinguished by a match/no-match query.
|
|
12
|
+
*
|
|
13
|
+
* This module adds no behavior; it re-exports the host matcher unchanged.
|
|
14
|
+
*/
|
|
15
|
+
export { fuzzyMatch } from "@earendil-works/pi-tui";
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { realpathSync } from "node:fs";
|
|
2
|
+
import { resolve } from "node:path";
|
|
3
|
+
import type { MultiAccountConfig } from "./config.js";
|
|
4
|
+
|
|
5
|
+
export type AccountGroupResolutionSource =
|
|
6
|
+
| "session-override"
|
|
7
|
+
| "cwd-default"
|
|
8
|
+
| "global-default"
|
|
9
|
+
| "unrestricted";
|
|
10
|
+
|
|
11
|
+
export type EffectiveAccountGroupResolution =
|
|
12
|
+
| {
|
|
13
|
+
readonly groupId: string;
|
|
14
|
+
readonly source: Exclude<AccountGroupResolutionSource, "unrestricted">;
|
|
15
|
+
}
|
|
16
|
+
| {
|
|
17
|
+
readonly source: "unrestricted";
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type AccountGroupPolicyConfig = Pick<
|
|
21
|
+
MultiAccountConfig,
|
|
22
|
+
"accountGroupCwdDefaults" | "defaultAccountGroup"
|
|
23
|
+
>;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Canonicalize only the supplied directory. This deliberately does not inspect
|
|
27
|
+
* Git metadata or call projectKeyForCwd/resolveProjectRoot, because linked
|
|
28
|
+
* worktrees that share a git-common-dir remain distinct exact directories.
|
|
29
|
+
*/
|
|
30
|
+
export function canonicalizeAccountGroupCwd(cwd: string): string {
|
|
31
|
+
const absolute = resolve(cwd);
|
|
32
|
+
try {
|
|
33
|
+
return realpathSync.native(absolute);
|
|
34
|
+
} catch {
|
|
35
|
+
return absolute;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Resolve one session's effective group in strict precedence order. Both the live
|
|
41
|
+
* cwd and each operator-authored absolute cwd key are realpath-canonicalized,
|
|
42
|
+
* then compared byte-for-byte. Subtrees and linked worktrees therefore never
|
|
43
|
+
* inherit a parent or shared-repository default.
|
|
44
|
+
*/
|
|
45
|
+
export function resolveEffectiveAccountGroup(
|
|
46
|
+
config: AccountGroupPolicyConfig,
|
|
47
|
+
cwd: string,
|
|
48
|
+
sessionOverride?: string,
|
|
49
|
+
): EffectiveAccountGroupResolution {
|
|
50
|
+
if (sessionOverride !== undefined) {
|
|
51
|
+
return Object.freeze({
|
|
52
|
+
groupId: sessionOverride,
|
|
53
|
+
source: "session-override" as const,
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const canonicalCwd = canonicalizeAccountGroupCwd(cwd);
|
|
58
|
+
let cwdDefault: string | undefined;
|
|
59
|
+
for (const [configuredCwd, groupId] of Object.entries(
|
|
60
|
+
config.accountGroupCwdDefaults ?? {},
|
|
61
|
+
)) {
|
|
62
|
+
if (canonicalizeAccountGroupCwd(configuredCwd) !== canonicalCwd) continue;
|
|
63
|
+
if (cwdDefault !== undefined && cwdDefault !== groupId) {
|
|
64
|
+
throw new Error(
|
|
65
|
+
"Conflicting account-group defaults canonicalize to the same exact cwd.",
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
cwdDefault = groupId;
|
|
69
|
+
}
|
|
70
|
+
if (cwdDefault !== undefined) {
|
|
71
|
+
return Object.freeze({ groupId: cwdDefault, source: "cwd-default" as const });
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (config.defaultAccountGroup !== undefined) {
|
|
75
|
+
return Object.freeze({
|
|
76
|
+
groupId: config.defaultAccountGroup,
|
|
77
|
+
source: "global-default" as const,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
return Object.freeze({ source: "unrestricted" as const });
|
|
81
|
+
}
|