@bitkyc08/opencodex 2.43.0 → 2.44.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 +25 -17
- package/gui/dist/assets/index-B7_K1Hsj.js +115 -0
- package/gui/dist/assets/index-ltx3L-WS.css +1 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/adapters/command-code.ts +23 -2
- package/src/adapters/cursor/native-exec.ts +63 -11
- package/src/adapters/cursor/tool-definitions.ts +1 -1
- package/src/adapters/cursor/tool-schemas.ts +59 -0
- package/src/adapters/kiro.ts +54 -8
- package/src/adapters/openai-chat.ts +23 -6
- package/src/adapters/openai-responses.ts +3 -1
- package/src/adapters/opencode-go.ts +35 -0
- package/src/claude/agents-inject.ts +12 -5
- package/src/claude/desktop-3p-library.ts +89 -0
- package/src/claude/desktop-3p.ts +166 -95
- package/src/claude/desktop-discovery-inputs.ts +44 -0
- package/src/claude/desktop-profile.ts +1 -1
- package/src/claude/desktop-remote-store-artifact.ts +185 -0
- package/src/claude/desktop-remote-store-io.ts +91 -0
- package/src/claude/desktop-remote-store-state.ts +127 -0
- package/src/claude/desktop-remote-store.ts +269 -0
- package/src/claude/inbound-model-options.ts +10 -2
- package/src/claude/inbound-records.ts +7 -0
- package/src/claude/inbound.ts +1 -1
- package/src/claude/model-info.ts +16 -3
- package/src/cli/account-api.ts +7 -1
- package/src/cli/aside-profiles.ts +17 -0
- package/src/cli/capabilities.ts +39 -0
- package/src/cli/claude-agent-startup-sync.ts +3 -1
- package/src/cli/claude-desktop.ts +127 -18
- package/src/cli/connect.ts +27 -14
- package/src/cli/dispatch.ts +30 -16
- package/src/cli/export-command.ts +3 -1
- package/src/cli/index.ts +28 -16
- package/src/cli/integrations.ts +46 -12
- package/src/cli/opencode.ts +11 -4
- package/src/cli/runtime-api.ts +7 -2
- package/src/cli/uninstall-client-state.ts +78 -0
- package/src/client/connect.ts +476 -223
- package/src/client/hub-client.ts +81 -0
- package/src/client/hub-relay.ts +38 -6
- package/src/client/lifecycle-lock.ts +129 -0
- package/src/client/state.ts +71 -42
- package/src/clients/aside-profiles.ts +224 -0
- package/src/codex/account-lifecycle.ts +5 -9
- package/src/codex/auth-api.ts +50 -4
- package/src/codex/catalog/metadata.ts +0 -2
- package/src/codex/catalog/provider-fetch.ts +7 -5
- package/src/codex/catalog/sync.ts +88 -24
- package/src/codex/convergence.ts +2 -0
- package/src/codex/inject.ts +45 -14
- package/src/codex/journal.ts +6 -6
- package/src/codex/quota-refresh-outcome.ts +27 -0
- package/src/codex/shim.ts +7 -1
- package/src/combos/resolve.ts +10 -7
- package/src/config.ts +18 -5
- package/src/generated/compatibility-version.json +209 -105
- package/src/images/loop.ts +5 -2
- package/src/integrations/aside-profile-context.ts +270 -0
- package/src/integrations/aside-profile-journal.ts +229 -0
- package/src/integrations/aside-profiles.ts +176 -0
- package/src/integrations/catalog-refresh.ts +40 -0
- package/src/integrations/config-io.ts +16 -1
- package/src/integrations/owned-refresh.ts +14 -2
- package/src/integrations/state.ts +3 -1
- package/src/integrations/writer.ts +3 -1
- package/src/lab/fabric/producer-isolate.ts +84 -46
- package/src/lib/account-selection-events.ts +32 -0
- package/src/lib/errors.ts +9 -1
- package/src/lib/provider-outbound.ts +2 -45
- package/src/lib/proxy-env.ts +67 -0
- package/src/oauth/account-quota-rank.ts +9 -1
- package/src/oauth/anthropic-routing.ts +91 -23
- package/src/oauth/generic-account-failover.ts +15 -53
- package/src/oauth/index.ts +67 -68
- package/src/oauth/pool-settings-capability.ts +2 -1
- package/src/oauth/store.ts +91 -7
- package/src/oauth/types.ts +8 -0
- package/src/providers/api-key-selection.ts +110 -0
- package/src/providers/api-keys.ts +38 -31
- package/src/providers/context-cap.ts +27 -3
- package/src/providers/key-failover.ts +27 -17
- package/src/providers/provider-id-rewrite.ts +10 -8
- package/src/providers/registry.ts +1 -0
- package/src/responses/custom-tool-compat.ts +10 -0
- package/src/responses/function-call-compat.ts +173 -0
- package/src/responses/namespace-tool-compat.ts +27 -4
- package/src/responses/parser.ts +8 -0
- package/src/responses/task-input.ts +36 -0
- package/src/responses/tool-name-aliases.ts +79 -0
- package/src/router.ts +2 -0
- package/src/routing/capability.ts +10 -3
- package/src/routing/compatibility/assemble.ts +16 -1
- package/src/routing/evaluator.ts +5 -1
- package/src/server/auth-cors.ts +3 -0
- package/src/server/chat-native.ts +44 -9
- package/src/server/claude-messages.ts +71 -36
- package/src/server/grok-responses-snapshot-repair.ts +333 -0
- package/src/server/index.ts +26 -10
- package/src/server/management/account-selection-stream.ts +70 -0
- package/src/server/management/aside-profile-routes.ts +231 -0
- package/src/server/management/config-routes.ts +36 -41
- package/src/server/management/context.ts +3 -0
- package/src/server/management/integration-routes.ts +61 -3
- package/src/server/management/logs-usage-routes.ts +1 -0
- package/src/server/management/model-routes.ts +20 -9
- package/src/server/management/model-rows.ts +10 -2
- package/src/server/management/oauth-account-routes.ts +7 -0
- package/src/server/management/provider-routes.ts +10 -8
- package/src/server/management/route-registry.ts +20 -2
- package/src/server/management-auth.ts +16 -0
- package/src/server/relay-eager.ts +17 -5
- package/src/server/relay.ts +43 -4
- package/src/server/request-log.ts +29 -1
- package/src/server/responses/agent-task-recovery-cache.ts +11 -0
- package/src/server/responses/agent-task-recovery.ts +24 -4
- package/src/server/responses/codex-ws-pool.ts +4 -4
- package/src/server/responses/codex-ws-session.ts +2 -2
- package/src/server/responses/collaboration.ts +2 -1
- package/src/server/responses/combo-stream-preflight.ts +26 -7
- package/src/server/responses/core.ts +668 -151
- package/src/server/responses/fetch-helpers.ts +6 -1
- package/src/server/responses/ws-upstream.ts +8 -3
- package/src/server/responses-custom-tool-repair.ts +29 -9
- package/src/server/responses-function-tool-repair.ts +183 -0
- package/src/server/responses-snapshot-codec.ts +14 -0
- package/src/server/responses-snapshot-repair.ts +1 -13
- package/src/server/responses-undeclared-tool-guard.ts +1 -68
- package/src/server/sse-payload-rewrite.ts +3 -1
- package/src/service.ts +6 -2
- package/src/types/config.ts +23 -16
- package/src/types/provider.ts +14 -3
- package/src/types/request.ts +2 -0
- package/src/usage/log.ts +9 -0
- package/src/web-search/loop.ts +5 -2
- package/gui/dist/assets/index-DS1NE4Jn.css +0 -1
- package/gui/dist/assets/index-Djowl68T.js +0 -112
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import type { AsideProfile } from "../clients/aside-profiles";
|
|
2
|
+
import { asideHomeDir } from "../clients/config-export";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import type { OwnedIntegrationRefreshOutcome } from "./owned-refresh";
|
|
5
|
+
import { readIntegrationState, type IntegrationState, type IntegrationStatus } from "./state";
|
|
6
|
+
import {
|
|
7
|
+
applyIntegrationCoordinated, disableIntegrationCoordinated,
|
|
8
|
+
overwriteIntegrationCoordinated, refreshIntegrationCoordinated,
|
|
9
|
+
} from "./writer";
|
|
10
|
+
import {
|
|
11
|
+
asideProfileEnabled, asideProfileFailure, asideProfileScope, asideWriteInput,
|
|
12
|
+
asideRootStore, createAsideProfileContext, persistAsidePolicy, runAsideProfileAction, selectAsideProfiles,
|
|
13
|
+
type AsideProfileContext, type AsideProfilesInput, type AsideProfileWriteOutcome,
|
|
14
|
+
} from "./aside-profile-context";
|
|
15
|
+
|
|
16
|
+
export { AsideProfileError } from "./aside-profile-context";
|
|
17
|
+
export type { AsideProfilesInput, AsideProfileWriteOutcome } from "./aside-profile-context";
|
|
18
|
+
|
|
19
|
+
export interface AsideProfileState extends IntegrationStatus {
|
|
20
|
+
profileId: number;
|
|
21
|
+
name?: string;
|
|
22
|
+
current: boolean;
|
|
23
|
+
enabled: boolean;
|
|
24
|
+
error?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface AsideProfileList extends IntegrationStatus {
|
|
28
|
+
profiles: AsideProfileState[];
|
|
29
|
+
allEnabled: boolean;
|
|
30
|
+
enabledCount: number;
|
|
31
|
+
appliedCount: number;
|
|
32
|
+
total: number;
|
|
33
|
+
error?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface AsideProfileMutationResult {
|
|
37
|
+
ok: boolean;
|
|
38
|
+
clientId: "aside";
|
|
39
|
+
changed: boolean;
|
|
40
|
+
state: IntegrationState;
|
|
41
|
+
message: string;
|
|
42
|
+
results: AsideProfileWriteOutcome[];
|
|
43
|
+
/** Preserve the ordinary refusal serializer for a single selected profile. */
|
|
44
|
+
result?: AsideProfileWriteOutcome;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function aggregateState(states: readonly IntegrationState[]): IntegrationState {
|
|
48
|
+
if (states.includes("unsafe")) return "unsafe";
|
|
49
|
+
if (states.includes("conflict")) return "conflict";
|
|
50
|
+
if (states.every(state => state === "absent")) return "absent";
|
|
51
|
+
return states.every(state => state === "current") ? "current" : "stale";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function profileState(ctx: AsideProfileContext, profile: AsideProfile): Promise<AsideProfileState> {
|
|
55
|
+
const metadata = {
|
|
56
|
+
profileId: profile.id, ...(profile.name !== undefined ? { name: profile.name } : {}),
|
|
57
|
+
current: profile.current, enabled: asideProfileEnabled(ctx, profile.id),
|
|
58
|
+
};
|
|
59
|
+
try {
|
|
60
|
+
const scope = asideProfileScope(ctx, profile);
|
|
61
|
+
const input = await asideWriteInput(ctx, scope);
|
|
62
|
+
return { ...readIntegrationState(input), ...metadata };
|
|
63
|
+
} catch (error) {
|
|
64
|
+
return {
|
|
65
|
+
clientId: "aside", ...metadata, state: "unsafe", installed: false,
|
|
66
|
+
configPath: profile.configPath, reason: "unresolvable-path", snapshotCount: -1,
|
|
67
|
+
retentionDegraded: true, error: asideProfileFailure(profile.id, error).message,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export async function listAsideProfileStates(input: AsideProfilesInput): Promise<AsideProfileList> {
|
|
73
|
+
let ctx: AsideProfileContext;
|
|
74
|
+
try { ctx = createAsideProfileContext(input); }
|
|
75
|
+
catch (error) {
|
|
76
|
+
return {
|
|
77
|
+
clientId: "aside", profiles: [], total: 0, enabledCount: 0, appliedCount: 0, allEnabled: false,
|
|
78
|
+
state: "unsafe", installed: false, configPath: join(asideHomeDir(input.env, input.home), "u"),
|
|
79
|
+
snapshotCount: -1, retentionDegraded: true, reason: "unresolvable-path",
|
|
80
|
+
error: asideProfileFailure(0, error).message,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
const profiles: AsideProfileState[] = [];
|
|
84
|
+
for (const profile of ctx.profiles) profiles.push(await profileState(ctx, profile));
|
|
85
|
+
const enabledCount = profiles.filter(profile => profile.enabled).length;
|
|
86
|
+
const snapshotCount = profiles.some(profile => profile.snapshotCount < 0) ? -1
|
|
87
|
+
: profiles.reduce((sum, profile) => sum + profile.snapshotCount, 0);
|
|
88
|
+
return {
|
|
89
|
+
clientId: "aside", profiles, total: profiles.length, enabledCount,
|
|
90
|
+
allEnabled: profiles.length > 0 && enabledCount === profiles.length,
|
|
91
|
+
appliedCount: profiles.filter(profile => profile.state === "current" || profile.state === "stale").length,
|
|
92
|
+
state: aggregateState(profiles.map(profile => profile.state)),
|
|
93
|
+
installed: profiles.some(profile => profile.installed),
|
|
94
|
+
configPath: profiles.find(profile => profile.current)?.configPath ?? profiles[0]?.configPath ?? "",
|
|
95
|
+
snapshotCount, retentionDegraded: profiles.some(profile => profile.retentionDegraded),
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export async function getAsideProfileState(input: AsideProfilesInput, id: number): Promise<AsideProfileState> {
|
|
100
|
+
const ctx = createAsideProfileContext(input);
|
|
101
|
+
return profileState(ctx, selectAsideProfiles(ctx, id)[0]!);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function mutateAsideProfiles(
|
|
105
|
+
input: AsideProfilesInput,
|
|
106
|
+
change: { enabled: boolean; profileId?: number; overwriteConflict?: boolean },
|
|
107
|
+
): Promise<AsideProfileMutationResult> {
|
|
108
|
+
return runAsideProfileAction<AsideProfileMutationResult>(input, change.profileId, `${change.enabled ? "enable" : "disable"}:${Boolean(change.overwriteConflict)}`, async (ctx, profiles) => {
|
|
109
|
+
const refused = new Map<number, AsideProfileWriteOutcome>();
|
|
110
|
+
for (const profile of profiles) {
|
|
111
|
+
try { asideProfileScope(ctx, profile); }
|
|
112
|
+
catch (error) { refused.set(profile.id, asideProfileFailure(profile.id, error)); }
|
|
113
|
+
}
|
|
114
|
+
// This await precedes model loading, writer preflight, snapshots and all client writes.
|
|
115
|
+
await persistAsidePolicy(ctx, change);
|
|
116
|
+
const results: AsideProfileWriteOutcome[] = [];
|
|
117
|
+
for (const profile of profiles) {
|
|
118
|
+
const refusal = refused.get(profile.id);
|
|
119
|
+
if (refusal) { results.push(refusal); continue; }
|
|
120
|
+
try {
|
|
121
|
+
const scope = asideProfileScope(ctx, profile);
|
|
122
|
+
const bound = await asideWriteInput(ctx, scope);
|
|
123
|
+
const operation = !change.enabled ? disableIntegrationCoordinated
|
|
124
|
+
: change.overwriteConflict ? overwriteIntegrationCoordinated : applyIntegrationCoordinated;
|
|
125
|
+
results.push({ ...await operation(bound, { lockSeams: input.lockSeams }), profileId: profile.id });
|
|
126
|
+
} catch (error) { results.push(asideProfileFailure(profile.id, error)); }
|
|
127
|
+
}
|
|
128
|
+
const ok = results.every(result => result.ok);
|
|
129
|
+
return {
|
|
130
|
+
ok, clientId: "aside", changed: results.some(result => result.ok && result.changed),
|
|
131
|
+
state: aggregateState(results.map(result => result.state)),
|
|
132
|
+
message: ok ? "Aside profile preferences applied" : "Aside preferences saved; some profiles could not be updated",
|
|
133
|
+
results, ...(results.length === 1 ? { result: results[0] } : {}),
|
|
134
|
+
};
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function refreshAsideProfiles(input: AsideProfilesInput): Promise<Array<OwnedIntegrationRefreshOutcome & { profileId: number }>> {
|
|
139
|
+
const policy = input.config.asideProfileSync;
|
|
140
|
+
const selected = Object.values(policy?.profiles ?? {}).some(enabled => enabled === true);
|
|
141
|
+
if (!selected && (policy?.allProfiles === false
|
|
142
|
+
|| (policy?.allProfiles !== true && !asideRootStore(input).readRecords().aside))) return Promise.resolve([]);
|
|
143
|
+
return runAsideProfileAction(input, undefined, "refresh", async (ctx, profiles) => {
|
|
144
|
+
const outcomes: Array<OwnedIntegrationRefreshOutcome & { profileId: number }> = [];
|
|
145
|
+
for (const profile of profiles) {
|
|
146
|
+
if (!asideProfileEnabled(ctx, profile.id)) continue;
|
|
147
|
+
try {
|
|
148
|
+
const scope = asideProfileScope(ctx, profile);
|
|
149
|
+
const owned = scope.store.readRecords().aside !== undefined;
|
|
150
|
+
const bound = await asideWriteInput(ctx, scope);
|
|
151
|
+
// A surviving ownership record means a removed block stays removed.
|
|
152
|
+
// A newly discovered, enabled profile may receive its first safe apply.
|
|
153
|
+
const operation = owned ? refreshIntegrationCoordinated : applyIntegrationCoordinated;
|
|
154
|
+
const result = await operation(bound, { lockSeams: input.lockSeams });
|
|
155
|
+
outcomes.push({
|
|
156
|
+
client: "aside", profileId: profile.id, ok: result.ok,
|
|
157
|
+
...(result.ok ? { changed: result.changed } : {}),
|
|
158
|
+
...(!result.ok || result.state === "absent" ? { reason: result.message } : {}),
|
|
159
|
+
...(!result.ok ? {
|
|
160
|
+
refusalReason: result.reason, state: result.state,
|
|
161
|
+
...(result.snapshotPath ? { snapshotPath: result.snapshotPath } : {}),
|
|
162
|
+
...(result.residual ? { residual: true } : {}),
|
|
163
|
+
} : {}),
|
|
164
|
+
});
|
|
165
|
+
} catch (error) {
|
|
166
|
+
const failure = asideProfileFailure(profile.id, error);
|
|
167
|
+
outcomes.push({ client: "aside", profileId: profile.id, ok: false, reason: failure.message,
|
|
168
|
+
...(!failure.ok ? { refusalReason: failure.reason, state: failure.state,
|
|
169
|
+
...(failure.snapshotPath ? { snapshotPath: failure.snapshotPath } : {}),
|
|
170
|
+
...(failure.residual ? { residual: true } : {}) } : {}),
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return outcomes;
|
|
175
|
+
});
|
|
176
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { redactSecretString } from "../lib/redact";
|
|
2
|
+
import type { ExportModel } from "../clients/config-export";
|
|
3
|
+
import type { IntegrationClientId } from "./registry";
|
|
4
|
+
import {
|
|
5
|
+
refreshOwnedIntegration,
|
|
6
|
+
type OwnedIntegrationRefreshInput,
|
|
7
|
+
type OwnedIntegrationRefreshOutcome,
|
|
8
|
+
} from "./owned-refresh";
|
|
9
|
+
|
|
10
|
+
/** Refresh only previously connected clients; a refused file never blocks its peers. */
|
|
11
|
+
export async function refreshOwnedCatalogIntegrations(
|
|
12
|
+
input: Omit<OwnedIntegrationRefreshInput, "clientId">,
|
|
13
|
+
clientIds: readonly IntegrationClientId[] = ["pi", "aside"],
|
|
14
|
+
): Promise<OwnedIntegrationRefreshOutcome[]> {
|
|
15
|
+
let models: Promise<readonly ExportModel[]> | undefined;
|
|
16
|
+
const loadModels = () => models ??= Promise.resolve().then(() =>
|
|
17
|
+
typeof input.models === "function" ? input.models() : input.models);
|
|
18
|
+
const outcomes: OwnedIntegrationRefreshOutcome[] = [];
|
|
19
|
+
for (const clientId of clientIds) {
|
|
20
|
+
try {
|
|
21
|
+
if (clientId === "aside") {
|
|
22
|
+
const { refreshAsideProfiles } = await import("./aside-profiles");
|
|
23
|
+
outcomes.push(...await refreshAsideProfiles({ ...input, models: loadModels }));
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const result = await refreshOwnedIntegration({ ...input, clientId, models: loadModels });
|
|
27
|
+
if (result) outcomes.push(result);
|
|
28
|
+
} catch (error) {
|
|
29
|
+
const busy = error !== null && typeof error === "object"
|
|
30
|
+
&& "code" in error && error.code === "integration_mutation_busy";
|
|
31
|
+
outcomes.push({
|
|
32
|
+
client: clientId,
|
|
33
|
+
ok: false,
|
|
34
|
+
reason: busy ? "integration_mutation_busy"
|
|
35
|
+
: redactSecretString(error instanceof Error ? error.message : String(error)),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return outcomes;
|
|
40
|
+
}
|
|
@@ -162,7 +162,22 @@ export function parseConfig(text: string | null, format: ConfigFormat): unknown
|
|
|
162
162
|
* evidence is gone.
|
|
163
163
|
*/
|
|
164
164
|
if (/(^|[\s,[=])[-+]?(?:inf|nan)(?=[\s,\]]|$)/mi.test(text)) return PARSE_FAILED;
|
|
165
|
-
|
|
165
|
+
const document = Bun.TOML.parse(text);
|
|
166
|
+
// TOML date/time scalars are Temporal objects with toJSON methods.
|
|
167
|
+
// The merge layer JSON-clones documents, which silently turns these
|
|
168
|
+
// into strings. Refuse before either status or a writer can admit a
|
|
169
|
+
// lossy rewrite, including dates nested in arrays and inline tables.
|
|
170
|
+
const pending: unknown[] = [document];
|
|
171
|
+
while (pending.length > 0) {
|
|
172
|
+
const value = pending.pop();
|
|
173
|
+
if (value === null || typeof value !== "object") continue;
|
|
174
|
+
if (!Array.isArray(value)) {
|
|
175
|
+
const prototype = Object.getPrototypeOf(value);
|
|
176
|
+
if (prototype !== Object.prototype && prototype !== null) return PARSE_FAILED;
|
|
177
|
+
}
|
|
178
|
+
for (const child of Object.values(value)) pending.push(child);
|
|
179
|
+
}
|
|
180
|
+
return document;
|
|
166
181
|
}
|
|
167
182
|
}
|
|
168
183
|
} catch {
|
|
@@ -27,6 +27,8 @@ export interface OwnedIntegrationRefreshInput {
|
|
|
27
27
|
home?: string;
|
|
28
28
|
store?: IntegrationStateStore;
|
|
29
29
|
io?: IntegrationIO;
|
|
30
|
+
/** Internal profile target selected before entering the coordinated writer. */
|
|
31
|
+
resolvedPaths?: { configPath: string; detectDir: string };
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
export interface OwnedIntegrationRefreshOutcome {
|
|
@@ -34,6 +36,11 @@ export interface OwnedIntegrationRefreshOutcome {
|
|
|
34
36
|
readonly ok: boolean;
|
|
35
37
|
readonly changed?: boolean;
|
|
36
38
|
readonly reason?: string;
|
|
39
|
+
readonly profileId?: number;
|
|
40
|
+
readonly refusalReason?: string;
|
|
41
|
+
readonly state?: string;
|
|
42
|
+
readonly snapshotPath?: string;
|
|
43
|
+
readonly residual?: boolean;
|
|
37
44
|
}
|
|
38
45
|
|
|
39
46
|
/**
|
|
@@ -59,7 +66,9 @@ export async function refreshOwnedIntegration(
|
|
|
59
66
|
const bound = { ...rest, models, store };
|
|
60
67
|
const result = await runIntegrationMutationFlight(
|
|
61
68
|
input.clientId,
|
|
62
|
-
|
|
69
|
+
// Separate catalog snapshots must not inherit another refresh's success.
|
|
70
|
+
// The shared flight owner returns busy for overlapping operations instead.
|
|
71
|
+
`refresh:${crypto.randomUUID()}`,
|
|
63
72
|
input.io?.now ?? Date.now,
|
|
64
73
|
() => refreshIntegrationCoordinated(bound, options),
|
|
65
74
|
);
|
|
@@ -70,5 +79,8 @@ export async function refreshOwnedIntegration(
|
|
|
70
79
|
changed: result.changed,
|
|
71
80
|
...(result.state === "absent" ? { reason: result.message } : {}),
|
|
72
81
|
}
|
|
73
|
-
: { client: input.clientId, ok: false, reason: result.message
|
|
82
|
+
: { client: input.clientId, ok: false, reason: result.message, refusalReason: result.reason, state: result.state,
|
|
83
|
+
...(result.snapshotPath ? { snapshotPath: result.snapshotPath } : {}),
|
|
84
|
+
...(result.residual ? { residual: true } : {}),
|
|
85
|
+
};
|
|
74
86
|
}
|
|
@@ -352,6 +352,8 @@ export interface IntegrationStateInput {
|
|
|
352
352
|
/** The whole integration state store, bound to one root. */
|
|
353
353
|
store?: IntegrationStateStore;
|
|
354
354
|
io?: IntegrationIO;
|
|
355
|
+
/** Internal explicit profile target; never accepted as a caller-provided path. */
|
|
356
|
+
resolvedPaths?: { configPath: string; detectDir: string };
|
|
355
357
|
}
|
|
356
358
|
|
|
357
359
|
export function exportContextOf(input: {
|
|
@@ -431,7 +433,7 @@ export function readIntegrationState(input: IntegrationStateInput): IntegrationS
|
|
|
431
433
|
try {
|
|
432
434
|
// One resolution for both, so a client whose paths come from mutable state
|
|
433
435
|
// cannot report one account's install beside another account's config path.
|
|
434
|
-
const paths = resolveIntegrationPaths(input.clientId, input.env, input.home);
|
|
436
|
+
const paths = input.resolvedPaths ?? resolveIntegrationPaths(input.clientId, input.env, input.home);
|
|
435
437
|
configPath = paths.configPath;
|
|
436
438
|
installed = io.statKind(paths.detectDir) === "dir";
|
|
437
439
|
} catch (error) {
|
|
@@ -711,7 +711,9 @@ function freezeIntegrationInput(input: IntegrationWriteInput): FrozenIntegration
|
|
|
711
711
|
* its manifest, so two independent calls could verify one account's install
|
|
712
712
|
* and then write another account's catalog if a switch landed between them.
|
|
713
713
|
*/
|
|
714
|
-
const resolvedPaths =
|
|
714
|
+
const resolvedPaths = input.resolvedPaths
|
|
715
|
+
? { ...input.resolvedPaths }
|
|
716
|
+
: resolveIntegrationPaths(input.clientId, env, home);
|
|
715
717
|
return { ...input, env, home, store, io, resolvedPaths };
|
|
716
718
|
}
|
|
717
719
|
|
|
@@ -84,6 +84,11 @@ function killChild(child: ChildProcess): void {
|
|
|
84
84
|
export async function runIsolatedFabricProducer(request: IsolateRequest): Promise<IsolatedProducerResult> {
|
|
85
85
|
const now = request.now ?? (() => Date.now());
|
|
86
86
|
let lastActivityAt = now();
|
|
87
|
+
// Budget enforcement must not follow wall-clock adjustments; telemetry still does.
|
|
88
|
+
const budgetNow = request.now ?? (() => performance.now());
|
|
89
|
+
const startedAt = request.now ? lastActivityAt : budgetNow();
|
|
90
|
+
const totalDeadline = startedAt + request.totalTimeoutMs;
|
|
91
|
+
let inactivityDeadline = startedAt + request.inactivityTimeoutMs;
|
|
87
92
|
|
|
88
93
|
return await new Promise<IsolatedProducerResult>((resolve, reject) => {
|
|
89
94
|
let child: ChildProcess;
|
|
@@ -104,48 +109,74 @@ export async function runIsolatedFabricProducer(request: IsolateRequest): Promis
|
|
|
104
109
|
let stdoutBuffer = "";
|
|
105
110
|
let stderrBytes = 0;
|
|
106
111
|
let settled = false;
|
|
112
|
+
let childClosed = false;
|
|
107
113
|
let receivedResult: SyntheticPatchV1 | undefined;
|
|
108
114
|
let killReason: FabricTaskError | undefined;
|
|
109
115
|
|
|
110
116
|
const finish = (fn: () => void) => {
|
|
111
|
-
|
|
117
|
+
// A latched failure owns settlement, but scratch cleanup must wait for close.
|
|
118
|
+
if (settled || (killReason && !childClosed)) return;
|
|
112
119
|
settled = true;
|
|
113
120
|
clearTimeout(totalTimer);
|
|
114
121
|
clearTimeout(inactivityTimer);
|
|
115
|
-
|
|
122
|
+
if (killReason) reject(killReason);
|
|
123
|
+
else fn();
|
|
116
124
|
};
|
|
117
125
|
|
|
118
126
|
const settleTimeout = (error: FabricTaskError) => {
|
|
119
|
-
if (settled) return;
|
|
127
|
+
if (settled || killReason) return;
|
|
120
128
|
killReason = error;
|
|
121
|
-
|
|
129
|
+
if (childClosed) finish(() => reject(error));
|
|
130
|
+
else killChild(child);
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
const expiredDeadline = (at: number): FabricTaskError | undefined => {
|
|
134
|
+
// Choose the earliest deadline, regardless of which timer/data callback ran first.
|
|
135
|
+
if (at >= inactivityDeadline && inactivityDeadline <= totalDeadline) {
|
|
136
|
+
return new FabricTaskError("inactivity timeout exceeded", "inactivity_timeout", "environment");
|
|
137
|
+
}
|
|
138
|
+
if (at >= totalDeadline) {
|
|
139
|
+
return new FabricTaskError("total timeout exceeded", "timeout", "environment");
|
|
140
|
+
}
|
|
141
|
+
return undefined;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const onInactivityTimeout = () => {
|
|
145
|
+
settleTimeout(expiredDeadline(budgetNow())
|
|
146
|
+
?? new FabricTaskError("inactivity timeout exceeded", "inactivity_timeout", "environment"));
|
|
122
147
|
};
|
|
123
148
|
|
|
124
149
|
const armInactivity = () => {
|
|
125
150
|
clearTimeout(inactivityTimer);
|
|
126
|
-
inactivityTimer = setTimeout(
|
|
127
|
-
settleTimeout(new FabricTaskError("inactivity timeout exceeded", "inactivity_timeout", "environment"));
|
|
128
|
-
}, request.inactivityTimeoutMs);
|
|
151
|
+
inactivityTimer = setTimeout(onInactivityTimeout, request.inactivityTimeoutMs);
|
|
129
152
|
};
|
|
130
153
|
|
|
131
|
-
let inactivityTimer: ReturnType<typeof setTimeout> = setTimeout(
|
|
132
|
-
settleTimeout(new FabricTaskError("inactivity timeout exceeded", "inactivity_timeout", "environment"));
|
|
133
|
-
}, request.inactivityTimeoutMs);
|
|
154
|
+
let inactivityTimer: ReturnType<typeof setTimeout> = setTimeout(onInactivityTimeout, request.inactivityTimeoutMs);
|
|
134
155
|
|
|
135
156
|
const totalTimer = setTimeout(() => {
|
|
136
|
-
settleTimeout(
|
|
157
|
+
settleTimeout(expiredDeadline(budgetNow())
|
|
158
|
+
?? new FabricTaskError("total timeout exceeded", "timeout", "environment"));
|
|
137
159
|
}, request.totalTimeoutMs);
|
|
138
160
|
|
|
139
161
|
const handleProtocolLine = (line: string) => {
|
|
162
|
+
if (settled || killReason) return;
|
|
140
163
|
try {
|
|
141
164
|
const message = parseProducerProtocolLine(line);
|
|
142
|
-
if (message.type === "activity") {
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
165
|
+
if (message.type === "activity" || message.type === "result") {
|
|
166
|
+
const at = budgetNow();
|
|
167
|
+
const expired = expiredDeadline(at);
|
|
168
|
+
if (expired) {
|
|
169
|
+
settleTimeout(expired);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
if (message.type === "activity") {
|
|
173
|
+
lastActivityAt = request.now ? at : now();
|
|
174
|
+
inactivityDeadline = at + request.inactivityTimeoutMs;
|
|
175
|
+
armInactivity();
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
146
178
|
}
|
|
147
179
|
if (message.type === "result") {
|
|
148
|
-
if (settled) return;
|
|
149
180
|
receivedResult = message.patch;
|
|
150
181
|
finish(() => resolve({ patch: message.patch, lastActivityAt }));
|
|
151
182
|
return;
|
|
@@ -176,6 +207,7 @@ export async function runIsolatedFabricProducer(request: IsolateRequest): Promis
|
|
|
176
207
|
};
|
|
177
208
|
|
|
178
209
|
const consumeStdout = (chunk: string) => {
|
|
210
|
+
if (settled || killReason) return;
|
|
179
211
|
stdoutBuffer += chunk;
|
|
180
212
|
if (Buffer.byteLength(stdoutBuffer, "utf8") > FABRIC_PRODUCER_PROTOCOL_MAX_BYTES) {
|
|
181
213
|
settleTimeout(new FabricTaskError("producer protocol output exceeded limit", "budget_exhausted", "environment"));
|
|
@@ -207,16 +239,50 @@ export async function runIsolatedFabricProducer(request: IsolateRequest): Promis
|
|
|
207
239
|
}
|
|
208
240
|
});
|
|
209
241
|
|
|
242
|
+
child.stderr?.on("error", (error) => {
|
|
243
|
+
settleTimeout(new FabricTaskError(error.message, "harness_failure", "harness"));
|
|
244
|
+
});
|
|
245
|
+
|
|
210
246
|
child.on("error", (error) => {
|
|
211
247
|
finish(() => reject(new FabricTaskError(error.message, "harness_failure", "harness")));
|
|
212
248
|
});
|
|
213
249
|
|
|
214
250
|
child.stdin?.on("error", (error: NodeJS.ErrnoException) => {
|
|
215
|
-
if (settled || error.code === "EPIPE") return;
|
|
251
|
+
if (settled || killReason || error.code === "EPIPE") return;
|
|
216
252
|
killChild(child);
|
|
217
253
|
finish(() => reject(new FabricTaskError(error.message, "harness_failure", "harness")));
|
|
218
254
|
});
|
|
219
255
|
|
|
256
|
+
child.on("close", (code, signal) => {
|
|
257
|
+
childClosed = true;
|
|
258
|
+
if (settled) return;
|
|
259
|
+
if (killReason) {
|
|
260
|
+
finish(() => reject(killReason!));
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (receivedResult) {
|
|
264
|
+
finish(() => resolve({ patch: receivedResult!, lastActivityAt }));
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
if (stdoutBuffer.trim()) {
|
|
268
|
+
try {
|
|
269
|
+
handleProtocolLine(stdoutBuffer.trim());
|
|
270
|
+
if (settled) return;
|
|
271
|
+
} catch {
|
|
272
|
+
/* fall through */
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (signal === "SIGKILL") {
|
|
276
|
+
finish(() => reject(new FabricTaskError("total timeout exceeded", "timeout", "environment")));
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
finish(() => reject(new FabricTaskError(
|
|
280
|
+
code === 0 ? "isolated producer returned no result" : `isolated producer exited (${code ?? signal ?? "unknown"})`,
|
|
281
|
+
"harness_failure",
|
|
282
|
+
"harness",
|
|
283
|
+
)));
|
|
284
|
+
});
|
|
285
|
+
|
|
220
286
|
const payload = JSON.stringify({
|
|
221
287
|
harnessKind: request.harnessKind,
|
|
222
288
|
executorModulePath: request.executorModulePath,
|
|
@@ -242,6 +308,7 @@ export async function runIsolatedFabricProducer(request: IsolateRequest): Promis
|
|
|
242
308
|
child.stdin?.write(payload);
|
|
243
309
|
child.stdin?.end();
|
|
244
310
|
} catch (error) {
|
|
311
|
+
if (killReason) return;
|
|
245
312
|
killChild(child);
|
|
246
313
|
finish(() => reject(new FabricTaskError(
|
|
247
314
|
error instanceof Error ? error.message : String(error),
|
|
@@ -250,35 +317,6 @@ export async function runIsolatedFabricProducer(request: IsolateRequest): Promis
|
|
|
250
317
|
)));
|
|
251
318
|
return;
|
|
252
319
|
}
|
|
253
|
-
|
|
254
|
-
child.on("close", (code, signal) => {
|
|
255
|
-
if (settled) return;
|
|
256
|
-
if (killReason) {
|
|
257
|
-
finish(() => reject(killReason!));
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
if (receivedResult) {
|
|
261
|
-
finish(() => resolve({ patch: receivedResult!, lastActivityAt }));
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
if (stdoutBuffer.trim()) {
|
|
265
|
-
try {
|
|
266
|
-
handleProtocolLine(stdoutBuffer.trim());
|
|
267
|
-
if (receivedResult) return;
|
|
268
|
-
} catch {
|
|
269
|
-
/* fall through */
|
|
270
|
-
}
|
|
271
|
-
}
|
|
272
|
-
if (signal === "SIGKILL") {
|
|
273
|
-
finish(() => reject(new FabricTaskError("total timeout exceeded", "timeout", "environment")));
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
276
|
-
finish(() => reject(new FabricTaskError(
|
|
277
|
-
code === 0 ? "isolated producer returned no result" : `isolated producer exited (${code ?? signal ?? "unknown"})`,
|
|
278
|
-
"harness_failure",
|
|
279
|
-
"harness",
|
|
280
|
-
)));
|
|
281
|
-
});
|
|
282
320
|
});
|
|
283
321
|
}
|
|
284
322
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/** Process-local invalidations. Connection limits, buffering, and timers belong to consumers. */
|
|
2
|
+
export type AccountSelectionEvent = {
|
|
3
|
+
provider: string;
|
|
4
|
+
kind: "oauth" | "api-key";
|
|
5
|
+
revision: number;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
const listeners = new Set<(event: AccountSelectionEvent) => void>();
|
|
9
|
+
let revision = 0;
|
|
10
|
+
|
|
11
|
+
/** Call only after the authoritative selection has been persisted. */
|
|
12
|
+
export function publishAccountSelection(provider: string, kind: AccountSelectionEvent["kind"]): void {
|
|
13
|
+
const event: AccountSelectionEvent = Object.freeze({ provider, kind, revision: ++revision });
|
|
14
|
+
for (const listener of [...listeners]) {
|
|
15
|
+
try {
|
|
16
|
+
listener(event);
|
|
17
|
+
} catch {
|
|
18
|
+
// A disconnected consumer must not turn a committed write into a reported failure.
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function subscribeAccountSelections(listener: (event: AccountSelectionEvent) => void): () => void {
|
|
24
|
+
// Give each subscription its own lifetime, even when a callback is reused.
|
|
25
|
+
const subscription = (event: AccountSelectionEvent) => listener(event);
|
|
26
|
+
listeners.add(subscription);
|
|
27
|
+
return () => { listeners.delete(subscription); };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function currentAccountSelectionRevision(): number {
|
|
31
|
+
return revision;
|
|
32
|
+
}
|
package/src/lib/errors.ts
CHANGED
|
@@ -4,10 +4,15 @@ export interface OcxErrorPayload {
|
|
|
4
4
|
code: string | null;
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
+
export const ENCRYPTED_FUNCTION_OUTPUT_REJECTION =
|
|
8
|
+
"Encrypted function output content could not be decrypted or decoded.";
|
|
9
|
+
|
|
7
10
|
/** Canonical human-readable message paths used by Responses upstream failures. */
|
|
8
11
|
export function upstreamErrorMessageFromPayload(payload: unknown): string | undefined {
|
|
9
12
|
if (!payload || typeof payload !== "object" || Array.isArray(payload)) return undefined;
|
|
10
13
|
const json = payload as {
|
|
14
|
+
type?: unknown;
|
|
15
|
+
message?: unknown;
|
|
11
16
|
error?: { message?: unknown };
|
|
12
17
|
last_error?: { message?: unknown };
|
|
13
18
|
response?: {
|
|
@@ -18,7 +23,10 @@ export function upstreamErrorMessageFromPayload(payload: unknown): string | unde
|
|
|
18
23
|
const message = json.error?.message
|
|
19
24
|
?? json.last_error?.message
|
|
20
25
|
?? json.response?.error?.message
|
|
21
|
-
?? json.response?.incomplete_details?.message
|
|
26
|
+
?? json.response?.incomplete_details?.message
|
|
27
|
+
// The Responses stream error event carries a flat message (type/code/message),
|
|
28
|
+
// unlike the response.failed envelope the branches above already cover.
|
|
29
|
+
?? (json.type === "error" ? json.message : undefined);
|
|
22
30
|
return typeof message === "string" ? message : undefined;
|
|
23
31
|
}
|
|
24
32
|
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
resolvePublicAddresses,
|
|
8
8
|
} from "./destination-policy";
|
|
9
9
|
import { pinnedHttpGet, pinnedHttpPost } from "./pinned-http";
|
|
10
|
-
import { effectiveProxyFor, outboundProxyConfigured } from "./proxy-env";
|
|
10
|
+
import { effectiveProxyFor, noProxyMatches, normalizeProxyHostname, outboundProxyConfigured } from "./proxy-env";
|
|
11
11
|
import { publicProviderBaseUrl } from "./provider-url";
|
|
12
12
|
|
|
13
13
|
type ProviderGetInit = Omit<RequestInit, "body" | "method" | "redirect">;
|
|
@@ -37,10 +37,6 @@ function pickPinnedAddress(addresses: Array<{ address: string; family: number }>
|
|
|
37
37
|
return addresses.find(address => address.family === 4) ?? addresses[0]!;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
function configuredProxyFor(): boolean {
|
|
41
|
-
return outboundProxyConfigured();
|
|
42
|
-
}
|
|
43
|
-
|
|
44
40
|
/**
|
|
45
41
|
* Registry-owned fake-IP transparency exception (Clash/Surge/Mihomo TUN mode).
|
|
46
42
|
*
|
|
@@ -76,45 +72,6 @@ function transparentFakeIpException(
|
|
|
76
72
|
return isCanonicalUrl(name, url);
|
|
77
73
|
}
|
|
78
74
|
|
|
79
|
-
function normalizeProxyHostname(hostname: string): string {
|
|
80
|
-
const normalized = hostname.trim().toLowerCase().replace(/\.+$/, "");
|
|
81
|
-
return normalized.startsWith("[") && normalized.endsWith("]")
|
|
82
|
-
? normalized.slice(1, -1)
|
|
83
|
-
: normalized;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
function noProxyMatches(url: URL): boolean {
|
|
87
|
-
const raw = process.env.NO_PROXY ?? process.env.no_proxy ?? "";
|
|
88
|
-
const hostname = normalizeProxyHostname(url.hostname);
|
|
89
|
-
const port = url.port || (url.protocol === "https:" ? "443" : "80");
|
|
90
|
-
for (const rawEntry of raw.split(",")) {
|
|
91
|
-
let entry = rawEntry.trim().toLowerCase();
|
|
92
|
-
if (!entry) continue;
|
|
93
|
-
if (entry === "*") return true;
|
|
94
|
-
entry = entry.replace(/^https?:\/\//, "").split("/", 1)[0]!;
|
|
95
|
-
|
|
96
|
-
let entryHost = entry;
|
|
97
|
-
let entryPort = "";
|
|
98
|
-
const bracketed = /^\[([^\]]+)](?::(\d+))?$/.exec(entry);
|
|
99
|
-
if (bracketed) {
|
|
100
|
-
entryHost = bracketed[1]!;
|
|
101
|
-
entryPort = bracketed[2] ?? "";
|
|
102
|
-
} else if ((entry.match(/:/g)?.length ?? 0) === 1) {
|
|
103
|
-
const separator = entry.lastIndexOf(":");
|
|
104
|
-
const possiblePort = entry.slice(separator + 1);
|
|
105
|
-
if (/^\d+$/.test(possiblePort)) {
|
|
106
|
-
entryHost = entry.slice(0, separator);
|
|
107
|
-
entryPort = possiblePort;
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
if (entryPort && entryPort !== port) continue;
|
|
111
|
-
entryHost = normalizeProxyHostname(entryHost.replace(/^\*?\./, ""));
|
|
112
|
-
if (!entryHost) continue;
|
|
113
|
-
if (hostname === entryHost || hostname.endsWith(`.${entryHost}`)) return true;
|
|
114
|
-
}
|
|
115
|
-
return false;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
75
|
let proxyBoundaryWarned = false;
|
|
119
76
|
let proxyDnsDegradationWarned = false;
|
|
120
77
|
|
|
@@ -181,7 +138,7 @@ async function providerOutboundRequest(
|
|
|
181
138
|
return provider.fetch(url, { ...init, method, redirect: "manual" });
|
|
182
139
|
}
|
|
183
140
|
const parsed = postUrl ?? new URL(url);
|
|
184
|
-
const proxyConfigured =
|
|
141
|
+
const proxyConfigured = outboundProxyConfigured();
|
|
185
142
|
// Snapshot the scheme-matched proxy once, before the DNS await, so admission and transport
|
|
186
143
|
// below reason about the same value. `null` here means "no proxy fetch would actually use",
|
|
187
144
|
// even if some other proxy variable is set.
|