stageflow 0.32.0 → 0.33.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.
package/README.md CHANGED
@@ -254,6 +254,7 @@ Full docs: **[tejasghutukade.github.io/stageflow](https://tejasghutukade.github.
254
254
  |-----|-------------|
255
255
  | [docs/README.md](docs/README.md) | Documentation index |
256
256
  | [docs/architecture.md](docs/architecture.md) | Runtime components, execution flow, persistence, and design decisions |
257
+ | [docs/hosted.md](docs/hosted.md) | Embed Stageflow from another application and receive ordered run events |
257
258
  | [docs/quickstart.md](docs/quickstart.md) | Expanded quick start |
258
259
  | [docs/yaml-catalog.md](docs/yaml-catalog.md) | Pipelines, stages, tasks schema |
259
260
  | [docs/cli-reference.md](docs/cli-reference.md) | `sf init`, `sf run`, `sf run-stage`, `sf graph`, `sf validate`, `sf ui`, `sf mcp`, `sf envelope`, `sf export-run`, `sf artifact`, `sf skills`, `sf providers` |
@@ -113,12 +113,6 @@ export class FakeAgent {
113
113
  armWait();
114
114
  }
115
115
  }
116
- if (sessionMode === "feedback_resume" && behavior.type === "wait_then_emit") {
117
- behavior = {
118
- type: "emit",
119
- envelope: behavior.envelope,
120
- };
121
- }
122
116
  const emitStartActivity = () => {
123
117
  if (started)
124
118
  return;
@@ -82,6 +82,12 @@ export declare function statusForProvider(runtime: ProviderAuthRuntime, provider
82
82
  export declare function listProviders(cwd: string, ctx?: ProviderAuthContext): Promise<ProvidersListResult>;
83
83
  export declare function getAuthStatus(cwd: string, providerId?: string, ctx?: ProviderAuthContext): Promise<ProviderAuthStatus | ProviderAuthStatus[]>;
84
84
  export declare function loginWithApiKey(cwd: string, providerId: string, apiKey: string, ctx?: ProviderAuthContext): Promise<ProviderAuthStatus>;
85
+ /** Write a provider API key into an existing Pi auth file. Does not create the operator home. */
86
+ export declare function configurePiProviderApiKey(options: {
87
+ authPath: string;
88
+ providerId: string;
89
+ apiKey: string;
90
+ }, ctx?: ProviderAuthContext): Promise<void>;
85
91
  export type OauthLoginResult = {
86
92
  provider: ProviderAuthStatus;
87
93
  warning?: string;
@@ -1,3 +1,5 @@
1
+ import { mkdir, writeFile, chmod, access } from "node:fs/promises";
2
+ import path from "node:path";
1
3
  import { CredentialSynchronizationError, ModelRuntime, } from "@earendil-works/pi-coding-agent";
2
4
  import { parseCredentialSource, resolveCredentialBinding, writeCredentialSourceToContext, isUsableAuthFile, } from "../runtime/credentialBinding.js";
3
5
  import { getCursorHostStatus } from "./cursorProvider.js";
@@ -22,6 +24,23 @@ export class ProviderAuthError extends Error {
22
24
  }
23
25
  }
24
26
  const defaultCreateRuntime = async (authPath) => ModelRuntime.create({ authPath, refreshOnCreate: false });
27
+ async function ensurePiAuthFile(authPath) {
28
+ const resolved = path.resolve(authPath);
29
+ await mkdir(path.dirname(resolved), { recursive: true });
30
+ try {
31
+ await access(resolved);
32
+ }
33
+ catch {
34
+ await writeFile(resolved, "{}\n", { encoding: "utf8", mode: 0o600 });
35
+ }
36
+ try {
37
+ await chmod(resolved, 0o600);
38
+ }
39
+ catch {
40
+ // best-effort on non-POSIX
41
+ }
42
+ return resolved;
43
+ }
25
44
  export const defaultContext = {
26
45
  createRuntime: defaultCreateRuntime,
27
46
  lock: makeMutationLock(),
@@ -159,26 +178,43 @@ function mapLoginError(err, providerId) {
159
178
  }
160
179
  throw new ProviderAuthError("Provider login failed", 500);
161
180
  }
181
+ async function loginApiKeyOnRuntime(runtime, providerId, apiKey) {
182
+ if (typeof apiKey !== "string" || apiKey.trim().length === 0) {
183
+ throw new ProviderAuthError("apiKey is required", 400);
184
+ }
185
+ const provider = runtime.getProvider(providerId);
186
+ if (!provider) {
187
+ throw new ProviderAuthError("Provider not found", 404);
188
+ }
189
+ if (typeof provider.auth.apiKey?.login !== "function") {
190
+ throw new ProviderAuthError("Provider does not support api_key login", 400);
191
+ }
192
+ try {
193
+ await runtime.login(providerId, "api_key", apiKeyInteraction(apiKey));
194
+ }
195
+ catch (err) {
196
+ mapLoginError(err, providerId);
197
+ }
198
+ return statusForProvider(runtime, providerId);
199
+ }
162
200
  export async function loginWithApiKey(cwd, providerId, apiKey, ctx = defaultContext) {
163
201
  if (typeof apiKey !== "string" || apiKey.trim().length === 0) {
164
202
  throw new ProviderAuthError("apiKey is required", 400);
165
203
  }
166
204
  return ctx.lock.withMutationLock(async () => {
167
205
  const runtime = await openRuntime(cwd, ctx);
168
- const provider = runtime.getProvider(providerId);
169
- if (!provider) {
170
- throw new ProviderAuthError("Provider not found", 404);
171
- }
172
- if (typeof provider.auth.apiKey?.login !== "function") {
173
- throw new ProviderAuthError("Provider does not support api_key login", 400);
174
- }
175
- try {
176
- await runtime.login(providerId, "api_key", apiKeyInteraction(apiKey));
177
- }
178
- catch (err) {
179
- mapLoginError(err, providerId);
180
- }
181
- return statusForProvider(runtime, providerId);
206
+ return loginApiKeyOnRuntime(runtime, providerId, apiKey);
207
+ });
208
+ }
209
+ /** Write a provider API key into an existing Pi auth file. Does not create the operator home. */
210
+ export async function configurePiProviderApiKey(options, ctx = defaultContext) {
211
+ if (typeof options.authPath !== "string" || options.authPath.trim() === "") {
212
+ throw new ProviderAuthError("authPath is required", 400);
213
+ }
214
+ const authPath = await ensurePiAuthFile(options.authPath);
215
+ await ctx.lock.withMutationLock(async () => {
216
+ const runtime = await ctx.createRuntime(authPath);
217
+ await loginApiKeyOnRuntime(runtime, options.providerId, options.apiKey);
182
218
  });
183
219
  }
184
220
  export async function loginWithOauth(cwd, providerId, interaction, ctx = defaultContext) {
@@ -11,6 +11,8 @@ export type AgentSelection = {
11
11
  };
12
12
  /** stage > pipeline > global > "pi", with stage gated off for now. */
13
13
  export declare function resolveAgentBackend(selection?: AgentSelection): AgentBackendId;
14
+ /** Existing Pi AgentPort. Hosted runs pass this into createHostedRuntime. */
15
+ export declare function createPiAgentPort(): AgentPort;
14
16
  export declare function resolveAgentPort(selection?: AgentSelection): AgentPort;
15
17
  /** The global tier's backend id from an already-loaded manifest, or undefined for missing/invalid/unset. */
16
18
  export declare function globalAgentBackendFromManifest(manifest: LoadedManifest | null): AgentBackendId | undefined;
@@ -9,11 +9,15 @@ export function resolveAgentBackend(selection = {}) {
9
9
  }
10
10
  return selection.pipeline ?? selection.global ?? "pi";
11
11
  }
12
+ /** Existing Pi AgentPort. Hosted runs pass this into createHostedRuntime. */
13
+ export function createPiAgentPort() {
14
+ return new PiAgentAdapter();
15
+ }
12
16
  export function resolveAgentPort(selection = {}) {
13
17
  const backend = resolveAgentBackend(selection);
14
18
  switch (backend) {
15
19
  case "pi":
16
- return new PiAgentAdapter();
20
+ return createPiAgentPort();
17
21
  case "claude":
18
22
  return new ClaudeAgentAdapter();
19
23
  }
@@ -1,28 +1,12 @@
1
- import type { StageGateKind } from "../types/stage.js";
2
1
  import type { LoadedManifest } from "../types/stageflowManifest.js";
3
2
  import { type PipelineListing, type TaskListing } from "./browseCatalog.js";
4
3
  import { type StageflowContext } from "../project/resolveStageflowContext.js";
5
4
  import { type CatalogContext } from "./resolveCatalogContext.js";
6
5
  export type { PipelineListing, PipelineStageListing, TaskListing } from "./browseCatalog.js";
7
- export type ValidStageListing = {
8
- path: string;
9
- id: string;
10
- used_by_pipeline_ids: string[];
11
- gate_kinds?: StageGateKind[];
12
- };
13
- export type BrokenStageListing = {
14
- path: string;
15
- error: string;
16
- id?: string;
17
- model?: string;
18
- gate_kinds?: StageGateKind[];
19
- };
20
- export type StageListing = ValidStageListing | BrokenStageListing;
21
6
  export type CatalogListOptions = {
22
7
  projectRoot: string;
23
8
  manifest: LoadedManifest;
24
9
  };
25
10
  export declare function listTasks(options: CatalogListOptions): Promise<TaskListing[]>;
26
11
  export declare function listPipelines(options: CatalogListOptions): Promise<PipelineListing[]>;
27
- export declare function listStages(_options?: unknown): Promise<StageListing[]>;
28
12
  export declare function listModels(cwdOrContext: string | CatalogContext | StageflowContext): Promise<string[]>;
@@ -17,9 +17,6 @@ export async function listPipelines(options) {
17
17
  issues: [],
18
18
  });
19
19
  }
20
- export async function listStages(_options) {
21
- return [];
22
- }
23
20
  export async function listModels(cwdOrContext) {
24
21
  const ctx = typeof cwdOrContext === "string"
25
22
  ? catalogContextFromStageflow(await resolveStageflowContext(cwdOrContext))
@@ -0,0 +1,3 @@
1
+ export declare function isContained(rootReal: string, candidateReal: string): boolean;
2
+ export declare function resolveDirectory(root: string): Promise<string>;
3
+ export declare function containedRealPath(rootReal: string, target: string): Promise<string>;
@@ -0,0 +1,36 @@
1
+ import { realpath, stat } from "node:fs/promises";
2
+ import path from "node:path";
3
+ export function isContained(rootReal, candidateReal) {
4
+ const relative = path.relative(rootReal, candidateReal);
5
+ return (relative === "" ||
6
+ (relative !== ".." &&
7
+ !relative.startsWith(`..${path.sep}`) &&
8
+ !path.isAbsolute(relative)));
9
+ }
10
+ export async function resolveDirectory(root) {
11
+ const absolute = path.resolve(root);
12
+ let info;
13
+ try {
14
+ info = await stat(absolute);
15
+ }
16
+ catch {
17
+ throw new Error(`package root does not exist: ${absolute}`);
18
+ }
19
+ if (!info.isDirectory()) {
20
+ throw new Error(`package root is not a directory: ${absolute}`);
21
+ }
22
+ return realpath(absolute);
23
+ }
24
+ export async function containedRealPath(rootReal, target) {
25
+ let real;
26
+ try {
27
+ real = await realpath(target);
28
+ }
29
+ catch {
30
+ throw new Error(`path is not readable: ${target}`);
31
+ }
32
+ if (!isContained(rootReal, real)) {
33
+ throw new Error(`path escapes package root: ${target}`);
34
+ }
35
+ return real;
36
+ }
@@ -0,0 +1,21 @@
1
+ export declare const HOSTED_ERROR_CODES: readonly ["invalid_input", "package_invalid", "agent_failed", "event_sink_failed", "cancelled", "timed_out", "hitl_unsupported", "internal"];
2
+ export type HostedErrorCode = (typeof HOSTED_ERROR_CODES)[number];
3
+ export type HostedRuntimeErrorDetails = {
4
+ code: HostedErrorCode;
5
+ externalRunId: string;
6
+ externalAttemptId: string;
7
+ runId?: string;
8
+ eventType?: string;
9
+ sequence?: number;
10
+ localResult?: import("./types.js").HostedPipelineResult;
11
+ };
12
+ export declare class HostedRuntimeError extends Error {
13
+ readonly code: HostedErrorCode;
14
+ readonly externalRunId: string;
15
+ readonly externalAttemptId: string;
16
+ readonly runId?: string;
17
+ readonly eventType?: string;
18
+ readonly sequence?: number;
19
+ readonly localResult?: import("./types.js").HostedPipelineResult;
20
+ constructor(message: string, details: HostedRuntimeErrorDetails);
21
+ }
@@ -0,0 +1,30 @@
1
+ export const HOSTED_ERROR_CODES = [
2
+ "invalid_input",
3
+ "package_invalid",
4
+ "agent_failed",
5
+ "event_sink_failed",
6
+ "cancelled",
7
+ "timed_out",
8
+ "hitl_unsupported",
9
+ "internal",
10
+ ];
11
+ export class HostedRuntimeError extends Error {
12
+ code;
13
+ externalRunId;
14
+ externalAttemptId;
15
+ runId;
16
+ eventType;
17
+ sequence;
18
+ localResult;
19
+ constructor(message, details) {
20
+ super(message);
21
+ this.name = "HostedRuntimeError";
22
+ this.code = details.code;
23
+ this.externalRunId = details.externalRunId;
24
+ this.externalAttemptId = details.externalAttemptId;
25
+ this.runId = details.runId;
26
+ this.eventType = details.eventType;
27
+ this.sequence = details.sequence;
28
+ this.localResult = details.localResult;
29
+ }
30
+ }
@@ -0,0 +1,3 @@
1
+ import { type SkillsPayload } from "../runtime/runSkills.js";
2
+ /** Skills shipped with a hosted package live at `<packageRoot>/.pi/skills/<name>/`. */
3
+ export declare function loadPackageSkills(packageRoot: string): Promise<SkillsPayload | undefined>;
@@ -0,0 +1,80 @@
1
+ import { readdir, readFile, realpath, stat } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { assertValidSkillName, assertValidSkillRelativePath, validateSkillsPayload, } from "../runtime/runSkills.js";
4
+ import { isContained } from "./containment.js";
5
+ /** Skills shipped with a hosted package live at `<packageRoot>/.pi/skills/<name>/`. */
6
+ export async function loadPackageSkills(packageRoot) {
7
+ const skillsRoot = path.join(packageRoot, ".pi", "skills");
8
+ let entries;
9
+ try {
10
+ entries = await readdir(skillsRoot, { withFileTypes: true });
11
+ }
12
+ catch (error) {
13
+ if (error.code === "ENOENT")
14
+ return undefined;
15
+ throw error;
16
+ }
17
+ const skillsRootReal = await realpath(skillsRoot);
18
+ if (!isContained(packageRoot, skillsRootReal)) {
19
+ throw new Error("package skills directory escapes the package root");
20
+ }
21
+ const skills = {};
22
+ for (const entry of entries) {
23
+ if (entry.name.startsWith("."))
24
+ continue;
25
+ const nameCheck = assertValidSkillName(entry.name);
26
+ if (!nameCheck.ok)
27
+ throw new Error(nameCheck.reason);
28
+ const skillDir = path.join(skillsRoot, entry.name);
29
+ let info;
30
+ try {
31
+ info = await stat(skillDir);
32
+ }
33
+ catch {
34
+ continue;
35
+ }
36
+ if (!info.isDirectory())
37
+ continue;
38
+ const skillReal = await realpath(skillDir);
39
+ if (!isContained(skillsRootReal, skillReal)) {
40
+ throw new Error(`package skill "${entry.name}" escapes the package root`);
41
+ }
42
+ const files = await readSkillFiles(skillDir, skillReal);
43
+ if (files["SKILL.md"] === undefined)
44
+ continue;
45
+ skills[entry.name] = files;
46
+ }
47
+ if (Object.keys(skills).length === 0)
48
+ return undefined;
49
+ const validated = validateSkillsPayload(skills);
50
+ if (!validated.ok)
51
+ throw new Error(validated.reason);
52
+ return validated.skills;
53
+ }
54
+ async function readSkillFiles(skillDir, skillReal) {
55
+ const files = {};
56
+ await walk(skillDir);
57
+ return files;
58
+ async function walk(dir) {
59
+ const entries = await readdir(dir, { withFileTypes: true });
60
+ for (const entry of entries) {
61
+ const abs = path.join(dir, entry.name);
62
+ const rel = path.relative(skillDir, abs);
63
+ const real = await realpath(abs);
64
+ if (!isContained(skillReal, real)) {
65
+ throw new Error(`package skill file "${rel}" escapes the package root`);
66
+ }
67
+ const target = await stat(real);
68
+ if (target.isDirectory()) {
69
+ await walk(abs);
70
+ continue;
71
+ }
72
+ if (!target.isFile())
73
+ continue;
74
+ const pathCheck = assertValidSkillRelativePath(rel);
75
+ if (!pathCheck.ok)
76
+ throw new Error(pathCheck.reason);
77
+ files[rel] = await readFile(real, "utf8");
78
+ }
79
+ }
80
+ }
@@ -0,0 +1,12 @@
1
+ import type { AgentPort } from "../agent/port.js";
2
+ import type { RunStore } from "../runstore/port.js";
3
+ import { type HostedEventSink, type HostedPipelineInput, type HostedPipelineResult } from "./types.js";
4
+ export type HostedRuntime = {
5
+ run(input: HostedPipelineInput): Promise<HostedPipelineResult>;
6
+ };
7
+ export declare function createHostedRuntime(dependencies: {
8
+ agent: AgentPort;
9
+ localStore: RunStore;
10
+ eventSink: HostedEventSink;
11
+ eventDeliveryTimeoutMs?: number;
12
+ }): HostedRuntime;