-
Notifications
You must be signed in to change notification settings - Fork 0
π Serve the plan review: type its schema, and open it as the host #675
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
019dfa3
f74ceaf
1cdca2f
2f3852b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -49,6 +49,7 @@ import { createAcpxProvider } from "@executablemd/acp"; | |||||
| import type { AcpxProviderDependencies } from "@executablemd/acp"; | ||||||
| import { InMemoryStream } from "@executablemd/durable-streams"; | ||||||
| import { API } from "@executablemd/runtime"; | ||||||
| import { FormOpener } from "@executablemd/web"; | ||||||
|
|
||||||
| import { hostAcpDependencies } from "./agent-stack.ts"; | ||||||
| import type { AgentStack } from "./agent-stack.ts"; | ||||||
|
|
@@ -143,10 +144,12 @@ export function* runPlanCommandDocument(profile: AuthorshipProfile): Operation<R | |||||
| ); | ||||||
| } | ||||||
|
|
||||||
| // Taken before the profile's own scope, because putting this build's adapter | ||||||
| // on disk is host work and the profile refuses a command to everything inside | ||||||
| // it. The refusals are installed on the scope below, so this one still answers | ||||||
| // with the capabilities the entrypoint gave this invocation. | ||||||
| // Taken before the profile's own scope, because two of the things this command | ||||||
| // does are the host's rather than the document's β putting this build's adapter | ||||||
| // on disk, and opening the review form β and both run a command, which the | ||||||
| // profile refuses to everything inside it. The refusals are installed on the | ||||||
| // scope below, so this one still answers with the capabilities the entrypoint | ||||||
| // gave this invocation. | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Redundant comment β restates what the code does.
Suggested change
|
||||||
| const host = yield* useScope(); | ||||||
|
|
||||||
| return yield* scoped(function* (): Operation<Result<string>> { | ||||||
|
|
@@ -161,7 +164,7 @@ export function* runPlanCommandDocument(profile: AuthorshipProfile): Operation<R | |||||
| } | ||||||
| const workdir = established.value; | ||||||
|
|
||||||
| yield* refuseDocumentCapabilities(); | ||||||
| yield* openFormsThroughHost(host); | ||||||
| yield* profile.installElicitation(); | ||||||
|
|
||||||
| const acpx = createAcpxProvider(authorshipCeiling(profile, workdir, host)); | ||||||
|
|
@@ -184,28 +187,38 @@ export function* runPlanCommandDocument(profile: AuthorshipProfile): Operation<R | |||||
|
|
||||||
| const source = yield* readPackagedDocument(PLAN_COMMAND_DOCUMENT); | ||||||
| try { | ||||||
| const approved = yield* collect( | ||||||
| yield* executeInstalled( | ||||||
| { | ||||||
| ...retainedSource(PLAN_COMMAND_IDENTITY, source), | ||||||
| // Invocation-owned and thrown away with the scope. Ordinary document | ||||||
| // and Prompt semantics need a durable stream; nothing about writing a | ||||||
| // Plan needs a durable one, and `--journal` belongs to the Plan you | ||||||
| // approved rather than to the conversation that wrote it. | ||||||
| stream: new InMemoryStream(), | ||||||
| // No repository component search. What the document may name is | ||||||
| // what this profile declares, so a file in the caller's tree cannot | ||||||
| // answer for `<CheckDraft>`, `<Prompt>` or anything else. | ||||||
| includes: [], | ||||||
| props: { | ||||||
| request: profile.request, | ||||||
| syntax: profile.syntax, | ||||||
| session: profile.session, | ||||||
| // The refusals go on a scope holding the document and nothing else, so | ||||||
| // what the ceiling covers is what it says it covers. It cannot separate | ||||||
| // the host's own acts from the document's on its own β a provider's work | ||||||
| // happens inside this execution, and `API.Process.exec` looks the same | ||||||
| // whichever party reached it β which is why those acts are stated above | ||||||
| // and run in the host's scope (src/host-acts.ts). | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Redundant comment β restates what the code does.
Suggested change
|
||||||
| const approved = yield* scoped(function* (): Operation<Json> { | ||||||
| yield* refuseDocumentCapabilities(); | ||||||
| return yield* collect( | ||||||
| yield* executeInstalled( | ||||||
| { | ||||||
| ...retainedSource(PLAN_COMMAND_IDENTITY, source), | ||||||
| // Invocation-owned and thrown away with the scope. Ordinary | ||||||
| // document and Prompt semantics need a durable stream; nothing | ||||||
| // about writing a Plan needs a durable one, and `--journal` | ||||||
| // belongs to the Plan you approved rather than to the | ||||||
| // conversation that wrote it. | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Redundant comment β restates what the code does.
Suggested change
|
||||||
| stream: new InMemoryStream(), | ||||||
| // No repository component search. What the document may name is | ||||||
| // what this profile declares, so a file in the caller's tree | ||||||
| // cannot answer for `<CheckDraft>`, `<Prompt>` or anything else. | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Redundant comment β restates what the code does.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Redundant comment β restates what the code does.
Suggested change
|
||||||
| includes: [], | ||||||
| props: { | ||||||
| request: profile.request, | ||||||
| syntax: profile.syntax, | ||||||
| session: profile.session, | ||||||
| }, | ||||||
| }, | ||||||
| }, | ||||||
| [{ components: [...agentIdentityComponents(), validator(profile)] }], | ||||||
| ), | ||||||
| ); | ||||||
| [{ components: [...agentIdentityComponents(), validator(profile)] }], | ||||||
| ), | ||||||
| ); | ||||||
| }); | ||||||
| if (typeof approved !== "string") { | ||||||
| return Err(new Error("the plan command document returned something that is not a Plan")); | ||||||
| } | ||||||
|
|
@@ -269,10 +282,10 @@ function validator(profile: AuthorshipProfile): IdentityComponent { | |||||
| * through ACPX's published pins and could reach neither (#672). | ||||||
| * | ||||||
| * Putting one on disk runs `npm install`, which is the one thing this profile | ||||||
| * refuses to everything inside it. So preparation runs in `host` β the scope | ||||||
| * this command was called in, which the refusals below were never installed on. | ||||||
| * The distinction is the whole of it: the document decides what to write and may | ||||||
| * run nothing, while the host installs the adapter it was always going to launch. | ||||||
| * refuses to everything inside it, so the host states that act as its own and it | ||||||
| * runs in `host` (src/host-acts.ts). The distinction is the whole of it: the | ||||||
| * document decides what to write and may run nothing, while the host installs | ||||||
| * the adapter it was always going to launch. | ||||||
| * | ||||||
| * Exported for the suite that pins exactly that: what a provider is built from | ||||||
| * is not observable through a provider, and a case that could only watch a turn | ||||||
|
|
@@ -302,12 +315,39 @@ export function authorshipCeiling( | |||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * Run one operation in a scope this one is nested inside, and wait for it there. | ||||||
| * Open this host's own review form the way this host opens anything. | ||||||
| * | ||||||
| * A ceiling refuses *ambiently*: the middleware sits on a scope, and a call | ||||||
| * carries no mark saying who made it β `API.Process.exec` looks the same whether | ||||||
| * an `exec` fence reached it or this host did. So showing a person the review, | ||||||
| * which runs `open`, `xdg-open` or `start`, was refused as though the document | ||||||
| * had asked, and `xmd plan` printed its URL and warned that it could not open it. | ||||||
| * | ||||||
| * The act is the host's: its provider asking its question, about a URL it is | ||||||
| * serving, decided by no document, agent or authored element. Only the opening | ||||||
| * moves β a file, a command, the network and a service stay refused β and a | ||||||
| * failed launch is still a warning printed beside a URL that stands on its own. | ||||||
| */ | ||||||
| function openFormsThroughHost(host: Scope): Operation<void> { | ||||||
| return FormOpener.around({ | ||||||
| *open([url], next): Operation<void> { | ||||||
| yield* inScope(host, () => next(url)); | ||||||
| }, | ||||||
| }); | ||||||
| } | ||||||
|
|
||||||
| /** | ||||||
| * Run one operation in a scope this one is nested inside, and wait for it here. | ||||||
| * | ||||||
| * The waiting is what makes it this operation's work: a task created in an outer | ||||||
| * scope outlives its creator by construction, so the halt is registered before | ||||||
| * the wait and an ended command takes an unfinished act with it rather than | ||||||
| * leaving one running under a conversation that is over. | ||||||
| * | ||||||
| * The wait is what makes it this operation's: a task created in an outer scope | ||||||
| * outlives the caller by construction, so the halt is registered before the wait | ||||||
| * and an ended command takes the work with it rather than leaving an install | ||||||
| * running under a conversation that is over. | ||||||
| * `@effectionx/scope-eval` answers a different question. Its worker decouples the | ||||||
| * call from the work β the operation finishes even when the caller is gone, which | ||||||
| * is what `persist`, `daemon` and `service` want from it and the opposite of what | ||||||
| * a host act wants. | ||||||
| */ | ||||||
| function* inScope<T>(scope: Scope, operation: () => Operation<T>): Operation<T> { | ||||||
| return yield* scoped(function* () { | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,102 @@ | ||
| /** | ||
| * Tier PH β the acts `xmd plan` performs as the host | ||
| * (specs/plan-command-spec.md Β§The authorship profile, | ||
| * specs/acp-client-spec.md Β§The `xmd plan` authorship profile). | ||
| * | ||
| * The profile refuses the command document a command, and two of the things the | ||
| * command itself does run one: it installs this build's ACP adapter, and it opens | ||
| * the review form in a browser. Neither is the document's act, and both were | ||
| * refused as though they were β the second one visibly, as | ||
| * `could not open a browser automatically (xmd plan asked for a command, β¦)` | ||
| * printed beside the URL a person then had to open by hand. | ||
| * | ||
| * These drive the real command with the real profile. What stands in for the | ||
| * outside world is the command itself: an `API.Process` recorder answers instead | ||
| * of spawning, installed at `min` so the profile's own refusal still outranks it | ||
| * wherever it applies β a recorder at full strength would answer for a refused | ||
| * call too, and these cases would pass against the defect. | ||
| */ | ||
|
|
||
| import { describe, it } from "@executablemd/test-support/bdd"; | ||
| import { expect } from "@executablemd/test-support/expect"; | ||
| import { join } from "node:path"; | ||
| import { API } from "@executablemd/runtime"; | ||
| import { FormOpener } from "@executablemd/web"; | ||
| import type { Operation } from "effection"; | ||
|
|
||
| import { runPlan } from "../src/plan.ts"; | ||
| import type { PlanCommand } from "../src/plan.ts"; | ||
| import { scanPlanArgs } from "../src/plan-args.ts"; | ||
| import type { AgentStack } from "../src/agent-stack.ts"; | ||
| import { ADAPTERS, AGENT, createPlanHarness, useWorkingDirectory } from "./support/plan-harness.ts"; | ||
| import type { PlanHarness } from "./support/plan-harness.ts"; | ||
|
|
||
| const REQUEST = "write a greeting"; | ||
|
|
||
| /** A Plan the host's validator accepts. */ | ||
| const PLAN = ['<File path="drafted.txt">the draft ran</File>', ""].join("\n"); | ||
|
|
||
| const STACK: AgentStack = { | ||
| provider: "acpx", | ||
| defaultAgent: AGENT, | ||
| permissionMode: "deny-all", | ||
| adapters: ADAPTERS, | ||
| }; | ||
|
|
||
| function writing(dir: string, output: string): PlanCommand { | ||
| const argv = ["plan", REQUEST]; | ||
| return { argv, scan: scanPlanArgs(argv), include: [dir], output, run: false, stack: STACK }; | ||
| } | ||
|
|
||
| /** Every command this invocation reached, answered rather than spawned. */ | ||
| function* recordCommands(commands: string[][]): Operation<void> { | ||
| yield* API.Process.around( | ||
| { | ||
| // deno-lint-ignore require-yield | ||
| *exec([options]) { | ||
| commands.push([...options.command]); | ||
| return { exitCode: 0, stdout: "", stderr: "" }; | ||
| }, | ||
| }, | ||
| { at: "min" }, | ||
| ); | ||
| } | ||
|
|
||
| /** | ||
| * A harness whose review opens a form first, the way the CLI's own does. | ||
| * | ||
| * `installWebElicitation` announces the URL and asks `FormOpener` to open it | ||
| * before it waits for an answer; this is that one act, without a port, a page or | ||
| * a browser. | ||
| */ | ||
| function openingHarness(harness: PlanHarness, url: string): PlanHarness { | ||
| const scripted = harness.deps.installElicitation; | ||
| harness.deps.installElicitation = function* (): Operation<void> { | ||
| yield* FormOpener.operations.open(url); | ||
| yield* scripted(); | ||
| }; | ||
| return harness; | ||
| } | ||
|
|
||
| describe("Tier PH β the acts xmd plan performs as the host", () => { | ||
| it("PH1: opening the review form reaches a command the document cannot", function* () { | ||
| yield* useWorkingDirectory(function* (dir, authorshipRoot) { | ||
| const commands: string[][] = []; | ||
| yield* recordCommands(commands); | ||
|
|
||
| const url = "http://127.0.0.1:0/f/token/"; | ||
| const harness = openingHarness(createPlanHarness({ authorshipRoot }), url); | ||
| harness.fake.script({ reply: PLAN }); | ||
| harness.script({ decision: "Approve" }); | ||
|
|
||
| const code = yield* runPlan(writing(dir, join(dir, "plan.md")), harness.deps); | ||
|
|
||
| // The command that opens a browser on this platform, whichever it is, with | ||
| // the URL the form is being served at. | ||
| expect(commands).toHaveLength(1); | ||
| expect(commands[0]).toContain(url); | ||
| expect(code).toBe(0); | ||
| expect(harness.reviews).toHaveLength(1); | ||
| }); | ||
| }); | ||
| }); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Redundant comment β restates what the code does.