Skip to content
Closed
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
9 changes: 9 additions & 0 deletions cli/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,15 @@ a candidate do not establish public availability or authorize publication.
the help no longer call the hold flat. `run quote --model` with a model the
service does not offer now returns `SERVICE_REQUEST_REJECTED` instead of
being ignored.
- Quotes are program-aware: the `run submit` plan posts its exact submission
body to the quote, `run quote FILE` posts the program and `run quote --from`
names it, and the `job create` plan names its pinned `program_ref`, so the
program's own run settings and declared tools count; explicit options
override them. `run quote` reports the service's `basis` (each value and its
source). The CLI still never inspects program text.
- `sync_service_interactions.py` accepts the service's public interaction
projection (`openprose.*-interactions/2`), which carries no principals
table; the vendored catalog is re-vendored from it.
- `job create` webhook specs accept `model`, `reasoning_effort`,
`repository_url`, `repository_branch` and `output` for the connected
contract (the service requires `program_ref` with them).
Expand Down
26 changes: 16 additions & 10 deletions cli/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -2478,14 +2478,20 @@ models, with `suggestedArgv` the same command using the nearest; a failed
lookup is advisory (the service decides) except an interrupt. `program draft`
quotes the run hold (`GET /run/quote` with no parameters, advisory) into its
plan. The hold depends on model, reasoning effort, environment, declared tools
and bound repositories. `run quote` and the `run submit` plan send only the
`model`, `reasoning_effort` and `environment` the user gave, plus
`repositories=1` for any `--repo` or `--commit-output`; the `job create` plan
sends the spec's `model`, `reasoning_effort` and `environment`, plus
`repositories=1` for a `repository_url` or `context_repository_url`. Nothing
is defaulted, the program is not inspected for the quote and tools are never
sent, so a quote does not cover the program's own run settings or declared
tools. `org
and bound repositories, and the service prices a program itself, so the CLI
never inspects program text. The `run submit` plan posts its exact submission
body to `POST /run/quote` with the `POST /run` query minus `live` and
`session`. `run quote FILE` posts `{content, model, reasoning_effort,
repositories, output}` (never inputs) with the same query; `run quote --from`
sends `GET /run/quote?program_ref=` (a bare SLUG or `@N` is resolved first, as
for `run submit`) with the given `model`, `reasoning_effort`, `environment` and
`repositories=1`; without either it sends only those parameters. The
`job create` plan sends the pinned `program_ref` with the spec's `model`,
`reasoning_effort` and `environment`, plus `repositories=1` for a
`repository_url` or `context_repository_url`. Nothing is defaulted; the
program quotes send the key when one is set (private programs need it). The
`run quote` result carries the service's `basis` (each value with its source:
request, program or default) when it sends one. `org
create` checks the service slug rule (1-63 lowercase letters, digits or
interior hyphens; not `openprose`, `system`, `default`, `invitations` or
UUID-shaped) before any request, suggesting a derived slug as `suggestedArgv`
Expand All @@ -2505,8 +2511,8 @@ the `--yes` help line reads `required because <confirmReason>.` and
<confirmReason>`; `render_service_help.py --check` and the manifest schema
refuse a missing, misplaced or circular reason. `run quote` results carry
`holdBasis` and its human output says what the hold depends on, that the
quote covers only the options given, and that the price is known only after
settlement.
quote covers the program and options given (`basis` names where each came
from), and that the price is known only after settlement.

