Skip to content
Merged
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
2 changes: 1 addition & 1 deletion apps/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
},
"scripts": {
"build": "node build-blueprints.ts && node ../../packages/compiler/build.ts ../web/dist && node build-extractor.ts ../web/dist",
"dev": "vp -C ../web build --mode development && vp run build && node ../connect/build.ts && wrangler d1 migrations apply DB --local --persist-to \"${DEV_PERSIST_TO:-.wrangler/state}\" && wrangler d1 migrations apply KNOWLEDGE --local --persist-to \"${DEV_PERSIST_TO:-.wrangler/state}\" && wrangler d1 migrations apply DB --local -c ../connect/wrangler.jsonc --persist-to \"${DEV_PERSIST_TO:-.wrangler/state}\" && wrangler dev -c wrangler.jsonc -c ../connect/wrangler.jsonc --persist-to \"${DEV_PERSIST_TO:-.wrangler/state}\" --var DEV_SKIP_ROUTER_SECRET:true --var DURABLE_OBJECT_JURISDICTION:none",
"dev": "vp -C ../web build --mode development && vp run build && node ../connect/build.ts && wrangler d1 migrations apply DB --local --persist-to \"${DEV_PERSIST_TO:-.wrangler/state}\" && wrangler d1 migrations apply KNOWLEDGE --local --persist-to \"${DEV_PERSIST_TO:-.wrangler/state}\" && wrangler d1 migrations apply DB --local -c ../connect/wrangler.jsonc --persist-to \"${DEV_PERSIST_TO:-.wrangler/state}\" && wrangler dev -c wrangler.jsonc -c ../connect/wrangler.jsonc --persist-to \"${DEV_PERSIST_TO:-.wrangler/state}\" --var DEV_SKIP_ROUTER_SECRET:true --var DURABLE_OBJECT_JURISDICTION:none --var RUNTIME_PROFILE:workerd",
"deploy": "vp run build && wrangler deploy",
"typegen": "wrangler types --include-runtime=false -c wrangler.jsonc -c ../connect/wrangler.jsonc",
"db:migrate": "wrangler d1 migrations apply DB --remote && wrangler d1 migrations apply KNOWLEDGE --remote",
Expand Down
65 changes: 53 additions & 12 deletions apps/core/src/agent-builds.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ import { WorkerEntrypoint, exports } from "cloudflare:workers";
import { z } from "zod";

import { asPerson } from "./agent-person.ts";
import { chatAuthority, chatContext } from "./agent-scope.ts";
import { admitEffect, chatAuthority, chatContext } from "./agent-scope.ts";
import type { AgentApi, AgentScope } from "./agent-scope.ts";
import { createFromBlueprint, listBlueprints } from "./app-blueprints.ts";
import { isPlainData } from "./app.ts";
Expand Down Expand Up @@ -437,6 +437,17 @@ export interface Proposal {

/** Building Apps, as a chat's code does it. */
export class BuildApi extends WorkerEntrypoint<Env, AgentScope> {
/**
* Admits a write of this run's, handed to what writes it so it runs
* right before the write commits, after any slow preparation (fetching
* packages, building, storing files): a run that ended or lost a
* permission meanwhile writes nothing. The check each method makes
* before it starts is the cheap early refusal.
*/
readonly #admit = async (): Promise<void> => {
await admitEffect(this.env, this.ctx.props);
};

/**
* Runs one build call as the chat's person, with the agent as the audit
* log's actor for what it changes: once the agent may build Apps, and
Expand Down Expand Up @@ -488,7 +499,9 @@ export class BuildApi extends WorkerEntrypoint<Env, AgentScope> {
return await this.#build(
"build.create",
async (by) =>
await this.#creating(async () => await createApp(this.env, by, input)),
await this.#creating(
async () => await createApp(this.env, by, input, this.#admit)
),
(created) => ({ app: created.id })
);
}
Expand Down Expand Up @@ -521,7 +534,14 @@ export class BuildApi extends WorkerEntrypoint<Env, AgentScope> {
"build.createFromBlueprint",
async (by) =>
await this.#creating(
async () => await createFromBlueprint(this.env, by, blueprint, input)
async () =>
await createFromBlueprint(
this.env,
by,
blueprint,
input,
this.#admit
)
),
(created) => ({
app: created.app.id,
Expand All @@ -536,10 +556,13 @@ export class BuildApi extends WorkerEntrypoint<Env, AgentScope> {
* nothing.
*/
async #creating<T>(create: () => Promise<T>): Promise<T> {
const { workspaceId, chatId } = this.ctx.props;
const { workspaceId, chatId, runId } = this.ctx.props;
// Admitted as it takes the create, which a D1 write follows.
await admitEffect(this.env, this.ctx.props);
const taken = await workspace(this.env, workspaceId).takeCreate(
chatId,
maxCreatesPerTurn
maxCreatesPerTurn,
runId
);
if (!taken) {
throw appErrors.create("app.creates_exhausted");
Expand Down Expand Up @@ -637,6 +660,7 @@ export class BuildApi extends WorkerEntrypoint<Env, AgentScope> {
const unchanged = written.flatMap(([path, content]) =>
(content ?? undefined) === base.get(path) ? [path] : []
);
await admitEffect(this.env, this.ctx.props);
const saved = await workspace(
this.env,
this.ctx.props.workspaceId
Expand All @@ -646,7 +670,8 @@ export class BuildApi extends WorkerEntrypoint<Env, AgentScope> {
draft.base,
Object.fromEntries(kept),
unchanged,
draft.revision
draft.revision,
this.ctx.props.runId
);
if (!saved) {
throw appErrors.create("app.conflict");
Expand All @@ -673,8 +698,11 @@ export class BuildApi extends WorkerEntrypoint<Env, AgentScope> {
"build.discard",
async (by) => {
const id = await this.#buildable(by, app);
const { workspaceId, chatId } = this.ctx.props;
await workspace(this.env, workspaceId).dropDraft(chatId, id);
const { workspaceId, chatId, runId } = this.ctx.props;
await admitEffect(this.env, this.ctx.props);
await workspace(this.env, workspaceId).dropDraft(chatId, id, {
runId,
});
},
() => ({ app: typeof app === "string" ? app : null })
);
Expand Down Expand Up @@ -750,7 +778,16 @@ export class BuildApi extends WorkerEntrypoint<Env, AgentScope> {
reviewUnavailable: false,
};
}
const proposed = await proposeDraft(this.env, by, id, over, message);
// The commit is admitted only while the run is still open.
await admitEffect(this.env, this.ctx.props);
const proposed = await proposeDraft(
this.env,
by,
id,
over,
message,
this.#admit
);
// Committed and up for review: from here on it is proposed, however
// the rest goes, and says so, so the agent doesn't try again (and
// meet nothing to commit). What follows is logged when it fails.
Expand All @@ -764,7 +801,7 @@ export class BuildApi extends WorkerEntrypoint<Env, AgentScope> {
}
// Only the revision committed: a write since stays a draft.
await workspace(this.env, workspaceId)
.dropDraft(chatId, id, draft.revision)
.dropDraft(chatId, id, { revision: draft.revision })
.catch(afterProposal("drop_draft", id, version));
const review = await reviewVersion(this.env, by, id, version).catch(
afterProposal("review", id, version)
Expand Down Expand Up @@ -795,13 +832,15 @@ export class BuildApi extends WorkerEntrypoint<Env, AgentScope> {
async (by) => {
const id = await this.#buildable(by, app);
const subject = { type: "app", appId: id };
await admitEffect(this.env, this.ctx.props);
return await requestPermission(
this.env,
by,
typeof request === "object" && request !== null
? { ...request, subject }
: request,
async (named, role) => await appFor(this.env, by, named, role)
async (named, role) => await appFor(this.env, by, named, role),
this.#admit
);
},
(requested) => ({
Expand All @@ -825,12 +864,14 @@ export class BuildApi extends WorkerEntrypoint<Env, AgentScope> {
"build.resolveDependencies",
async (by) => {
const id = await this.#buildable(by, app);
await admitEffect(this.env, this.ctx.props);
return await resolveDependencies(
Comment thread
nickruigrok marked this conversation as resolved.
this.env,
by,
typeof intent === "object" && intent !== null
? { ...intent, app: id }
: intent
: intent,
this.#admit
);
},
(resolved) => ({
Expand Down
11 changes: 10 additions & 1 deletion apps/core/src/agent-connections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { WorkerEntrypoint, exports } from "cloudflare:workers";
import { z } from "zod";

import {
admitEffect,
auditRefusal,
auditedCall,
chatAuthority,
Expand Down Expand Up @@ -231,7 +232,12 @@ export class ConnectionsApi extends WorkerEntrypoint<Env, AgentScope> {
method: "connections.request",
detailOf: (asked: AskedConnection) => ({ permission: asked.id }),
},
async () => await askInChat(this.env, scope, ask)
async () => {
await admitEffect(this.env, scope);
return await askInChat(this.env, scope, ask, async () => {
await admitEffect(this.env, scope);
});
}
);
} catch (error) {
throw forSandbox(error);
Expand Down Expand Up @@ -280,6 +286,9 @@ export class ConnectionsApi extends WorkerEntrypoint<Env, AgentScope> {
);
let result: ConnectResult;
try {
// Admitted as it goes to connect, not only as the call began; a
// refusal here is recorded here, as nothing else records this call.
await admitEffect(this.env, scope, "connections.call");
result = await this.env.CONNECT.call(request);
} catch (error) {
throw forSandbox(error);
Expand Down
15 changes: 13 additions & 2 deletions apps/core/src/agent-memory.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
import { WorkerEntrypoint, exports } from "cloudflare:workers";

import { chatAuthority, chatContext, requireOpenRun } from "./agent-scope.ts";
import {
admitEffect,
chatAuthority,
chatContext,
requireOpenRun,
} from "./agent-scope.ts";
import type { AgentApi, AgentScope } from "./agent-scope.ts";
import { forSandbox } from "./bindings.ts";
import { saveUserMemory } from "./knowledge/memory.ts";
Expand All @@ -20,12 +25,18 @@ export class MemoryApi extends WorkerEntrypoint<Env, AgentScope> {
const scope = this.ctx.props;
await requireOpenRun(this.env, scope, "memory.saveUser");
try {
// Recorded here when refused: knowledge records only a save.
await admitEffect(this.env, scope, "memory.saveUser");
const saved = await saveUserMemory(
this.env,
chatAuthority(scope),
chatContext(scope),
{ type: "own" },
input
input,
// Again right before the save commits, after it was prepared.
async () => {
await admitEffect(this.env, scope, "memory.saveUser");
}
);
return { version: saved.currentVersion };
} catch (error) {
Expand Down
Loading
Loading