@plico-ai/server 0.0.0-reserved.0 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/NOTICE +4 -0
  2. package/README.md +8 -3
  3. package/dist/api.d.ts +32 -0
  4. package/dist/api.js +87 -0
  5. package/dist/app/assemble.d.ts +10 -0
  6. package/dist/app/assemble.js +49 -0
  7. package/dist/app/errors.d.ts +6 -0
  8. package/dist/app/errors.js +24 -0
  9. package/dist/app/events.d.ts +9 -0
  10. package/dist/app/events.js +52 -0
  11. package/dist/app/metadata.d.ts +4 -0
  12. package/dist/app/metadata.js +24 -0
  13. package/dist/app/runs.d.ts +31 -0
  14. package/dist/app/runs.js +81 -0
  15. package/dist/app/scope.d.ts +8 -0
  16. package/dist/app/scope.js +18 -0
  17. package/dist/app/threads.d.ts +15 -0
  18. package/dist/app/threads.js +133 -0
  19. package/dist/app/types.d.ts +103 -0
  20. package/dist/app/types.js +1 -0
  21. package/dist/body.d.ts +4 -0
  22. package/dist/body.js +54 -0
  23. package/dist/create-plico.d.ts +18 -0
  24. package/dist/create-plico.js +75 -0
  25. package/dist/errors.d.ts +7 -0
  26. package/dist/errors.js +19 -0
  27. package/dist/index.d.ts +6 -0
  28. package/dist/index.js +3 -0
  29. package/dist/listener.d.ts +6 -0
  30. package/dist/listener.js +8 -0
  31. package/dist/mount.d.ts +32 -0
  32. package/dist/mount.js +34 -0
  33. package/dist/origin-guard.d.ts +15 -0
  34. package/dist/origin-guard.js +39 -0
  35. package/dist/project.d.ts +2 -0
  36. package/dist/project.js +10 -0
  37. package/dist/recovery.d.ts +19 -0
  38. package/dist/recovery.js +84 -0
  39. package/dist/responses.d.ts +1 -0
  40. package/dist/responses.js +6 -0
  41. package/dist/routes/artifacts.d.ts +2 -0
  42. package/dist/routes/artifacts.js +165 -0
  43. package/dist/routes/eval.d.ts +2 -0
  44. package/dist/routes/eval.js +38 -0
  45. package/dist/routes/health.d.ts +2 -0
  46. package/dist/routes/health.js +8 -0
  47. package/dist/routes/index.d.ts +11 -0
  48. package/dist/routes/index.js +72 -0
  49. package/dist/routes/metadata-query.d.ts +4 -0
  50. package/dist/routes/metadata-query.js +13 -0
  51. package/dist/routes/project.d.ts +2 -0
  52. package/dist/routes/project.js +23 -0
  53. package/dist/routes/readiness.d.ts +2 -0
  54. package/dist/routes/readiness.js +17 -0
  55. package/dist/routes/runs.d.ts +2 -0
  56. package/dist/routes/runs.js +82 -0
  57. package/dist/routes/threads.d.ts +2 -0
  58. package/dist/routes/threads.js +60 -0
  59. package/dist/routes/types.d.ts +30 -0
  60. package/dist/routes/types.js +1 -0
  61. package/dist/script.d.ts +8 -0
  62. package/dist/script.js +35 -0
  63. package/dist/serve-static.d.ts +2 -0
  64. package/dist/serve-static.js +37 -0
  65. package/dist/server.d.ts +4 -0
  66. package/dist/server.js +25 -0
  67. package/dist/sse.d.ts +3 -0
  68. package/dist/sse.js +12 -0
  69. package/dist/stream.d.ts +13 -0
  70. package/dist/stream.js +58 -0
  71. package/dist/thread-failure.d.ts +7 -0
  72. package/dist/thread-failure.js +42 -0
  73. package/dist/thread-titles.d.ts +11 -0
  74. package/dist/thread-titles.js +37 -0
  75. package/dist/types.d.ts +12 -0
  76. package/dist/types.js +1 -0
  77. package/package.json +37 -8
