Files
zopu-code/packages/backend/convex/workExecutionAgent.ts
-Puter c644ec8d01 feat(git): thin project onboarding, provider integration, and AgentOS repository access
Effect primitives:
- git-provider: GitProvider, connection states, normalized errors, URL
  normalization, host compatibility, credential freshness window
- git-provisioning: validated Puter commands, migration states, idempotency
  keys, safe replacement rules, owner-safe guards
- git-webhook: supported events, signature verification, delivery states
- host-repository: provider-neutral credential-safe clone via GIT_ASKPASS

Normalized Convex schema:
- gitProviderAccounts, refined gitConnections, gitProviderOrganizations,
  gitRepositories, gitMigrations, gitWebhookDeliveries
- projects: gitRepositoryId + instructions fields
- Schema fields optional for backward compatibility, with backfill cron

Backend:
- Connection health: verify action, hourly reconciliation (covers stale
  active + reauth-required + undefined-state legacy connections)
- Puter provisioning: createPuterUser/Organization/Repository with owner
  binding, startGithubMigration (durable via scheduler), getMigration
- Org ownership: explicit member add with admin role + verification
- Webhook HTTP actions: HMAC verification, delivery persistence with
  idempotency, repository resolution, byte-length payload limit
- Automatic Puter webhook creation after repo creation/migration with
  fail-loud state tracking
- Repository sync after connection (Gitea + GitHub)
- AgentOS execution resolves gitRepositoryId for real clone URL
- Credential gating: state + freshness checks before execution and
  project creation
- listForOrganization for cross-project Work filtering

Frontend:
- /projects onboarding page with GitHub OAuth (linkSocial) and Puter PAT
- Zero-project redirect, repository selection, context editor
- Provider-aware settings panel (no serverUrl for Puter)
- Project selection via ?project= query param
- GitHub scopes: repo + read:org

Agent runtime:
- Clones user repository with GIT_ASKPASS credential helper (no token in
  URL, args, or git config), provider-aware username
- Removed fixed Zopu source path and .env copy
2026-07-31 14:36:56 +05:30

120 lines
4.0 KiB
TypeScript

"use node";
import { env } from "@code/env/convex";
import {
WorkAttemptExecutionError,
decodeWorkAttemptExecutionFailure,
decodeWorkAttemptExecutionResult,
} from "@code/primitives/execution-runtime";
import { makeFunctionReference } from "convex/server";
import { ConvexError, v } from "convex/values";
import { Effect } from "effect";
import { internal } from "./_generated/api";
import type { Id } from "./_generated/dataModel";
import { internalAction } from "./_generated/server";
import { decryptCredential } from "./gitConnections";
const getRepositoryRef = makeFunctionReference<
"query",
{ repositoryId: Id<"gitRepositories"> },
{
readonly cloneUrl: string;
readonly defaultBranch: string;
} | null
>("gitProvisioning:getRepository");
const backendUrl = () => env.AGENT_BACKEND_URL ?? env.FLUE_URL;
export const executeAttempt = internalAction({
args: { attemptId: v.id("workAttempts") },
handler: async (ctx, args) => {
const running = await ctx.runMutation(
internal.workExecutionWorkflow.markAttemptRunning,
args
);
if (!running) {
throw new ConvexError("Attempt is no longer runnable");
}
const context = await ctx.runQuery(
internal.workExecutionWorkflow.executionContext,
args
);
const credential = await decryptCredential(
context.connection.credentialCiphertext,
context.connection.credentialIv
);
// Resolve the project's normalized repository if available; fall back to
// legacy sourceUrl for backward compatibility with pre-backfill projects.
const repository = context.project.gitRepositoryId
? await ctx.runQuery(getRepositoryRef, {
repositoryId: context.project.gitRepositoryId,
})
: null;
const repositoryUrl = repository?.cloneUrl ?? context.project.sourceUrl;
const baseBranch =
repository?.defaultBranch ?? context.project.defaultBranch ?? "main";
const response = await fetch(
`${backendUrl()}/internal/work-attempts/execute`,
{
body: JSON.stringify({
attemptId: String(context.attempt._id),
auth: {
credential,
provider: context.connection.provider,
serverUrl: context.connection.serverUrl,
username: context.connection.username,
},
baseBranch,
prompt: context.prompt,
repositoryUrl,
runId: String(context.run._id),
workId: String(context.work._id),
workspaceKey: context.attempt.workspaceKey,
}),
headers: {
authorization: `Bearer ${env.FLUE_DB_TOKEN}`,
"content-type": "application/json",
},
method: "POST",
}
);
const payload = (await response.json()) as unknown;
if (!response.ok) {
// Decode the agent's classified failure envelope and re-throw as the
// typed runtime error so the workflow handler maps reason/retryable to
// a durable attempt classification. A malformed envelope falls back to
// an InvalidInput failure (non-retryable).
const failure = await Effect.runPromise(
decodeWorkAttemptExecutionFailure(payload)
);
throw new WorkAttemptExecutionError(failure.error);
}
return await Effect.runPromise(decodeWorkAttemptExecutionResult(payload));
},
});
export const cancelAttempt = internalAction({
args: {
attemptId: v.string(),
workspaceKey: v.string(),
},
handler: async (_ctx, args) => {
// The workspace key remains the URL path segment (workspace identity),
// while the body carries the attemptId so the runtime can target the
// matching Pi ACP session for cancellation.
await fetch(
`${backendUrl()}/internal/work-attempts/${encodeURIComponent(args.workspaceKey)}/cancel`,
{
body: JSON.stringify({ attemptId: args.attemptId }),
headers: {
authorization: `Bearer ${env.FLUE_DB_TOKEN}`,
"content-type": "application/json",
},
method: "POST",
}
);
},
});