Fresh history cut from cynarAI/evoo antigravity-handoff/project-timeline (Voice First + project timeline). Brand docs/titles as evOo; keep @rakazo/* packages as tech debt. Excludes node_modules, caches, secrets, voice blobs.
184 lines
5.9 KiB
TypeScript
184 lines
5.9 KiB
TypeScript
import type { AdapterContext, ManagedConnectorProvider } from "@rakazo/adapter-kit";
|
|
import { SecretAskPurpose } from "@rakazo/contracts";
|
|
import type { PrismaClient, RunSecretWriter } from "@rakazo/db";
|
|
import { type ApprovalPausedToolResult, resolveDuplicateEffectGate } from "./approval-effect.js";
|
|
import { storeBotSecret } from "./bot-secrets.js";
|
|
import type { EncryptedSecretStore } from "./secrets.js";
|
|
|
|
export function runSecretKind(runId: string): string {
|
|
return `run-secret:${runId}`;
|
|
}
|
|
|
|
export function normalizeSecretAskPurpose(purpose: string | undefined): SecretAskPurpose {
|
|
return SecretAskPurpose.safeParse(purpose).data ?? "otp";
|
|
}
|
|
|
|
export function secretPausedToolResult(): ApprovalPausedToolResult {
|
|
return {
|
|
kind: "agent_tool_result",
|
|
content: [{ type: "text", text: "Waiting for protected input." }],
|
|
details: { secret: "paused" },
|
|
terminate: true,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Delete the stored secret before connector side effects, then persist the tool result.
|
|
* Keeping the ciphertext until after complete() lets a crash retry resubmit a single-use OTP.
|
|
* Callers must claim the effect to executing first and reconcile via connection status when
|
|
* the secret is already gone on retry.
|
|
*/
|
|
export async function commitConsumedRunSecret<TResult, TFailed>(input: {
|
|
deleteSecret: () => Promise<void>;
|
|
afterSecretTaken: () => Promise<TResult>;
|
|
persist: (result: TResult) => Promise<boolean>;
|
|
onPersistFailed: TFailed;
|
|
}): Promise<TResult | TFailed> {
|
|
await input.deleteSecret();
|
|
const result = await input.afterSecretTaken();
|
|
if (!(await input.persist(result))) return input.onPersistFailed;
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* When a completed request_secret effect still has a run-secret row, decide whether
|
|
* that row is a crash leftover (same OTP, do not resubmit) or a newer replacement.
|
|
*/
|
|
export function resolveCompletedSecretLeftover(input: {
|
|
secretCreatedAt: Date;
|
|
effectUpdatedAt: Date;
|
|
}): "drop_leftover" | "consume_replacement" {
|
|
return input.secretCreatedAt.getTime() <= input.effectUpdatedAt.getTime()
|
|
? "drop_leftover"
|
|
: "consume_replacement";
|
|
}
|
|
|
|
/**
|
|
* When the stored run secret is already gone, decide whether to reuse a settled
|
|
* effect result, settle an in-flight attempt, or ask again.
|
|
*/
|
|
export function resolveMissingRunSecretAction(
|
|
effect: { status: string; result?: unknown } | null | undefined,
|
|
):
|
|
| { action: "return"; result: unknown }
|
|
| { action: "uncertain"; toolName: string }
|
|
| { action: "settle_attempt" }
|
|
| { action: "ask" } {
|
|
if (!effect) return { action: "ask" };
|
|
const gate = resolveDuplicateEffectGate(effect, "request_secret");
|
|
if (gate.action === "return") return { action: "return", result: gate.result };
|
|
if (gate.action === "uncertain") {
|
|
// executing means connector may already have consumed the OTP
|
|
return { action: "settle_attempt" };
|
|
}
|
|
if (effect.status === "executing") return { action: "settle_attempt" };
|
|
return { action: "ask" };
|
|
}
|
|
|
|
export async function reconcileManagedConnection(
|
|
prisma: PrismaClient,
|
|
connectors: { managed(id: string): ManagedConnectorProvider | undefined } | undefined,
|
|
run: { spaceId: string; userId: string },
|
|
context: AdapterContext,
|
|
connectionId: string,
|
|
): Promise<"connected" | "pending" | "missing"> {
|
|
const row = await prisma.connection.findFirst({
|
|
where: {
|
|
id: connectionId,
|
|
spaceId: run.spaceId,
|
|
userId: run.userId,
|
|
},
|
|
});
|
|
if (!row) return "missing";
|
|
if (row.status === "connected") return "connected";
|
|
if (row.status !== "pending") return "missing";
|
|
const connector = connectors?.managed(row.connectorId);
|
|
if (!connector) return "pending";
|
|
try {
|
|
const ready = await connector.connectionReady(context, row.provider);
|
|
if (ready) {
|
|
await prisma.connection.update({
|
|
where: { id: row.id },
|
|
data: { status: "connected" },
|
|
});
|
|
return "connected";
|
|
}
|
|
} catch {
|
|
return "pending";
|
|
}
|
|
return "pending";
|
|
}
|
|
|
|
export function createRunSecretWriter(secretStore: EncryptedSecretStore): RunSecretWriter {
|
|
return {
|
|
async store({ runId, userId, spaceId, botId, credential, plaintext, tx }) {
|
|
if (credential) {
|
|
await storeBotSecret({
|
|
tx,
|
|
secretStore,
|
|
scope: { userId, spaceId, botId },
|
|
destination: credential,
|
|
plaintext,
|
|
});
|
|
return;
|
|
}
|
|
const stored = await secretStore.put(plaintext, {
|
|
operationId: runId,
|
|
traceId: runId,
|
|
spaceId,
|
|
userId,
|
|
signal: new AbortController().signal,
|
|
});
|
|
await tx.secret.create({
|
|
data: {
|
|
id: stored.id,
|
|
userId,
|
|
spaceId,
|
|
kind: runSecretKind(runId),
|
|
ciphertext: stored.ciphertext,
|
|
},
|
|
});
|
|
},
|
|
};
|
|
}
|
|
|
|
export async function tryCompleteConnectionWithCode(
|
|
prisma: PrismaClient,
|
|
connectors: { managed(id: string): ManagedConnectorProvider | undefined } | undefined,
|
|
run: { spaceId: string; userId: string },
|
|
context: AdapterContext,
|
|
connectionId: string,
|
|
code: string,
|
|
): Promise<{ connected: boolean; error?: string }> {
|
|
const row = await prisma.connection.findFirst({
|
|
where: {
|
|
id: connectionId,
|
|
spaceId: run.spaceId,
|
|
userId: run.userId,
|
|
status: { in: ["pending", "connected"] },
|
|
},
|
|
});
|
|
if (!row) return { connected: false };
|
|
// Already finished on a prior attempt; do not resubmit the OTP.
|
|
if (row.status === "connected") return { connected: true };
|
|
const connector = connectors?.managed(row.connectorId);
|
|
if (!connector) return { connected: false };
|
|
const state = row.providerRef ?? row.provider;
|
|
try {
|
|
await connector.complete({ state, code }, context);
|
|
const ready = await connector.connectionReady(context, row.provider);
|
|
if (ready) {
|
|
await prisma.connection.update({
|
|
where: { id: row.id },
|
|
data: { status: "connected" },
|
|
});
|
|
}
|
|
return { connected: ready };
|
|
} catch {
|
|
return {
|
|
connected: false,
|
|
error: "Connection could not be completed.",
|
|
};
|
|
}
|
|
}
|