@bastani/atomic 0.9.10-alpha.1 → 0.9.11-alpha.1
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.
- package/CHANGELOG.md +80 -0
- package/dist/builtin/cursor/CHANGELOG.md +12 -0
- package/dist/builtin/cursor/package.json +2 -2
- package/dist/builtin/intercom/CHANGELOG.md +21 -0
- package/dist/builtin/intercom/package.json +2 -2
- package/dist/builtin/mcp/CHANGELOG.md +17 -0
- package/dist/builtin/mcp/package.json +1 -1
- package/dist/builtin/subagents/CHANGELOG.md +27 -0
- package/dist/builtin/subagents/package.json +1 -1
- package/dist/builtin/web-access/CHANGELOG.md +17 -0
- package/dist/builtin/web-access/package.json +1 -1
- package/dist/builtin/workflows/CHANGELOG.md +93 -0
- package/dist/builtin/workflows/README.md +8 -3
- package/dist/builtin/workflows/builtin/deep-research-codebase.ts +1 -1
- package/dist/builtin/workflows/package.json +1 -1
- package/dist/builtin/workflows/src/durable/backend.ts +2 -2
- package/dist/builtin/workflows/src/durable/dbos-embedded-postgres-root.ts +173 -0
- package/dist/builtin/workflows/src/durable/dbos-embedded-postgres.ts +36 -12
- package/dist/builtin/workflows/src/durable/dbos-lifecycle.ts +9 -3
- package/dist/builtin/workflows/src/durable/factory.ts +35 -6
- package/dist/builtin/workflows/src/durable/local-command.ts +11 -2
- package/dist/builtin/workflows/src/extension/extension-lifecycle.ts +14 -2
- package/dist/builtin/workflows/src/extension/public-types.ts +3 -4
- package/dist/builtin/workflows/src/extension/render-call.ts +4 -0
- package/dist/builtin/workflows/src/extension/render-result.ts +31 -1
- package/dist/builtin/workflows/src/extension/workflow-prompts.ts +5 -2
- package/dist/builtin/workflows/src/extension/workflow-schema.ts +3 -2
- package/dist/builtin/workflows/src/extension/workflow-tool-content.ts +1 -0
- package/dist/builtin/workflows/src/extension/workflow-tool-control.ts +88 -2
- package/dist/builtin/workflows/src/extension/workflow-tool.ts +13 -0
- package/dist/core/agent-session-events.d.ts.map +1 -1
- package/dist/core/agent-session-events.js +2 -1
- package/dist/core/agent-session-events.js.map +1 -1
- package/dist/core/agent-session-retry.d.ts.map +1 -1
- package/dist/core/agent-session-retry.js +7 -0
- package/dist/core/agent-session-retry.js.map +1 -1
- package/dist/core/agent-session-runtime.d.ts +13 -0
- package/dist/core/agent-session-runtime.d.ts.map +1 -1
- package/dist/core/agent-session-runtime.js +12 -0
- package/dist/core/agent-session-runtime.js.map +1 -1
- package/dist/core/atomic-guide-command.d.ts.map +1 -1
- package/dist/core/atomic-guide-command.js +2 -0
- package/dist/core/atomic-guide-command.js.map +1 -1
- package/dist/core/auth-storage-backends.d.ts +4 -0
- package/dist/core/auth-storage-backends.d.ts.map +1 -1
- package/dist/core/auth-storage-backends.js +48 -3
- package/dist/core/auth-storage-backends.js.map +1 -1
- package/dist/core/auth-storage.d.ts.map +1 -1
- package/dist/core/auth-storage.js +27 -11
- package/dist/core/auth-storage.js.map +1 -1
- package/dist/core/codex-errors.d.ts +3 -0
- package/dist/core/codex-errors.d.ts.map +1 -0
- package/dist/core/codex-errors.js +12 -0
- package/dist/core/codex-errors.js.map +1 -0
- package/dist/core/slash-commands.js +1 -1
- package/dist/core/slash-commands.js.map +1 -1
- package/dist/modes/interactive/interactive-auth-routing.d.ts +2 -1
- package/dist/modes/interactive/interactive-auth-routing.d.ts.map +1 -1
- package/dist/modes/interactive/interactive-auth-routing.js +10 -5
- package/dist/modes/interactive/interactive-auth-routing.js.map +1 -1
- package/dist/modes/interactive/interactive-autocomplete.js +12 -2
- package/dist/modes/interactive/interactive-autocomplete.js.map +1 -1
- package/dist/modes/interactive-engine/extension-ui-bridge.d.ts +3 -1
- package/dist/modes/interactive-engine/extension-ui-bridge.d.ts.map +1 -1
- package/dist/modes/interactive-engine/extension-ui-bridge.js +6 -0
- package/dist/modes/interactive-engine/extension-ui-bridge.js.map +1 -1
- package/dist/modes/interactive-engine/isolated-runtime.d.ts +4 -5
- package/dist/modes/interactive-engine/isolated-runtime.d.ts.map +1 -1
- package/dist/modes/interactive-engine/isolated-runtime.js +14 -24
- package/dist/modes/interactive-engine/isolated-runtime.js.map +1 -1
- package/dist/modes/interactive-engine/remote-model-catalog.d.ts +15 -0
- package/dist/modes/interactive-engine/remote-model-catalog.d.ts.map +1 -0
- package/dist/modes/interactive-engine/remote-model-catalog.js +67 -0
- package/dist/modes/interactive-engine/remote-model-catalog.js.map +1 -0
- package/dist/modes/rpc/rpc-client-api.d.ts +8 -1
- package/dist/modes/rpc/rpc-client-api.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-client-api.js +9 -0
- package/dist/modes/rpc/rpc-client-api.js.map +1 -1
- package/dist/modes/rpc/rpc-command-handler.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-command-handler.js +30 -0
- package/dist/modes/rpc/rpc-command-handler.js.map +1 -1
- package/dist/modes/rpc/rpc-types.d.ts +67 -7
- package/dist/modes/rpc/rpc-types.d.ts.map +1 -1
- package/dist/modes/rpc/rpc-types.js.map +1 -1
- package/docs/docs.json +9 -1
- package/docs/models/artificial-analysis-index.md +82 -0
- package/docs/models/model-selection.md +63 -0
- package/docs/models/pareto-efficiency.md +64 -0
- package/docs/providers.md +5 -1
- package/docs/quickstart.md +1 -1
- package/docs/rpc.md +27 -0
- package/docs/workflows.md +20 -7
- package/npm-shrinkwrap.json +24 -24
- package/package.json +4 -4
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Root-execution support for the embedded DBOS Postgres.
|
|
3
|
+
*
|
|
4
|
+
* PostgreSQL categorically refuses to run `initdb`/`postgres` as UID 0, so a
|
|
5
|
+
* root Atomic process (common in containers, CI sandboxes, and eval harnesses)
|
|
6
|
+
* cannot provision the embedded cluster directly. On Linux we instead resolve
|
|
7
|
+
* an unprivileged system account, keep the cluster under a root-safe base
|
|
8
|
+
* directory (`/root` is mode 0700 and untraversable by that account), and run
|
|
9
|
+
* every Postgres command with dropped privileges.
|
|
10
|
+
*
|
|
11
|
+
* Privilege dropping is strategy-probed at runtime: Node honors the child
|
|
12
|
+
* process `uid`/`gid` spawn options, but Bun currently ignores them, so we
|
|
13
|
+
* verify the drop actually happens (`id -u` must report the owner) and fall
|
|
14
|
+
* back to `setpriv`, `runuser`, or `su` wrappers when it does not.
|
|
15
|
+
*
|
|
16
|
+
* The embedded binaries themselves may also live under an untraversable
|
|
17
|
+
* prefix (for example a root-owned `~/.nvm` global install), so the caller can
|
|
18
|
+
* probe them as the unprivileged owner and fall back to a one-time copy into
|
|
19
|
+
* the cluster base directory.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { cpSync, existsSync } from "node:fs";
|
|
23
|
+
import { homedir } from "node:os";
|
|
24
|
+
import { dirname, join } from "node:path";
|
|
25
|
+
import { runLocalCommand, type LocalCommandResult } from "./local-command.js";
|
|
26
|
+
|
|
27
|
+
export interface EmbeddedPostgresOwner {
|
|
28
|
+
readonly uid: number;
|
|
29
|
+
readonly gid: number;
|
|
30
|
+
readonly name: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface EmbeddedPostgresRunContext {
|
|
34
|
+
/** Directory that holds the cluster, log file, and setup locks. */
|
|
35
|
+
readonly baseDir: string;
|
|
36
|
+
/** Present only when commands must drop privileges (Linux root). */
|
|
37
|
+
readonly owner?: EmbeddedPostgresOwner;
|
|
38
|
+
/** Runs a command as the owner; identity pass-through when no owner. */
|
|
39
|
+
readonly runAsOwner: LocalCommandRunner;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface EmbeddedPostgresBinaryPaths {
|
|
43
|
+
readonly pg_ctl: string;
|
|
44
|
+
readonly initdb: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export type LocalCommandRunner = (
|
|
48
|
+
command: string,
|
|
49
|
+
args: readonly string[],
|
|
50
|
+
options?: { readonly uid?: number; readonly gid?: number },
|
|
51
|
+
) => Promise<LocalCommandResult>;
|
|
52
|
+
|
|
53
|
+
/** Root-safe cluster location: system path, traversable by system accounts. */
|
|
54
|
+
export const ROOT_EMBEDDED_BASE_DIR = "/var/lib/atomic-postgres";
|
|
55
|
+
|
|
56
|
+
/** Unprivileged accounts tried in order; `postgres` wins when present. */
|
|
57
|
+
const OWNER_CANDIDATES = ["postgres", "nobody", "daemon"] as const;
|
|
58
|
+
|
|
59
|
+
export function defaultEmbeddedBaseDir(): string {
|
|
60
|
+
return join(homedir(), ".atomic", "postgres");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Resolve where and as whom the embedded cluster should run. Non-root (and
|
|
65
|
+
* every non-Linux platform) keeps the historical home-directory layout. Linux
|
|
66
|
+
* root without a resolvable unprivileged account, or without any working
|
|
67
|
+
* privilege-drop mechanism, also falls through to the default context so
|
|
68
|
+
* PostgreSQL's own root refusal surfaces with full detail.
|
|
69
|
+
*/
|
|
70
|
+
export async function resolveEmbeddedRunContext(
|
|
71
|
+
runner: LocalCommandRunner = runLocalCommand,
|
|
72
|
+
euid: number | undefined = process.getuid?.(),
|
|
73
|
+
platform: NodeJS.Platform = process.platform,
|
|
74
|
+
): Promise<EmbeddedPostgresRunContext> {
|
|
75
|
+
if (platform !== "linux" || euid !== 0) {
|
|
76
|
+
return { baseDir: defaultEmbeddedBaseDir(), runAsOwner: runner };
|
|
77
|
+
}
|
|
78
|
+
for (const name of OWNER_CANDIDATES) {
|
|
79
|
+
const owner = await lookupOwner(runner, name);
|
|
80
|
+
if (owner === undefined) continue;
|
|
81
|
+
const runAsOwner = await resolvePrivilegeDrop(runner, owner);
|
|
82
|
+
if (runAsOwner === undefined) continue;
|
|
83
|
+
return { baseDir: ROOT_EMBEDDED_BASE_DIR, owner, runAsOwner };
|
|
84
|
+
}
|
|
85
|
+
return { baseDir: defaultEmbeddedBaseDir(), runAsOwner: runner };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Return a runner that verifiably executes commands as the owner, or
|
|
90
|
+
* `undefined` when no drop mechanism works. Each strategy is validated by
|
|
91
|
+
* running `id -u` through it and requiring the owner's uid on stdout — the
|
|
92
|
+
* spawn `uid`/`gid` options in particular are silently ignored by Bun.
|
|
93
|
+
*/
|
|
94
|
+
export async function resolvePrivilegeDrop(
|
|
95
|
+
runner: LocalCommandRunner,
|
|
96
|
+
owner: EmbeddedPostgresOwner,
|
|
97
|
+
): Promise<LocalCommandRunner | undefined> {
|
|
98
|
+
const strategies: readonly LocalCommandRunner[] = [
|
|
99
|
+
(command, args) => runner(command, args, { uid: owner.uid, gid: owner.gid }),
|
|
100
|
+
(command, args) => runner("setpriv", [
|
|
101
|
+
`--reuid=${owner.uid}`, `--regid=${owner.gid}`, "--clear-groups", "--", command, ...args,
|
|
102
|
+
]),
|
|
103
|
+
(command, args) => runner("runuser", ["-u", owner.name, "--", command, ...args]),
|
|
104
|
+
(command, args) => runner("su", ["-s", "/bin/sh", "-c", shellCommand(command, args), owner.name]),
|
|
105
|
+
];
|
|
106
|
+
for (const strategy of strategies) {
|
|
107
|
+
const probe = await strategy("id", ["-u"]).catch(() => undefined);
|
|
108
|
+
if (probe !== undefined && probe.exitCode === 0 && probe.stdout.trim() === String(owner.uid)) {
|
|
109
|
+
return strategy;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
return undefined;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Ensure the embedded binaries are executable by the drop-privilege owner.
|
|
117
|
+
* Probes `initdb --version` as the owner; on failure (typically an
|
|
118
|
+
* untraversable ancestor such as `/root`) copies the package's `native` tree
|
|
119
|
+
* into the cluster base directory once and reuses it afterwards.
|
|
120
|
+
*/
|
|
121
|
+
export async function prepareBinariesForOwner(
|
|
122
|
+
binaries: EmbeddedPostgresBinaryPaths,
|
|
123
|
+
context: EmbeddedPostgresRunContext,
|
|
124
|
+
runner: LocalCommandRunner = runLocalCommand,
|
|
125
|
+
): Promise<EmbeddedPostgresBinaryPaths> {
|
|
126
|
+
const owner = context.owner;
|
|
127
|
+
if (owner === undefined) return binaries;
|
|
128
|
+
|
|
129
|
+
const probe = await context.runAsOwner(binaries.initdb, ["--version"]).catch(() => undefined);
|
|
130
|
+
if (probe !== undefined && probe.exitCode === 0) return binaries;
|
|
131
|
+
|
|
132
|
+
// `<packageRoot>/native/bin/initdb` → copy the whole `native` tree so the
|
|
133
|
+
// binaries keep their relative `../lib` runtime library references.
|
|
134
|
+
const nativeDir = dirname(dirname(binaries.initdb));
|
|
135
|
+
const copiedNativeDir = join(context.baseDir, "pg-runtime", "native");
|
|
136
|
+
const copied: EmbeddedPostgresBinaryPaths = {
|
|
137
|
+
pg_ctl: join(copiedNativeDir, "bin", "pg_ctl"),
|
|
138
|
+
initdb: join(copiedNativeDir, "bin", "initdb"),
|
|
139
|
+
};
|
|
140
|
+
if (!existsSync(copied.initdb)) {
|
|
141
|
+
cpSync(nativeDir, copiedNativeDir, { recursive: true });
|
|
142
|
+
}
|
|
143
|
+
const chown = await runner("chown", ["-R", `${owner.uid}:${owner.gid}`, join(context.baseDir, "pg-runtime")]);
|
|
144
|
+
if (chown.exitCode !== 0) {
|
|
145
|
+
throw new Error(
|
|
146
|
+
`Could not hand the copied embedded Postgres runtime to ${owner.name}: ${chown.stderr.trim() || chown.stdout.trim() || `exit ${chown.exitCode}`}`,
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
return copied;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async function lookupOwner(runner: LocalCommandRunner, name: string): Promise<EmbeddedPostgresOwner | undefined> {
|
|
153
|
+
const uid = await lookupId(runner, ["-u", name]);
|
|
154
|
+
const gid = await lookupId(runner, ["-g", name]);
|
|
155
|
+
if (uid === undefined || gid === undefined || uid === 0) return undefined;
|
|
156
|
+
return { uid, gid, name };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function lookupId(runner: LocalCommandRunner, args: readonly string[]): Promise<number | undefined> {
|
|
160
|
+
try {
|
|
161
|
+
const result = await runner("id", args);
|
|
162
|
+
if (result.exitCode !== 0) return undefined;
|
|
163
|
+
const value = Number.parseInt(result.stdout.trim(), 10);
|
|
164
|
+
return Number.isInteger(value) && value > 0 ? value : undefined;
|
|
165
|
+
} catch {
|
|
166
|
+
return undefined;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Single-quote a command line for `su -c`; arguments never embed user input. */
|
|
171
|
+
function shellCommand(command: string, args: readonly string[]): string {
|
|
172
|
+
return [command, ...args].map((part) => `'${part.replaceAll("'", "'\\''")}'`).join(" ");
|
|
173
|
+
}
|
|
@@ -10,12 +10,23 @@
|
|
|
10
10
|
* is started with `pg_ctl`, which daemonizes the server into its own session:
|
|
11
11
|
* it survives Atomic exiting and is shared by every concurrent Atomic session.
|
|
12
12
|
* Atomic never stops it.
|
|
13
|
+
*
|
|
14
|
+
* PostgreSQL refuses to run as UID 0, so a root Atomic process (containers,
|
|
15
|
+
* CI sandboxes, eval harnesses) resolves an unprivileged system account, keeps
|
|
16
|
+
* the cluster under `/var/lib/atomic-postgres` instead (a root home directory
|
|
17
|
+
* is untraversable for that account), and runs every Postgres command with
|
|
18
|
+
* dropped privileges. See dbos-embedded-postgres-root.ts.
|
|
13
19
|
*/
|
|
14
20
|
|
|
15
|
-
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
|
|
16
|
-
import {
|
|
21
|
+
import { chmodSync, chownSync, copyFileSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, symlinkSync, writeFileSync } from "node:fs";
|
|
22
|
+
import { tmpdir } from "node:os";
|
|
17
23
|
import { dirname, join, relative } from "node:path";
|
|
18
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
prepareBinariesForOwner,
|
|
26
|
+
resolveEmbeddedRunContext,
|
|
27
|
+
type EmbeddedPostgresRunContext,
|
|
28
|
+
} from "./dbos-embedded-postgres-root.js";
|
|
29
|
+
import { commandFailureDetail, delay, tcpReachable } from "./local-command.js";
|
|
19
30
|
|
|
20
31
|
const EMBEDDED_HOST = "127.0.0.1";
|
|
21
32
|
const EMBEDDED_PORT = 5439;
|
|
@@ -47,17 +58,20 @@ export function ensureEmbeddedDbosPostgres(): Promise<void> {
|
|
|
47
58
|
|
|
48
59
|
async function ensure(): Promise<void> {
|
|
49
60
|
if (await tcpReachable(EMBEDDED_HOST, EMBEDDED_PORT)) return;
|
|
50
|
-
const
|
|
51
|
-
hydrateBinaryLibraryLinks(
|
|
52
|
-
const
|
|
61
|
+
const loaded = await loadEmbeddedPostgresBinaries();
|
|
62
|
+
hydrateBinaryLibraryLinks(loaded.pg_ctl);
|
|
63
|
+
const context = await resolveEmbeddedRunContext();
|
|
64
|
+
const root = context.baseDir;
|
|
53
65
|
const dataDir = join(root, `v${EMBEDDED_PG_MAJOR}`);
|
|
54
66
|
const logFile = join(root, `v${EMBEDDED_PG_MAJOR}.log`);
|
|
55
67
|
mkdirSync(root, { recursive: true, mode: 0o700 });
|
|
68
|
+
if (context.owner !== undefined) chownSync(root, context.owner.uid, context.owner.gid);
|
|
69
|
+
const binaries = await prepareBinariesForOwner(loaded, context);
|
|
56
70
|
|
|
57
71
|
await withSetupLock(join(root, `v${EMBEDDED_PG_MAJOR}.setup-lock`), async () => {
|
|
58
72
|
if (await tcpReachable(EMBEDDED_HOST, EMBEDDED_PORT)) return;
|
|
59
|
-
if (!existsSync(join(dataDir, "PG_VERSION"))) await initializeCluster(binaries.initdb, dataDir);
|
|
60
|
-
await startCluster(binaries.pg_ctl, dataDir, logFile);
|
|
73
|
+
if (!existsSync(join(dataDir, "PG_VERSION"))) await initializeCluster(binaries.initdb, dataDir, context);
|
|
74
|
+
await startCluster(binaries.pg_ctl, dataDir, logFile, context);
|
|
61
75
|
});
|
|
62
76
|
|
|
63
77
|
for (let attempt = 0; attempt < READY_ATTEMPTS; attempt += 1) {
|
|
@@ -67,11 +81,16 @@ async function ensure(): Promise<void> {
|
|
|
67
81
|
throw new Error(`Embedded Postgres started but never accepted connections on ${EMBEDDED_HOST}:${EMBEDDED_PORT}; see ${logFile}.`);
|
|
68
82
|
}
|
|
69
83
|
|
|
70
|
-
async function initializeCluster(
|
|
84
|
+
async function initializeCluster(
|
|
85
|
+
initdb: string,
|
|
86
|
+
dataDir: string,
|
|
87
|
+
context: EmbeddedPostgresRunContext,
|
|
88
|
+
): Promise<void> {
|
|
71
89
|
const passwordFile = join(tmpdir(), `atomic-pg-pw-${process.pid}-${crypto.randomUUID().slice(0, 8)}`);
|
|
72
90
|
writeFileSync(passwordFile, `${EMBEDDED_PASSWORD}\n`, { mode: 0o600 });
|
|
73
91
|
try {
|
|
74
|
-
|
|
92
|
+
if (context.owner !== undefined) chownSync(passwordFile, context.owner.uid, context.owner.gid);
|
|
93
|
+
const result = await context.runAsOwner(initdb, [
|
|
75
94
|
"-D", dataDir,
|
|
76
95
|
"-U", EMBEDDED_USER,
|
|
77
96
|
"-A", "password",
|
|
@@ -87,8 +106,13 @@ async function initializeCluster(initdb: string, dataDir: string): Promise<void>
|
|
|
87
106
|
}
|
|
88
107
|
}
|
|
89
108
|
|
|
90
|
-
async function startCluster(
|
|
91
|
-
|
|
109
|
+
async function startCluster(
|
|
110
|
+
pgCtl: string,
|
|
111
|
+
dataDir: string,
|
|
112
|
+
logFile: string,
|
|
113
|
+
context: EmbeddedPostgresRunContext,
|
|
114
|
+
): Promise<void> {
|
|
115
|
+
const result = await context.runAsOwner(pgCtl, [
|
|
92
116
|
"-D", dataDir,
|
|
93
117
|
"-l", logFile,
|
|
94
118
|
"-o", `-p ${EMBEDDED_PORT} -c listen_addresses=${EMBEDDED_HOST}`,
|
|
@@ -119,10 +119,16 @@ export function getReadyDbosBackendSync(): DbosDurableBackend | undefined {
|
|
|
119
119
|
|
|
120
120
|
export async function shutdownDbos(): Promise<void> {
|
|
121
121
|
if (shutdownPromise !== undefined) return await shutdownPromise;
|
|
122
|
-
|
|
122
|
+
const configuredPromise = configured;
|
|
123
|
+
if (configuredPromise === undefined) return;
|
|
123
124
|
shutdownPromise = (async () => {
|
|
124
|
-
|
|
125
|
-
|
|
125
|
+
// A backend that never reached "ready" has nothing to flush or stop.
|
|
126
|
+
// `configured`/`launchPromise` memoize rejections, so re-awaiting them
|
|
127
|
+
// unguarded would rethrow the original provisioning failure out of every
|
|
128
|
+
// session dispose — crashing otherwise-successful runs at process exit.
|
|
129
|
+
const durability = await configuredPromise.catch(() => undefined);
|
|
130
|
+
if (durability === undefined) return;
|
|
131
|
+
if (launchPromise !== undefined) await launchPromise.catch(() => undefined);
|
|
126
132
|
if (state !== "ready") return;
|
|
127
133
|
state = "shutting_down";
|
|
128
134
|
await durability.backend.flush();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/** DBOS-
|
|
1
|
+
/** DBOS-first durable backend factory with a non-durable last-resort fallback. */
|
|
2
2
|
|
|
3
3
|
import { InMemoryDurableBackend, type DurableWorkflowBackend } from "./backend.js";
|
|
4
4
|
import {
|
|
@@ -9,8 +9,9 @@ import {
|
|
|
9
9
|
|
|
10
10
|
let injectedBackend: DurableWorkflowBackend | undefined;
|
|
11
11
|
let initializedBackend: DurableWorkflowBackend | undefined;
|
|
12
|
+
let initializing: Promise<DurableWorkflowBackend> | undefined;
|
|
12
13
|
|
|
13
|
-
/** Return the injected test backend or the process-wide
|
|
14
|
+
/** Return the injected test backend or the process-wide initialized backend. */
|
|
14
15
|
export function getDurableBackend(): DurableWorkflowBackend {
|
|
15
16
|
const backend = injectedBackend ?? initializedBackend ?? getReadyDbosBackendSync();
|
|
16
17
|
if (backend === undefined) throw new DbosNotReadyError();
|
|
@@ -20,7 +21,10 @@ export function getDurableBackend(): DurableWorkflowBackend {
|
|
|
20
21
|
/** Internal injection seam. Production initialization uses DBOS. */
|
|
21
22
|
export function setDurableBackend(backend: DurableWorkflowBackend | undefined): void {
|
|
22
23
|
injectedBackend = backend;
|
|
23
|
-
if (backend === undefined)
|
|
24
|
+
if (backend === undefined) {
|
|
25
|
+
initializedBackend = undefined;
|
|
26
|
+
initializing = undefined;
|
|
27
|
+
}
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
/** Create an isolated current-interface backend for tests only. */
|
|
@@ -28,9 +32,34 @@ export function createInMemoryTestBackend(): InMemoryDurableBackend {
|
|
|
28
32
|
return new InMemoryDurableBackend();
|
|
29
33
|
}
|
|
30
34
|
|
|
31
|
-
/**
|
|
35
|
+
/**
|
|
36
|
+
* Configure, register, launch, and install the DBOS backend.
|
|
37
|
+
*
|
|
38
|
+
* When no durable backend can be provisioned (no `DBOS_SYSTEM_DATABASE_URL`,
|
|
39
|
+
* embedded Postgres unavailable — e.g. running as root without an
|
|
40
|
+
* unprivileged account — and no Docker), workflows degrade to a process-local
|
|
41
|
+
* in-memory backend with a loud warning instead of refusing to run at all.
|
|
42
|
+
* Non-durable runs execute normally but do not survive the process:
|
|
43
|
+
* `/workflow resume` after exit has nothing to restore.
|
|
44
|
+
*/
|
|
32
45
|
export async function initializeDurableBackend(): Promise<DurableWorkflowBackend> {
|
|
33
46
|
if (injectedBackend !== undefined) return injectedBackend;
|
|
34
|
-
initializedBackend
|
|
35
|
-
|
|
47
|
+
if (initializedBackend !== undefined) return initializedBackend;
|
|
48
|
+
initializing ??= getReadyDbosBackend()
|
|
49
|
+
.catch((error: unknown) => degradeToNonDurableBackend(error))
|
|
50
|
+
.then((backend) => {
|
|
51
|
+
initializedBackend = backend;
|
|
52
|
+
return backend;
|
|
53
|
+
});
|
|
54
|
+
return await initializing;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function degradeToNonDurableBackend(error: unknown): DurableWorkflowBackend {
|
|
58
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
59
|
+
console.error(
|
|
60
|
+
"atomic-workflows: durable backend unavailable — continuing NON-DURABLY with an in-memory backend. "
|
|
61
|
+
+ "Workflow runs will execute, but their state will not survive this process and `/workflow resume` "
|
|
62
|
+
+ `after exit will not work. Restore durability by fixing Postgres provisioning: ${detail}`,
|
|
63
|
+
);
|
|
64
|
+
return new InMemoryDurableBackend();
|
|
36
65
|
}
|
|
@@ -11,16 +11,25 @@ export interface LocalCommandResult {
|
|
|
11
11
|
|
|
12
12
|
const OUTPUT_LIMIT_BYTES = 16_384;
|
|
13
13
|
|
|
14
|
+
export interface LocalCommandOptions {
|
|
15
|
+
readonly env?: Readonly<Record<string, string>>;
|
|
16
|
+
/** POSIX drop-privilege identity for the spawned process (root only). */
|
|
17
|
+
readonly uid?: number;
|
|
18
|
+
readonly gid?: number;
|
|
19
|
+
}
|
|
20
|
+
|
|
14
21
|
export function runLocalCommand(
|
|
15
22
|
command: string,
|
|
16
23
|
args: readonly string[],
|
|
17
|
-
|
|
24
|
+
options?: LocalCommandOptions,
|
|
18
25
|
): Promise<LocalCommandResult> {
|
|
19
26
|
return new Promise((resolve, reject) => {
|
|
20
27
|
const child = spawn(command, [...args], {
|
|
21
28
|
stdio: ["ignore", "pipe", "pipe"],
|
|
22
29
|
windowsHide: true,
|
|
23
|
-
...(env !== undefined ? { env: { ...process.env, ...env } } : {}),
|
|
30
|
+
...(options?.env !== undefined ? { env: { ...process.env, ...options.env } } : {}),
|
|
31
|
+
...(options?.uid !== undefined ? { uid: options.uid } : {}),
|
|
32
|
+
...(options?.gid !== undefined ? { gid: options.gid } : {}),
|
|
24
33
|
});
|
|
25
34
|
let stdout = "";
|
|
26
35
|
let stderr = "";
|
|
@@ -16,10 +16,22 @@ import { shutdownDbos } from "../durable/dbos-lifecycle.js";
|
|
|
16
16
|
|
|
17
17
|
let processShutdownInstalled = false;
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Session dispose and process exit must never crash on durability teardown:
|
|
21
|
+
* a genuine flush/stop failure is diagnostic, not fatal, and an unhandled
|
|
22
|
+
* rejection here turns an otherwise-successful run into a nonzero exit.
|
|
23
|
+
*/
|
|
24
|
+
function shutdownDbosQuietly(): Promise<void> {
|
|
25
|
+
return shutdownDbos().catch((error: unknown) => {
|
|
26
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
27
|
+
console.error(`atomic-workflows: DBOS durability shutdown failed: ${detail}`);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
19
31
|
function installDbosProcessShutdown(): void {
|
|
20
32
|
if (processShutdownInstalled) return;
|
|
21
33
|
processShutdownInstalled = true;
|
|
22
|
-
process.once("beforeExit", () =>
|
|
34
|
+
process.once("beforeExit", () => void shutdownDbosQuietly());
|
|
23
35
|
}
|
|
24
36
|
|
|
25
37
|
export interface WorkflowLifecycleRegistrationDeps {
|
|
@@ -123,6 +135,6 @@ export function registerWorkflowLifecycleHandlers(
|
|
|
123
135
|
deps.storeWidgetRef.current = null;
|
|
124
136
|
runtimeState.resetWorkflowDiscoveryForSession();
|
|
125
137
|
runtimeState.setNotificationsActive(false);
|
|
126
|
-
await
|
|
138
|
+
await shutdownDbosQuietly();
|
|
127
139
|
});
|
|
128
140
|
}
|
|
@@ -2,6 +2,7 @@ import type {
|
|
|
2
2
|
CreateAgentSessionOptions,
|
|
3
3
|
DefaultResourceLoaderInheritanceSnapshot,
|
|
4
4
|
} from "@bastani/atomic";
|
|
5
|
+
import type { Api, Model } from "@earendil-works/pi-ai/compat";
|
|
5
6
|
import type { SessionManager } from "../shared/persistence-restore.js";
|
|
6
7
|
import type { StageSessionRuntime } from "../runs/foreground/stage-runner.js";
|
|
7
8
|
import type { RunStatus, StageStatus } from "../shared/store-types.js";
|
|
@@ -58,10 +59,7 @@ export interface PiCommandOptions {
|
|
|
58
59
|
getArgumentCompletions?: (partial: string) => PiArgumentCompletionResult | Promise<PiArgumentCompletionResult>;
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
export
|
|
62
|
-
readonly provider: string;
|
|
63
|
-
readonly id: string;
|
|
64
|
-
}
|
|
62
|
+
export type PiRuntimeModel = Model<Api>;
|
|
65
63
|
|
|
66
64
|
export interface PiRuntimeModelRegistry {
|
|
67
65
|
getAvailable(): PiRuntimeModel[];
|
|
@@ -222,6 +220,7 @@ export interface WorkflowToolArgs {
|
|
|
222
220
|
workflow?: string;
|
|
223
221
|
inputs?: WorkflowInputValues;
|
|
224
222
|
action?:
|
|
223
|
+
| "models"
|
|
225
224
|
| "run"
|
|
226
225
|
| "list"
|
|
227
226
|
| "get"
|
|
@@ -11,6 +11,7 @@ export interface WorkflowToolArgs {
|
|
|
11
11
|
workflow?: string;
|
|
12
12
|
inputs?: WorkflowInputValues;
|
|
13
13
|
action?:
|
|
14
|
+
| "models"
|
|
14
15
|
| "run"
|
|
15
16
|
| "list"
|
|
16
17
|
| "get"
|
|
@@ -106,6 +107,9 @@ export function renderCall(args: WorkflowToolArgs, opts: RenderCallOpts = {}): s
|
|
|
106
107
|
case "get":
|
|
107
108
|
line = name === undefined ? "workflow: get" : `workflow: get ${quoted(name)}`;
|
|
108
109
|
break;
|
|
110
|
+
case "models":
|
|
111
|
+
line = "workflow: list configured models";
|
|
112
|
+
break;
|
|
109
113
|
default:
|
|
110
114
|
line = name === undefined ? `workflow: ${action}` : `workflow: ${action} ${quoted(name)}`;
|
|
111
115
|
break;
|
|
@@ -149,6 +149,14 @@ type ReloadResult = WorkflowReloadReport & { action: "reload"; status: "ok" | "n
|
|
|
149
149
|
type InterruptResult = { action: "interrupt"; runId: string; status: string; message: string };
|
|
150
150
|
type QuitResult = { action: "quit"; runId: string; status: string; message: string };
|
|
151
151
|
type ResumeResult = { action: "resume"; runId: string; status: string; message: string };
|
|
152
|
+
export interface ModelCatalogEntry {
|
|
153
|
+
provider: string;
|
|
154
|
+
id: string;
|
|
155
|
+
fullId: string;
|
|
156
|
+
isCurrent: boolean;
|
|
157
|
+
availableThinkingLevels?: readonly string[];
|
|
158
|
+
}
|
|
159
|
+
type ModelsResult = { action: "models"; models: ModelCatalogEntry[] };
|
|
152
160
|
|
|
153
161
|
export type WorkflowToolResult =
|
|
154
162
|
| ListResult
|
|
@@ -165,7 +173,8 @@ export type WorkflowToolResult =
|
|
|
165
173
|
| ReloadResult
|
|
166
174
|
| InterruptResult
|
|
167
175
|
| QuitResult
|
|
168
|
-
| ResumeResult
|
|
176
|
+
| ResumeResult
|
|
177
|
+
| ModelsResult;
|
|
169
178
|
|
|
170
179
|
export interface RenderResultOpts {
|
|
171
180
|
isPartial?: boolean;
|
|
@@ -432,6 +441,27 @@ export function renderResult(result: WorkflowToolResult, opts?: RenderResultOpts
|
|
|
432
441
|
return renderNotice("WORKFLOW RESUME", `${r.runId}: ${r.message}`, opts, themed);
|
|
433
442
|
}
|
|
434
443
|
|
|
444
|
+
case "models": {
|
|
445
|
+
const r = result as ModelsResult;
|
|
446
|
+
if (r.models.length === 0) {
|
|
447
|
+
return renderNotice("WORKFLOW MODELS", "no models in configured catalog — configured-auth snapshot, not proof of credentials, entitlements, OAuth freshness, or live provider access.", opts, themed);
|
|
448
|
+
}
|
|
449
|
+
const currentLine = r.models.find((model) => model.isCurrent);
|
|
450
|
+
const lines = r.models.map((model) => {
|
|
451
|
+
const levels = model.availableThinkingLevels?.length
|
|
452
|
+
? ` [levels: ${model.availableThinkingLevels.join(", ")}]`
|
|
453
|
+
: "";
|
|
454
|
+
return `${model.provider}/${model.id}${model.isCurrent ? " (current)" : ""}${levels}`;
|
|
455
|
+
}).join("; ");
|
|
456
|
+
const suffix = currentLine !== undefined ? "" : " (no current model)";
|
|
457
|
+
return renderNotice(
|
|
458
|
+
"WORKFLOW MODELS",
|
|
459
|
+
`${lines}${suffix} — configured-auth catalog snapshot, not proof of credentials, entitlements, OAuth freshness, or live provider access.`,
|
|
460
|
+
opts,
|
|
461
|
+
themed,
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
|
|
435
465
|
default: {
|
|
436
466
|
// Runtime guard — handles values coerced from external sources.
|
|
437
467
|
const fallback = result as { action: string; message?: string };
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
export const WORKFLOW_TOOL_DESCRIPTION =
|
|
2
2
|
"Run named builtin, project, user, or package workflows; custom definitions may import reusable project/package workflows or builtin definitions from @bastani/workflows/builtin and nest them with ctx.workflow(...), including deeper composition within the configured maxDepth; " +
|
|
3
3
|
"when workflow execution fits but another shape would better achieve the task, author a custom TypeScript workflow({...}) inline with normal coding tools, reload it, and run it; " +
|
|
4
|
-
"discover with list/get/inputs, list session runs with status (no runId; statusFilter narrows the list), inspect status/stages/stage details, " +
|
|
4
|
+
"discover with list/get/inputs/models, list session runs with status (no runId; statusFilter narrows the list), inspect status/stages/stage details, " +
|
|
5
5
|
"send prompt answers or steering, pause/resume/interrupt/quit runs, and reload workflow resources. " +
|
|
6
6
|
"For large stage handoffs, write context to files/artifacts, pass paths via reads, and prompt downstream agents to 'Read the file at <path>...' instead of injecting large previous text. " +
|
|
7
7
|
"For transcripts, prefer status/stages/stage to get sessionFile/transcriptPath, " +
|
|
8
8
|
"quote the exact path without rewriting separators (Windows backslashes are valid), " +
|
|
9
|
-
"then search it with rg/grep and read small ranges; transcript is path-only by default when sessionFile/transcriptPath exists, explicit tail/limit returns bounded previews, and missing transcript paths fall back to a small preview."
|
|
9
|
+
"then search it with rg/grep and read small ranges; transcript is path-only by default when sessionFile/transcriptPath exists, explicit tail/limit returns bounded previews, and missing transcript paths fall back to a small preview. " +
|
|
10
|
+
"Use action 'models' to inspect models in the configured catalog; the result is a configured-auth snapshot showing what's present in the registry with configured authentication, not proof of credentials, entitlements, OAuth freshness, or live provider access. " +
|
|
11
|
+
"When authoring a workflow that should dynamically select a model, first call workflow({ action: 'models' }) to inspect the configured catalog, then select from the returned provider/id entries considering the isCurrent marker and available thinking levels.";
|
|
10
12
|
|
|
11
13
|
export const DEFAULT_PROMPT_GUIDANCE: string[] = [
|
|
12
14
|
`**Workflows**: Treat workflows as the default execution path for any non-trivial task and for any request that has inherent structure plus an objective you can make verifiable. Use the \`workflow\` tool for existing named workflows; when the task needs a graph that is not installed, author a custom TypeScript \`workflow({...})\` inline with normal coding tools, reload workflow resources, and run it.
|
|
@@ -30,6 +32,7 @@ export const DEFAULT_PROMPT_GUIDANCE: string[] = [
|
|
|
30
32
|
- When a user asks to create or edit a workflow, clarify only unresolved requirements that materially affect its purpose, inputs, stages, handoffs, validation, success criteria, or starter pattern. Read the workflow docs/examples, implement the TypeScript definition with normal coding tools, reload it, and run representative test inputs before presenting it. Use the create-spec skill when it adds value; it is not mandatory when context is already sufficient.
|
|
31
33
|
- Consult docs/workflows.md and its starter patterns (Classify-and-act, Fan-out-and-synthesize, Adversarial verification, Generate-and-filter, Tournament, and Loop until done) when designing a stage graph.
|
|
32
34
|
- Treat workflow composition as a first-class authoring option. Before duplicating stages, inspect reusable workflow modules and builtin exports; import their definitions and invoke them with \`ctx.workflow(...)\`, mapping typed inputs and consuming only declared outputs. Nested workflows can themselves import children, so combine small reusable graphs into a richer parent while respecting \`maxDepth\`.
|
|
35
|
+
- Prefer \`ctx.tool(name, args, fn)\` for workflow-owned TypeScript operations with side effects, such as filesystem writes, network mutations, and external API actions. A completed \`ctx.tool\` call is durably checkpointed with its serializable result, so resume replays that result without rerunning \`fn\`. Keep pure computation and side-effect-free transformations as ordinary TypeScript. Do not wrap agent-stage internals or every function call indiscriminately; this rule applies to side effects orchestrated directly by the workflow definition.
|
|
33
36
|
- Pass large stage context through files/artifacts and \`reads\` with an explicit \`Read the file at <path>...\` prompt rather than large \`previous\` payloads or injected session history.
|
|
34
37
|
- Separate implementation/review acceptance from explicitly authorized final actions such as PR/MR/review creation, release tagging, deployment, or publication. Stop implementation loops once acceptance is proven and carry a remaining final action separately.
|
|
35
38
|
- A model stage sees its local prompt, artifacts, tools, and reads, not the graph name or surrounding implementation. State the concrete action, evidence, and success criteria directly.`,
|
|
@@ -9,6 +9,7 @@ export const WorkflowParametersSchema = Type.Object({
|
|
|
9
9
|
description: "Key/value inputs passed to a named workflow run.",
|
|
10
10
|
})),
|
|
11
11
|
action: Type.Optional(Type.Union([
|
|
12
|
+
Type.Literal("models"),
|
|
12
13
|
Type.Literal("run"),
|
|
13
14
|
Type.Literal("list"),
|
|
14
15
|
Type.Literal("get"),
|
|
@@ -24,7 +25,7 @@ export const WorkflowParametersSchema = Type.Object({
|
|
|
24
25
|
Type.Literal("resume"),
|
|
25
26
|
Type.Literal("reload"),
|
|
26
27
|
], {
|
|
27
|
-
description: "Workflow action: run/list/get/inputs/status, inspect stage metadata, send messages or prompt answers, pause/resume/interrupt/quit runs, or reload workflow resources. 'status' without runId lists every workflow run in the current session with concise per-run summaries (status, timing, active stages, awaiting-input prompts); filter the listing with statusFilter. 'status' with runId returns one run's full detail. For transcript inspection, prefer status/stages/stage first to get sessionFile/transcriptPath, quote the exact path without rewriting separators (Windows backslashes are valid), then search it with rg/grep and read small ranges; transcript is path-only by default when sessionFile/transcriptPath exists, explicit tail/limit returns bounded previews, and missing transcript paths fall back to a small preview.",
|
|
28
|
+
description: "Workflow action: run/list/get/inputs/models/status, inspect stage metadata, send messages or prompt answers, pause/resume/interrupt/quit runs, inspect the configured model catalog, or reload workflow resources. 'status' without runId lists every workflow run in the current session with concise per-run summaries (status, timing, active stages, awaiting-input prompts); filter the listing with statusFilter. 'status' with runId returns one run's full detail. For transcript inspection, prefer status/stages/stage first to get sessionFile/transcriptPath, quote the exact path without rewriting separators (Windows backslashes are valid), then search it with rg/grep and read small ranges; transcript is path-only by default when sessionFile/transcriptPath exists, explicit tail/limit returns bounded previews, and missing transcript paths fall back to a small preview.",
|
|
28
29
|
})),
|
|
29
30
|
runId: Type.Optional(Type.String({
|
|
30
31
|
description: "Run identifier or unique prefix for status/stages/stage/transcript/send/pause/resume/interrupt/quit. Omit runId with action 'status' to list all session runs and their statuses. Use '--all' or all:true for supported bulk run-control actions.",
|
|
@@ -54,7 +55,7 @@ export const WorkflowParametersSchema = Type.Object({
|
|
|
54
55
|
description: "Filter stages (stages action) or listed runs (status action without runId) by status; 'all' (default) includes everything. For the status listing, run statuses match directly, 'awaiting_input' selects runs with at least one stage awaiting input or pending human prompt, and in-flight runs are listed first.",
|
|
55
56
|
})),
|
|
56
57
|
format: Type.Optional(Type.Union([Type.Literal("text"), Type.Literal("json")], {
|
|
57
|
-
description: "Agent-visible output format for data-bearing inspection actions (status, stages, stage, transcript); 'json' returns the full structured result.",
|
|
58
|
+
description: "Agent-visible output format for data-bearing inspection actions (models, status, stages, stage, transcript); 'json' returns the full structured result.",
|
|
58
59
|
})),
|
|
59
60
|
limit: Type.Optional(Type.Integer({
|
|
60
61
|
minimum: 0,
|