**Transport.** Requests never follow redirects and are never retried, not even
GETs. Each operation has a transport class with bounded time and size:
Expand Down
4 changes: 2 additions & 2 deletions cli/bun/src/core/service/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,8 +63,8 @@ export interface HoldOptions {
/**
* The `GET /run/quote` query (manifest order): only what was given, never a
* default, so a quote with no such option stays parameter-free. Any bound
* repository sends `repositories=1`; declared tools are never sent, because
* the CLI does not read the program.
* repository sends `repositories=1`; declared tools are never sent: the CLI
* does not read the program (with a program_ref the service does).
*/
export function holdQuery(options: HoldOptions): Array<[string, string]> {
const query: Array<[string, string]> = [];
Expand Down
12 changes: 12 additions & 0 deletions cli/bun/src/core/service/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,18 @@ export class Context {
return failure("SERVICE_AUTH_REQUIRED", details);
}

/**
* Whether a credential is configured (the environment variable, else the OS
* store), for requests whose key is optional: they send it when there is
* one and go anonymous otherwise. An unreadable store counts as none.
*/
async credentialConfigured(): Promise<boolean> {
const value = this.deps.env[this.environment.credentialEnv];
if (value !== undefined && value !== "") return true;
try { return (await this.transport.storedCredential()) !== null; }
catch { return false; }
}

knownCredential(): string | undefined { return this.credential; }
/** Where the resolved credential came from, once resolved. */
credentialOrigin(): "environment" | "store" | undefined { return this.credentialSource; }
Expand Down
10 changes: 7 additions & 3 deletions cli/bun/src/core/service/jobs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -591,13 +591,17 @@ function specString(spec: JsonObject, key: string): string | undefined {
}

/**
* The anonymous GET /run/quote hold for the confirmation plan, quoted from the
* spec's model, reasoning effort, environment and bound repositories (only
* those the spec gives); the price policy reference stays internal.
* The GET /run/quote hold for the confirmation plan, quoted for the pinned
* program the job stores (so the service applies its run settings and
* declared tools) and the spec's model, reasoning effort, environment and
* bound repositories (only those the spec gives); the price policy reference
* stays internal.
*/
async function quote(context: Context, spec: JsonObject): Promise<JsonObject> {
const repository = (key: string): boolean => specString(spec, key) !== undefined;
const request = requestFor(context.operation, 0, "/run/quote");
const programRef = specString(spec, "program_ref");
if (programRef !== undefined) request.query.push(["program_ref", programRef]);
request.query.push(...holdQuery({
model: specString(spec, "model"),
reasoningEffort: specString(spec, "reasoning_effort"),
Expand Down
12 changes: 11 additions & 1 deletion cli/bun/src/core/service/program-ref.ts
Original file line number Diff line number Diff line change
Expand Up @@ -185,8 +185,10 @@ export async function resolveRevNumber(context: Context, reference: ProgramRef,
* caller's own program must be one of its revisions (a commit id is named as
* such, with the rev_id to pass), and a latest reference reads the newest
* rev_id. Another owner's pinned reference is checked by the service.
* Without a `read` request (`run quote`), a bare SLUG pins the newest
* revision the caller's listing names.
*/
export async function resolveToRun(context: Context, reference: ProgramRef, list: number, read: number, option: string | undefined, verifyPinned: boolean): Promise<string> {
export async function resolveToRun(context: Context, reference: ProgramRef, list: number, read: number | undefined, option: string | undefined, verifyPinned: boolean): Promise<string> {
const given = option === undefined ? "" : context.option(option) ?? "";
if (reference.owner === "" || reference.revNumber !== undefined || (verifyPinned && reference.rev !== undefined)) {
const slug = reference.slug;
Expand Down Expand Up @@ -220,6 +222,10 @@ export async function resolveToRun(context: Context, reference: ProgramRef, list
}
throw noRevision(context, name, slug, rev, own.revisions);
}
} else if (read === undefined && reference.rev === undefined) {
const newest = own.revisions[0]?.rev_id;
if (typeof newest !== "string" || !validRev(newest)) throw failure("SERVICE_PROTOCOL_INVALID");
reference.rev = newest;
}
} else {
if (reference.owner === "") throw missingOwn(context, slug, missing?.details ?? {});
Expand All @@ -231,6 +237,10 @@ export async function resolveToRun(context: Context, reference: ProgramRef, list
}
}
}
const fixed = pinned(reference);
if (fixed !== undefined) return fixed;
// Without a read request the service resolves the latest revision itself.
if (read === undefined) return `${reference.owner}/${reference.slug}`;
return (await resolveProgramRef(context, reference, read)).ref;
}

Expand Down
5 changes: 1 addition & 4 deletions cli/bun/src/core/service/programs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -162,10 +162,7 @@ async function list(context: Context): Promise<Json> {

/** `program show` reads anonymously when no credential is configured (public programs). */
async function hasCredential(context: Context): Promise<boolean> {
const value = context.deps.env[context.environment.credentialEnv];
if (value !== undefined && value !== "") return true;
try { return (await context.transport.storedCredential()) !== null; }
catch { return false; }
return await context.credentialConfigured();
}

function fileLine(label: string, file: JsonObject, target: string | undefined): string {
Expand Down
Loading
Loading