@@ -0,0 +1,103 @@
1
+ import type { RequestListener } from "node:http";
2
+ import type { PlicoEvent, RunSummary, Thread, ThreadMessage } from "@plico-ai/contracts";
3
+ import type { DryRunScriptStep, Engine, EventStore, ModelProvider, ThreadStore } from "@plico-ai/runtime";
4
+ import type { MountOptions } from "../mount.js";
5
+ import type { Metadata } from "./metadata.js";
6
+ /** What the shared layer runs on. Internal: hosts pass a project folder to `createPlico` instead. */
7
+ export interface AppDeps {
8
+ projectRoot: string;
9
+ eventStore: EventStore;
10
+ threadStore?: ThreadStore;
11
+ provider?: ModelProvider;
12
+ providerRecoveryKey?: string;
13
+ /** Account-only identity used to resume queued runs after a model change. */
14
+ providerAccountKey?: string;
15
+ engine: Engine;
16
+ /**
17
+ * Set by a mounted handler for one request. New threads and runs carry it as their metadata, and
18
+ * every read and action refuses what it does not match.
19
+ */
20
+ scope?: Metadata;
21
+ /** Inspection only: the HTTP API answers reads and refuses every change. */
22
+ inspect?: true;
23
+ /** Set by a mounted handler for one request: host data for the runs and decisions it causes. */
24
+ context?: Metadata;
25
+ }
26
+ export interface EventFollowOptions {
27
+ /** Yield events after this sequence number. Defaults to the start of the run. */
28
+ after?: number;
29
+ /** Stops following. The iterator then ends without error. */
30
+ signal?: AbortSignal;
31
+ /** Milliseconds between reads of the event log. Defaults to 2000. */
32
+ pollMs?: number;
33
+ }
34
+ export interface SendMessageOptions {
35
+ /** Record the message without starting a run. Defaults to true. */
36
+ startRun?: boolean;
37
+ /**
38
+ * Host data passed to tools as `ctx.context` for the run this message starts, and recorded on its
39
+ * `run.started` event. It must not carry secrets.
40
+ */
41
+ context?: Metadata;
42
+ }
43
+ export interface Plico {
44
+ threads: {
45
+ /** `metadata` is host data kept with the thread, set now and never changed. */
46
+ create(options?: {
47
+ metadata?: Metadata;
48
+ }): Promise<Thread>;
49
+ get(threadId: string): Promise<Thread | undefined>;
50
+ /** `metadata` keeps threads whose top-level keys equal every given value. */
51
+ list(filter?: {
52
+ metadata?: Metadata;
53
+ }): Promise<Thread[]>;
54
+ /** Rejects with `thread.not_found` for an unknown thread. */
55
+ rename(threadId: string, title: string): Promise<Thread>;
56
+ messages(threadId: string): Promise<ThreadMessage[]>;
57
+ /**
58
+ * Records a user message and, unless `startRun` is false, starts the run that answers it. The
59
+ * returned message carries the `runId`. Rejects with `thread.busy` while another run is active.
60
+ */
61
+ send(threadId: string, content: string, options?: SendMessageOptions): Promise<ThreadMessage>;
62
+ };
63
+ runs: {
64
+ /**
65
+ * Starts a run that has no thread and no model, running the optional script against the
66
+ * project's tools. Provider-backed runs start through `threads.send`.
67
+ */
68
+ start(options?: {
69
+ script?: DryRunScriptStep[];
70
+ metadata?: Metadata;
71
+ /** Passed to tools as `ctx.context` and recorded on `run.started`. No secrets. */
72
+ context?: Metadata;
73
+ }): Promise<{
74
+ runId: string;
75
+ }>;
76
+ get(runId: string): Promise<RunSummary | undefined>;
77
+ /** `metadata` keeps runs whose top-level keys equal every given value. */
78
+ list(filter?: {
79
+ metadata?: Metadata;
80
+ }): Promise<RunSummary[]>;
81
+ cancel(runId: string): Promise<string>;
82
+ /** Follows a run's events from a cursor until it reaches a terminal state or is stopped. */
83
+ events(runId: string, options?: EventFollowOptions): AsyncGenerator<PlicoEvent, void, void>;
84
+ };
85
+ approvals: {
86
+ /**
87
+ * Delivers an approval decision to a waiting run. `context` describes who decided and is
88
+ * recorded on `approval.decided`. Returns the engine's signal result.
89
+ */
90
+ decide(runId: string, requestId: string, approved: boolean, options?: {
91
+ context?: Metadata;
92
+ }): Promise<string>;
93
+ };
94
+ /**
95
+ * The `/v1` API as a web-standard handler for Hono, Next.js, Bun and Deno. The host's own
96
+ * authentication runs first; `scope` then keeps each request to its own data.
97
+ */
98
+ fetch(options?: MountOptions): (request: Request) => Promise<Response>;
99
+ /** The `/v1` API as a Node handler for Express, Fastify and `http`. Honours a parsed `req.body`. */
100
+ node(options?: MountOptions): RequestListener;
101
+ /** Stops following events, waits for recovery and closes the stores Plico opened. */
102
+ close(): Promise<void>;
103
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/body.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ /** Like `readJsonBody`, but an empty body is `{}` so a bare POST still works. */
2
+ export declare function readOptionalJsonBody(request: Request): Promise<Record<string, unknown>>;
3
+ export declare function readJsonBody(request: Request): Promise<Record<string, unknown>>;
4
+ export declare const MAX_BODY_BYTES: number;
package/dist/body.js ADDED
@@ -0,0 +1,54 @@
1
+ import { HttpError } from "./errors.js";
2
+ /** Like `readJsonBody`, but an empty body is `{}` so a bare POST still works. */
3
+ export async function readOptionalJsonBody(request) {
4
+ const raw = await readBody(request);
5
+ return raw.trim() === "" ? {} : parseJsonBody(request, raw);
6
+ }
7
+ export async function readJsonBody(request) {
8
+ return parseJsonBody(request, await readBody(request));
9
+ }
10
+ function parseJsonBody(request, raw) {
11
+ const mediaType = (request.headers.get("content-type") ?? "").split(";")[0]?.trim().toLowerCase();
12
+ if (mediaType !== "application/json") {
13
+ throw new HttpError(415, "request.unsupported_media_type", "Send the request body as application/json.");
14
+ }
15
+ try {
16
+ const parsed = JSON.parse(raw);
17
+ if (!isRecord(parsed) || Array.isArray(parsed)) {
18
+ throw new Error("Expected JSON object body.");
19
+ }
20
+ return parsed;
21
+ }
22
+ catch {
23
+ throw new HttpError(400, "request.invalid_json", "Malformed JSON body.");
24
+ }
25
+ }
26
+ export const MAX_BODY_BYTES = 1024 * 1024;
27
+ async function readBody(request) {
28
+ const declared = Number(request.headers.get("content-length"));
29
+ if (declared > MAX_BODY_BYTES)
30
+ throw tooLarge();
31
+ const reader = request.body?.getReader();
32
+ if (!reader)
33
+ return "";
34
+ const chunks = [];
35
+ let size = 0;
36
+ for (;;) {
37
+ const { done, value } = await reader.read();
38
+ if (done)
39
+ break;
40
+ size += value.length;
41
+ if (size > MAX_BODY_BYTES) {
42
+ await reader.cancel();
43
+ throw tooLarge();
44
+ }
45
+ chunks.push(value);
46
+ }
47
+ return Buffer.concat(chunks).toString("utf8");
48
+ }
49
+ function tooLarge() {
50
+ return new HttpError(413, "request.too_large", "Request body is too large.");
51
+ }
52
+ function isRecord(value) {
53
+ return typeof value === "object" && value !== null;
54
+ }
@@ -0,0 +1,18 @@
1
+ import type { Plico } from "./app/types.js";
2
+ export interface CreatePlicoOptions {
3
+ /** The project folder. Its files define the agent; nothing else can. */
4
+ projectRoot: string;
5
+ /** SQLite file for threads, runs, events and the engine journal. */
6
+ databasePath: string;
7
+ }
8
+ /**
9
+ * Opens a Plico agent inside the calling process: stores, engine and provider come from the project
10
+ * and the database path. Runs a dead process left behind continue in the background, and queued
11
+ * runs are resumed before this returns. Call `close()` before the process exits.
12
+ */
13
+ export declare function createPlico(options: CreatePlicoOptions): Promise<Plico>;
14
+ /**
15
+ * `createPlico`, or with `inspect` a Plico that only reads: it recovers and resumes nothing, so a
16
+ * second process on a host's database cannot act on the host's runs.
17
+ */
18
+ export declare function openPlico(options: CreatePlicoOptions, inspect: boolean): Promise<Plico>;
@@ -0,0 +1,75 @@
1
+ import { validateProject } from "@plico-ai/core";
2
+ import { createConfiguredModelProvider, createEngine, createMemoryJournal, createSqliteEngine, createSqliteEventStore, createSqliteThreadStore, modelProviderAccountKey, modelProviderRecoveryKey, } from "@plico-ai/runtime";
3
+ import { assemblePlico } from "./app/assemble.js";
4
+ import { recoverPlicoState, resumeQueuedRuns } from "./recovery.js";
5
+ import { withThreadTitles } from "./thread-titles.js";
6
+ /**
7
+ * Opens a Plico agent inside the calling process: stores, engine and provider come from the project
8
+ * and the database path. Runs a dead process left behind continue in the background, and queued
9
+ * runs are resumed before this returns. Call `close()` before the process exits.
10
+ */
11
+ export function createPlico(options) {
12
+ return openPlico(options, false);
13
+ }
14
+ /**
15
+ * `createPlico`, or with `inspect` a Plico that only reads: it recovers and resumes nothing, so a
16
+ * second process on a host's database cannot act on the host's runs.
17
+ */
18
+ export async function openPlico(options, inspect) {
19
+ const unknown = Object.keys(options).filter((key) => key !== "projectRoot" && key !== "databasePath");
20
+ if (unknown.length > 0)
21
+ throw new TypeError(`createPlico accepts only projectRoot and databasePath; files define the agent. Unknown option: ${unknown.join(", ")}`);
22
+ const { projectRoot, databasePath } = options;
23
+ // Inspection opens the database read-only, so it cannot settle runs the host left unfinished.
24
+ const eventStore = createSqliteEventStore({ databasePath, readOnly: inspect });
25
+ const validation = await validateProject(projectRoot);
26
+ const leaseMs = validation.project?.config.execution?.leaseMs;
27
+ const engine = inspect
28
+ ? createEngine({ journal: createMemoryJournal() })
29
+ : createSqliteEngine({
30
+ databasePath,
31
+ ...(leaseMs === undefined ? {} : { leaseMs }),
32
+ });
33
+ const model = validation.project?.config.model;
34
+ const provider = model ? createConfiguredModelProvider(model, options.projectRoot) : undefined;
35
+ const providerRecoveryKey = model ? modelProviderRecoveryKey(model) : undefined;
36
+ const providerAccountKey = model ? modelProviderAccountKey(model) : undefined;
37
+ const threadStore = createSqliteThreadStore({ databasePath, readOnly: inspect });
38
+ let closing = false;
39
+ const recoveryOptions = {
40
+ projectRoot,
41
+ eventStore: withThreadTitles({ projectRoot, eventStore, threadStore, provider }),
42
+ threadStore,
43
+ engine,
44
+ ...(provider ? { provider } : {}),
45
+ ...(providerRecoveryKey ? { providerRecoveryKey } : {}),
46
+ isClosed: () => closing,
47
+ };
48
+ if (!inspect) {
49
+ void recoverPlicoState(recoveryOptions).catch((error) => console.error(`Recovery incomplete: ${error instanceof Error ? error.message : String(error)}`));
50
+ try {
51
+ await resumeQueuedRuns({
52
+ ...recoveryOptions,
53
+ ...(providerAccountKey ? { providerAccountKey } : {}),
54
+ });
55
+ }
56
+ catch (error) {
57
+ closing = true;
58
+ threadStore.close();
59
+ throw error;
60
+ }
61
+ }
62
+ return assemblePlico({
63
+ projectRoot,
64
+ eventStore,
65
+ threadStore,
66
+ engine,
67
+ ...(provider ? { provider } : {}),
68
+ ...(providerRecoveryKey ? { providerRecoveryKey } : {}),
69
+ ...(providerAccountKey ? { providerAccountKey } : {}),
70
+ ...(inspect ? { inspect: true } : {}),
71
+ }, () => {
72
+ closing = true;
73
+ threadStore.close();
74
+ });
75
+ }
@@ -0,0 +1,7 @@
1
+ export declare class HttpError extends Error {
2
+ readonly status: number;
3
+ readonly code: string;
4
+ constructor(status: number, code: string, message: string);
5
+ }
6
+ export declare function errorResponse(status: number, code: string, message: string, headers?: Record<string, string>): Response;
7
+ export declare function errorMessage(error: unknown): string;
package/dist/errors.js ADDED
@@ -0,0 +1,19 @@
1
+ import { jsonResponse } from "./responses.js";
2
+ export class HttpError extends Error {
3
+ status;
4
+ code;
5
+ constructor(status, code, message) {
6
+ super(message);
7
+ this.status = status;
8
+ this.code = code;
9
+ }
10
+ }
11
+ export function errorResponse(status, code, message, headers) {
12
+ return jsonResponse(status, { error: { code, message } }, headers);
13
+ }
14
+ export function errorMessage(error) {
15
+ if (error instanceof Error && error.message) {
16
+ return error.message;
17
+ }
18
+ return String(error);
19
+ }
@@ -0,0 +1,6 @@
1
+ export { PlicoError } from "./app/errors.js";
2
+ export type { EventFollowOptions, Plico, SendMessageOptions, } from "./app/types.js";
3
+ export { type CreatePlicoOptions, createPlico } from "./create-plico.js";
4
+ export type { MountOptions } from "./mount.js";
5
+ export { servePlico } from "./server.js";
6
+ export type { ServePlicoOptions } from "./types.js";
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { PlicoError } from "./app/errors.js";
2
+ export { createPlico } from "./create-plico.js";
3
+ export { servePlico } from "./server.js";
@@ -0,0 +1,6 @@
1
+ import type { RequestListener } from "node:http";
2
+ import { type ListenerOptions } from "./api.js";
3
+ import type { Plico } from "./app/types.js";
4
+ export type { ListenerOptions } from "./api.js";
5
+ /** `createApi` for a Node server. It never replaces the global `Request` or `Response`. */
6
+ export declare function createRequestListener(plico: Plico, options?: ListenerOptions): RequestListener;
@@ -0,0 +1,8 @@
1
+ import { getRequestListener } from "@hono/node-server";
2
+ import { createApi } from "./api.js";
3
+ import { internalsOf } from "./app/assemble.js";
4
+ /** `createApi` for a Node server. It never replaces the global `Request` or `Response`. */
5
+ export function createRequestListener(plico, options = {}) {
6
+ const api = createApi(internalsOf(plico), options);
7
+ return getRequestListener((request) => api.fetch(request), { overrideGlobalObjects: false });
8
+ }
@@ -0,0 +1,32 @@
1
+ import type { RequestListener } from "node:http";
2
+ import { type RequestData } from "./api.js";
3
+ import type { PlicoInternals } from "./app/assemble.js";
4
+ /** Options for `plico.fetch()` and `plico.node()`: the API mounted inside a host app. */
5
+ export interface MountOptions {
6
+ /**
7
+ * The host's answer to "whose data is this request about", such as `{ userId }`. Plico stamps it
8
+ * on new threads and runs as their metadata, and refuses every read and action on anything it
9
+ * does not match. A client cannot widen it. It also turns off the project-level routes
10
+ * (`/v1/project`, `/v1/instructions`, `/v1/eval`, `/v1/readiness`, `/v1/runs/dry`). A function
11
+ * that throws or returns anything but a JSON object fails the request with a 500.
12
+ */
13
+ scope?: RequestData;
14
+ /**
15
+ * Host data for the runs and approval decisions a request causes: tools read it as `ctx.context`
16
+ * and `run.started` records it. It must not carry secrets.
17
+ */
18
+ context?: RequestData;
19
+ /** The path the API is mounted under when the host does not strip it, such as `/plico`. */
20
+ basePath?: string;
21
+ /** The origin browsers use to reach the host, when it differs from the request URL. */
22
+ publicOrigin?: string;
23
+ /** Accept a `script` on `POST /v1/runs`. Off by default. */
24
+ allowScript?: boolean;
25
+ }
26
+ /** A web-standard handler, for Hono, Next.js route handlers, Bun and Deno. */
27
+ export declare function createFetchHandler(internals: PlicoInternals, options?: MountOptions): (request: Request) => Promise<Response>;
28
+ /**
29
+ * A Node `(req, res)` handler, for Express, Fastify and plain `http`. A body the host already
30
+ * parsed into `req.body` (such as with `express.json()`) is used instead of the request stream.
31
+ */
32
+ export declare function createNodeHandler(internals: PlicoInternals, options?: MountOptions): RequestListener;
package/dist/mount.js ADDED
@@ -0,0 +1,34 @@
1
+ import { getRequestListener } from "@hono/node-server";
2
+ import { createApi } from "./api.js";
3
+ function apiOptions(options) {
4
+ // The host authenticates the request, so the loopback Host rule does not apply.
5
+ return { ...options, allowRemote: true };
6
+ }
7
+ /** A web-standard handler, for Hono, Next.js route handlers, Bun and Deno. */
8
+ export function createFetchHandler(internals, options = {}) {
9
+ const api = createApi(internals, apiOptions(options));
10
+ return async (request) => api.fetch(request);
11
+ }
12
+ /**
13
+ * A Node `(req, res)` handler, for Express, Fastify and plain `http`. A body the host already
14
+ * parsed into `req.body` (such as with `express.json()`) is used instead of the request stream.
15
+ */
16
+ export function createNodeHandler(internals, options = {}) {
17
+ const api = createApi(internals, apiOptions(options));
18
+ return getRequestListener((request, env) => {
19
+ const incoming = env.incoming;
20
+ return api.fetch(withParsedBody(request, incoming?.body));
21
+ }, { overrideGlobalObjects: false });
22
+ }
23
+ function withParsedBody(request, parsed) {
24
+ if (parsed === undefined || request.method === "GET" || request.method === "HEAD")
25
+ return request;
26
+ const headers = new Headers(request.headers);
27
+ headers.delete("content-length");
28
+ const body = typeof parsed === "string"
29
+ ? parsed
30
+ : parsed instanceof Uint8Array
31
+ ? new TextDecoder().decode(parsed)
32
+ : JSON.stringify(parsed);
33
+ return new Request(request.url, { method: request.method, headers, body });
34
+ }
@@ -0,0 +1,15 @@
1
+ export interface GuardOptions {
2
+ allowRemote: boolean;
3
+ /** Origin browsers see, such as when TLS ends at a proxy. Defaults to connection scheme plus Host. */
4
+ publicOrigin?: string | undefined;
5
+ }
6
+ /**
7
+ * Keeps a web page in the developer's browser from driving a local Plico server.
8
+ *
9
+ * Plico has no authentication, so a loopback bind is the only protection. A page can still reach
10
+ * it by sending a cross-origin request, or by pointing its own domain at 127.0.0.1 (DNS
11
+ * rebinding). Browsers always send `Origin` on cross-origin requests and a `Host` that names the
12
+ * attacker's domain, so both are checked. `Origin` must match in scheme, host and port. Clients
13
+ * that send no `Origin`, such as curl and the CLI, are unaffected.
14
+ */
15
+ export declare function assertTrustedRequest(request: Request, options: GuardOptions): void;
@@ -0,0 +1,39 @@
1
+ import { isIPv4 } from "node:net";
2
+ import { HttpError } from "./errors.js";
3
+ /**
4
+ * Keeps a web page in the developer's browser from driving a local Plico server.
5
+ *
6
+ * Plico has no authentication, so a loopback bind is the only protection. A page can still reach
7
+ * it by sending a cross-origin request, or by pointing its own domain at 127.0.0.1 (DNS
8
+ * rebinding). Browsers always send `Origin` on cross-origin requests and a `Host` that names the
9
+ * attacker's domain, so both are checked. `Origin` must match in scheme, host and port. Clients
10
+ * that send no `Origin`, such as curl and the CLI, are unaffected.
11
+ */
12
+ export function assertTrustedRequest(request, options) {
13
+ const { allowRemote, publicOrigin } = options;
14
+ const own = new URL(request.url);
15
+ if (!allowRemote && !isLoopbackHost(hostname(own.host))) {
16
+ throw new HttpError(421, "request.host_not_allowed", "Host not allowed: this Plico server only answers requests addressed to a loopback host.");
17
+ }
18
+ const origin = request.headers.get("origin");
19
+ if (origin !== null && !isSameOrigin(origin, publicOrigin ?? own.origin)) {
20
+ throw new HttpError(403, "request.origin_not_allowed", "Cross-origin requests to this Plico server are not allowed.");
21
+ }
22
+ }
23
+ function isSameOrigin(origin, expected) {
24
+ try {
25
+ return new URL(origin).origin === new URL(expected).origin;
26
+ }
27
+ catch {
28
+ return false;
29
+ }
30
+ }
31
+ function hostname(host) {
32
+ const bracketed = /^\[([^\]]+)\]/.exec(host);
33
+ if (bracketed?.[1])
34
+ return bracketed[1].toLowerCase();
35
+ return (host.split(":")[0] ?? "").toLowerCase();
36
+ }
37
+ function isLoopbackHost(name) {
38
+ return name === "localhost" || name === "::1" || (isIPv4(name) && name.startsWith("127."));
39
+ }
@@ -0,0 +1,2 @@
1
+ import { type LoadedProject } from "@plico-ai/core";
2
+ export declare function loadProjectOrThrow(projectRoot: string): Promise<LoadedProject>;
@@ -0,0 +1,10 @@
1
+ import { validateProject } from "@plico-ai/core";
2
+ import { HttpError } from "./errors.js";
3
+ export async function loadProjectOrThrow(projectRoot) {
4
+ const validation = await validateProject(projectRoot);
5
+ if (!validation.ok || !validation.project) {
6
+ const message = validation.errors.map((issue) => `${issue.path}: ${issue.message}`).join("; ");
7
+ throw new HttpError(400, "project.invalid", message || "Invalid project.");
8
+ }
9
+ return validation.project;
10
+ }
@@ -0,0 +1,19 @@
1
+ import { type Engine, type EventStore, type ModelProvider, type ThreadStore } from "@plico-ai/runtime";
2
+ interface RecoveryOptions {
3
+ projectRoot: string;
4
+ eventStore: EventStore;
5
+ threadStore?: ThreadStore;
6
+ engine: Engine;
7
+ provider?: ModelProvider;
8
+ providerRecoveryKey?: string;
9
+ /** Stops recovery from touching stores a shutting-down server is closing. */
10
+ isClosed?: () => boolean;
11
+ }
12
+ interface ResumeQueuedOptions extends RecoveryOptions {
13
+ providerAccountKey?: string;
14
+ }
15
+ /** Continue runs a dead process left behind, settling any failed thread reservations. */
16
+ export declare function recoverPlicoState(options: RecoveryOptions): Promise<void>;
17
+ /** Start thread runs that were reserved but never reached the engine. */
18
+ export declare function resumeQueuedRuns(options: ResumeQueuedOptions): Promise<void>;
19
+ export {};
@@ -0,0 +1,84 @@
1
+ import { resolve } from "node:path";
2
+ import { recoverRuns, runProject, } from "@plico-ai/runtime";
3
+ import { finishFailedThreadRun } from "./thread-failure.js";
4
+ /** Continue runs a dead process left behind, settling any failed thread reservations. */
5
+ export async function recoverPlicoState(options) {
6
+ const dryRuns = recoverRuns({
7
+ engine: options.engine,
8
+ eventStore: options.eventStore,
9
+ recoveryFilter: { projectRoot: options.projectRoot },
10
+ });
11
+ const modelRuns = options.provider && options.providerRecoveryKey
12
+ ? recoverRuns({
13
+ engine: options.engine,
14
+ eventStore: options.eventStore,
15
+ provider: options.provider,
16
+ recoveryFilter: {
17
+ projectRoot: options.projectRoot,
18
+ providerRecoveryKey: options.providerRecoveryKey,
19
+ providerOnly: true,
20
+ },
21
+ })
22
+ : Promise.resolve([]);
23
+ const [dry, model] = await Promise.all([dryRuns, modelRuns]);
24
+ for (const run of [...dry, ...model]) {
25
+ if (run.state !== "failed")
26
+ continue;
27
+ console.error(`Recovered run ${run.runId}: failed${run.error ? ` (${run.error})` : ""}`);
28
+ if (!options.threadStore || options.isClosed?.())
29
+ continue;
30
+ const threadId = await options.threadStore.getThreadForRun(run.runId);
31
+ if (!threadId || options.isClosed?.())
32
+ continue;
33
+ const metadata = (await options.threadStore.getThread(threadId))?.metadata;
34
+ await finishFailedThreadRun(run.runId, threadId, options.projectRoot, options.engine, options.eventStore, run.error, metadata);
35
+ }
36
+ }
37
+ /** Start thread runs that were reserved but never reached the engine. */
38
+ export async function resumeQueuedRuns(options) {
39
+ if (!options.provider || !options.providerAccountKey || !options.threadStore)
40
+ return;
41
+ if (!options.threadStore.storageIdentity ||
42
+ options.threadStore.storageIdentity !== options.eventStore.storageIdentity)
43
+ throw new Error("Thread and run stores must use the same database.");
44
+ const queued = await options.threadStore.listQueuedRuns();
45
+ for (const run of queued) {
46
+ if (options.isClosed?.())
47
+ return;
48
+ if (!run.projectRoot ||
49
+ identity(run.projectRoot) !== identity(options.projectRoot) ||
50
+ run.providerKey !== options.providerAccountKey)
51
+ continue;
52
+ const metadata = (await options.threadStore.getThread(run.threadId))?.metadata;
53
+ const status = await options.engine.status(run.runId);
54
+ if (status) {
55
+ if (status.state === "failed" || status.state === "cancelled" || status.state === "completed")
56
+ await finishFailedThreadRun(run.runId, run.threadId, options.projectRoot, options.engine, options.eventStore, status.state === "failed" ? "Queued run failed before it could be resumed." : undefined, metadata).catch((error) => console.error("Could not settle queued thread run", error));
57
+ continue;
58
+ }
59
+ void runProject(options.projectRoot, {
60
+ runId: run.runId,
61
+ threadId: run.threadId,
62
+ ...(metadata === undefined ? {} : { metadata }),
63
+ ...(run.context === undefined ? {} : { context: run.context }),
64
+ history: run.history,
65
+ message: run.content,
66
+ provider: options.provider,
67
+ providerRecoveryKey: options.providerRecoveryKey ?? options.providerAccountKey,
68
+ engine: options.engine,
69
+ eventStore: options.eventStore,
70
+ }).catch(async (error) => {
71
+ try {
72
+ await finishFailedThreadRun(run.runId, run.threadId, options.projectRoot, options.engine, options.eventStore, error, metadata);
73
+ }
74
+ catch (settleError) {
75
+ console.error("Could not record failed queued thread run", settleError);
76
+ }
77
+ console.error(`Queued run ${run.runId} failed: ${error instanceof Error ? error.message : String(error)}`);
78
+ });
79
+ }
80
+ }
81
+ function identity(path) {
82
+ const absolute = resolve(path);
83
+ return process.platform === "win32" ? absolute.toLowerCase() : absolute;
84
+ }
@@ -0,0 +1 @@
1
+ export declare function jsonResponse(status: number, body: unknown, headers?: Record<string, string>): Response;
@@ -0,0 +1,6 @@
1
+ export function jsonResponse(status, body, headers) {
2
+ return new Response(JSON.stringify(body), {
3
+ status,
4
+ headers: { "content-type": "application/json; charset=utf-8", ...headers },
5
+ });
6
+ }
@@ -0,0 +1,2 @@
1
+ import type { Route } from "./types.js";
2
+ export declare const artifactRoutes: Route[];