@plasm_lang/vercel-agent 0.3.63

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 (121) hide show
  1. package/README.md +235 -0
  2. package/agent/.plasm/archives/runs/runs/pr2d5c4a99707b4c19b650553d50229a1d600d28e3d98a9c58f18e5026cecc86ca.json +64 -0
  3. package/agent/.plasm/archives/runs/runs/pr2e0c0d8ad443c63c82da7435ee1a002b0e0fa718b640263c0a9d3e6e5944812f.json +64 -0
  4. package/agent/.plasm/archives/runs/runs/pr2faedb8354f40ee6d828e3af07b421fda9ccda973a4f7347fce3639f03a0a869.json +64 -0
  5. package/agent/.plasm/archives/runs/runs/pr586b47c55547b0702c572bce4255558b22dbe5e682d6359169577e0ea75fe98f.json +64 -0
  6. package/agent/.plasm/archives/runs/runs/pr76212356445e3b00fcf256835aaec18bac68576324b90d0be92d47f0b4a862a7.json +56 -0
  7. package/agent/.plasm/archives/runs/runs/pr9ec805d689e00db9270a9539858f2fb7216c24acbfea943d450e37b641149da1.json +64 -0
  8. package/agent/.plasm/archives/runs/runs/prc3c0c4ba2e28fc94ed6d37b6796e277a7997d9cb3184640d14c35c98bc6d136f.json +64 -0
  9. package/agent/.plasm/archives/runs/runs/prf04de32522f2fdcb17818907d91bccce7dcaecbd1259041cc448d447b6993244.json +64 -0
  10. package/agent/.plasm/archives/traces/traces/local/00000000000000000000000000000000/records.ndjson +1 -0
  11. package/agent/.plasm/archives/traces/traces/local/00000000000000000000000000000000/summary.json +13 -0
  12. package/agent/.plasm/discovery/manifest.json +126 -0
  13. package/agent/.plasm/sessions/TGlzdCBwcm9kdWN0cyBmcm9tIHRoZSBleGVjdXRlX3RpbnkgY2F0YWxvZw.json +44 -0
  14. package/agent/.plasm/sessions/TGlzdCBwcm9kdWN0cyBmcm9tIHRoZSBleGVjdXRlX3RpbnkgY2F0YWxvZw.teaching.tsv +23 -0
  15. package/agent/.plasm/sessions/bGlzdCBleGVjdXRlX3RpbnkgcHJvZHVjdHM.json +151 -0
  16. package/agent/.plasm/sessions/bGlzdCBleGVjdXRlX3RpbnkgcHJvZHVjdHM.teaching.tsv +131 -0
  17. package/agent/.plasm/sessions/bGlzdCBleGVjdXRlX3RpbnkgcHJvZHVjdHMgYXN5bmMgdHJhbnNwb3J0.json +44 -0
  18. package/agent/.plasm/sessions/bGlzdCBleGVjdXRlX3RpbnkgcHJvZHVjdHMgYXN5bmMgdHJhbnNwb3J0.teaching.tsv +23 -0
  19. package/agent/.plasm/stubs/.gitkeep +0 -0
  20. package/agent/.plasm/stubs/execute_tiny.ts +107 -0
  21. package/agent/agent.ts +52 -0
  22. package/agent/catalogs/README.md +15 -0
  23. package/agent/channels/.gitkeep +0 -0
  24. package/agent/channels/execute-tiny-webhook.ts +59 -0
  25. package/agent/channels/health.ts +16 -0
  26. package/agent/hooks/.gitkeep +0 -0
  27. package/agent/hooks/trace-log.ts +10 -0
  28. package/agent/instructions.md +25 -0
  29. package/agent/schedules/.gitkeep +0 -0
  30. package/agent/schedules/ping.ts +13 -0
  31. package/agent/skills/.gitkeep +0 -0
  32. package/agent/skills/plasm-authoring.md +8 -0
  33. package/agent/subagents/.gitkeep +0 -0
  34. package/agent/subagents/tiny/agent.ts +28 -0
  35. package/bin/plasm-agent.mjs +18 -0
  36. package/package.json +77 -0
  37. package/scripts/plasm-node.mjs +19 -0
  38. package/scripts/resolve-ts-extension.mjs +18 -0
  39. package/src/archive/adapters.ts +27 -0
  40. package/src/archive/index.ts +99 -0
  41. package/src/archive/local-run-archive.ts +90 -0
  42. package/src/archive/local-trace-archive.ts +91 -0
  43. package/src/archive/paths.ts +15 -0
  44. package/src/archive/postgres-kv-adapter.ts +72 -0
  45. package/src/archive/prod-archive-store.ts +143 -0
  46. package/src/archive/resolve-backend.ts +60 -0
  47. package/src/archive/run-id.ts +23 -0
  48. package/src/archive/types.ts +75 -0
  49. package/src/archive/vercel-blob-adapter.ts +21 -0
  50. package/src/archive/vercel-kv-adapter.ts +24 -0
  51. package/src/authoring/channel-dispatch.ts +44 -0
  52. package/src/authoring/context.ts +34 -0
  53. package/src/authoring/define-channel.ts +83 -0
  54. package/src/authoring/define-hook.ts +51 -0
  55. package/src/authoring/define-schedule.ts +64 -0
  56. package/src/authoring/define-skill.ts +38 -0
  57. package/src/authoring/hook-runner.ts +18 -0
  58. package/src/authoring/schedule-manager.ts +118 -0
  59. package/src/authoring/slot-loader.ts +253 -0
  60. package/src/authoring/subagent-loader.ts +121 -0
  61. package/src/catalog/loader.ts +71 -0
  62. package/src/cli/build.ts +54 -0
  63. package/src/cli/dev.ts +60 -0
  64. package/src/cli/info.ts +12 -0
  65. package/src/cli/init.ts +372 -0
  66. package/src/cli/link.ts +68 -0
  67. package/src/cli/project-root.ts +57 -0
  68. package/src/define-agent.ts +150 -0
  69. package/src/dev/client/ansi.ts +36 -0
  70. package/src/dev/client/http-session.ts +180 -0
  71. package/src/dev/client/repl.ts +92 -0
  72. package/src/dev/client/slash.ts +119 -0
  73. package/src/dev/dev-session.ts +153 -0
  74. package/src/dev/http.ts +29 -0
  75. package/src/dev/server.ts +147 -0
  76. package/src/dev/session-routes.ts +185 -0
  77. package/src/discovery/project-walker.ts +272 -0
  78. package/src/engine/connect-auth.ts +135 -0
  79. package/src/engine/create-host-transport.ts +7 -0
  80. package/src/engine/fixture-mock-transport.ts +54 -0
  81. package/src/engine/host-transport-bridge.ts +32 -0
  82. package/src/engine/host-transport.ts +84 -0
  83. package/src/engine/napi-binding.ts +265 -0
  84. package/src/evals/define-eval.ts +56 -0
  85. package/src/evals/run-eval.ts +136 -0
  86. package/src/gateway-model.ts +43 -0
  87. package/src/index.ts +296 -0
  88. package/src/instrumentation.ts +56 -0
  89. package/src/load-env.ts +63 -0
  90. package/src/operator/routes.ts +287 -0
  91. package/src/operator/types.ts +63 -0
  92. package/src/operator/ui-shell.ts +134 -0
  93. package/src/project-info.ts +229 -0
  94. package/src/runtime/agent-runtime.ts +469 -0
  95. package/src/runtime/compaction.ts +81 -0
  96. package/src/runtime/logical-session.ts +72 -0
  97. package/src/runtime/plasm-agent.ts +199 -0
  98. package/src/server/plasm-handler.ts +331 -0
  99. package/src/session-state.ts +135 -0
  100. package/src/state/define-state.ts +57 -0
  101. package/src/state/fs-state-adapter.ts +72 -0
  102. package/src/state/kv-state-adapter.ts +62 -0
  103. package/src/state/postgres-state-adapter.ts +116 -0
  104. package/src/stubs/capability-invoke-shape.ts +135 -0
  105. package/src/stubs/catalog-client.ts +170 -0
  106. package/src/stubs/catalog-hash.ts +11 -0
  107. package/src/stubs/catalog-introspection.ts +121 -0
  108. package/src/stubs/cgs-ts-types.ts +164 -0
  109. package/src/stubs/domain-parser.ts +203 -0
  110. package/src/stubs/generator.ts +390 -0
  111. package/src/stubs/input-type-to-ts.ts +233 -0
  112. package/src/stubs/plasm-value-emitter.ts +162 -0
  113. package/src/stubs/program-builder.ts +82 -0
  114. package/src/stubs/stub-symbols.ts +89 -0
  115. package/src/symbol-registry.ts +74 -0
  116. package/src/telemetry/plasm-spans.ts +83 -0
  117. package/src/tools/descriptions.ts +94 -0
  118. package/src/tools/format.ts +29 -0
  119. package/src/tools/harness-tools.ts +65 -0
  120. package/src/tools/plasm-tools.ts +104 -0
  121. package/src/workflow/world-bootstrap.ts +52 -0
@@ -0,0 +1,272 @@
1
+ import { access, readFile, readdir, stat } from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ const SLOT_DIRS = [
5
+ "catalogs",
6
+ "skills",
7
+ "channels",
8
+ "schedules",
9
+ "hooks",
10
+ "subagents",
11
+ ] as const;
12
+
13
+ export type AuthoredSlot = (typeof SLOT_DIRS)[number];
14
+
15
+ export interface DiscoveryDiagnostic {
16
+ level: "error" | "warning";
17
+ slot: AuthoredSlot | "instructions" | "agent";
18
+ path: string;
19
+ message: string;
20
+ }
21
+
22
+ export interface DiscoveredCatalog {
23
+ name: string;
24
+ path: string;
25
+ entryId?: string;
26
+ }
27
+
28
+ export interface DiscoveredNamedFile {
29
+ name: string;
30
+ path: string;
31
+ kind: "typescript" | "markdown";
32
+ }
33
+
34
+ export interface DiscoveredSubagent {
35
+ name: string;
36
+ path: string;
37
+ agentPath: string;
38
+ }
39
+
40
+ export interface DiscoveredInstructions {
41
+ path: string;
42
+ kind: "markdown" | "typescript";
43
+ }
44
+
45
+ export interface ProjectDiscovery {
46
+ agentRoot: string;
47
+ instructions?: DiscoveredInstructions;
48
+ catalogs: DiscoveredCatalog[];
49
+ skills: DiscoveredNamedFile[];
50
+ channels: DiscoveredNamedFile[];
51
+ schedules: DiscoveredNamedFile[];
52
+ hooks: DiscoveredNamedFile[];
53
+ subagents: DiscoveredSubagent[];
54
+ diagnostics: DiscoveryDiagnostic[];
55
+ }
56
+
57
+ async function pathExists(p: string): Promise<boolean> {
58
+ try {
59
+ await access(p);
60
+ return true;
61
+ } catch {
62
+ return false;
63
+ }
64
+ }
65
+
66
+ function fileKind(filename: string): "typescript" | "markdown" | null {
67
+ if (filename.endsWith(".ts")) return "typescript";
68
+ if (filename.endsWith(".md")) return "markdown";
69
+ return null;
70
+ }
71
+
72
+ function pathDerivedName(filePath: string): string {
73
+ return path.basename(filePath, path.extname(filePath));
74
+ }
75
+
76
+ async function readCatalogEntryId(catalogDir: string): Promise<string | undefined> {
77
+ const domainPath = path.join(catalogDir, "domain.yaml");
78
+ if (!(await pathExists(domainPath))) return undefined;
79
+ const raw = await readFile(domainPath, "utf8");
80
+ const match = raw.match(/^entry_id:\s*["']?([^"'\n]+)["']?/m);
81
+ return match?.[1]?.trim();
82
+ }
83
+
84
+ async function scanNamedFiles(
85
+ slotDir: string,
86
+ slot: Exclude<AuthoredSlot, "catalogs" | "subagents">,
87
+ ): Promise<DiscoveredNamedFile[]> {
88
+ if (!(await pathExists(slotDir))) return [];
89
+ const entries = await readdir(slotDir, { withFileTypes: true });
90
+ const out: DiscoveredNamedFile[] = [];
91
+ for (const entry of entries) {
92
+ if (!entry.isFile()) continue;
93
+ const kind = fileKind(entry.name);
94
+ if (!kind) continue;
95
+ out.push({
96
+ name: pathDerivedName(entry.name),
97
+ path: path.join(slotDir, entry.name),
98
+ kind,
99
+ });
100
+ }
101
+ out.sort((a, b) => a.name.localeCompare(b.name));
102
+ return out;
103
+ }
104
+
105
+ async function scanCatalogs(
106
+ agentRoot: string,
107
+ diagnostics: DiscoveryDiagnostic[],
108
+ ): Promise<DiscoveredCatalog[]> {
109
+ const catalogsDir = path.join(agentRoot, "catalogs");
110
+ if (!(await pathExists(catalogsDir))) return [];
111
+
112
+ const entries = await readdir(catalogsDir, { withFileTypes: true });
113
+ const out: DiscoveredCatalog[] = [];
114
+
115
+ for (const entry of entries) {
116
+ const catalogDir = path.join(catalogsDir, entry.name);
117
+ const dirStat = await stat(catalogDir).catch(() => null);
118
+ if (!dirStat?.isDirectory()) continue;
119
+ const domainYaml = path.join(catalogDir, "domain.yaml");
120
+ const mappingsYaml = path.join(catalogDir, "mappings.yaml");
121
+
122
+ if (!(await pathExists(domainYaml))) {
123
+ diagnostics.push({
124
+ level: "error",
125
+ slot: "catalogs",
126
+ path: catalogDir,
127
+ message: "catalog directory is missing domain.yaml",
128
+ });
129
+ continue;
130
+ }
131
+ if (!(await pathExists(mappingsYaml))) {
132
+ diagnostics.push({
133
+ level: "error",
134
+ slot: "catalogs",
135
+ path: catalogDir,
136
+ message: "catalog directory is missing mappings.yaml",
137
+ });
138
+ continue;
139
+ }
140
+
141
+ const entryId = await readCatalogEntryId(catalogDir);
142
+ out.push({
143
+ name: entry.name,
144
+ path: catalogDir,
145
+ entryId,
146
+ });
147
+ }
148
+
149
+ out.sort((a, b) => a.name.localeCompare(b.name));
150
+ return out;
151
+ }
152
+
153
+ async function scanSubagents(
154
+ agentRoot: string,
155
+ diagnostics: DiscoveryDiagnostic[],
156
+ ): Promise<DiscoveredSubagent[]> {
157
+ const subagentsDir = path.join(agentRoot, "subagents");
158
+ if (!(await pathExists(subagentsDir))) return [];
159
+
160
+ const entries = await readdir(subagentsDir, { withFileTypes: true });
161
+ const out: DiscoveredSubagent[] = [];
162
+
163
+ for (const entry of entries) {
164
+ const subagentDir = path.join(subagentsDir, entry.name);
165
+ const dirStat = await stat(subagentDir).catch(() => null);
166
+ if (!dirStat?.isDirectory()) continue;
167
+ const agentPath = path.join(subagentDir, "agent.ts");
168
+ if (!(await pathExists(agentPath))) {
169
+ diagnostics.push({
170
+ level: "warning",
171
+ slot: "subagents",
172
+ path: subagentDir,
173
+ message: "subagent directory is missing agent.ts",
174
+ });
175
+ continue;
176
+ }
177
+ out.push({
178
+ name: entry.name,
179
+ path: subagentDir,
180
+ agentPath,
181
+ });
182
+ }
183
+
184
+ out.sort((a, b) => a.name.localeCompare(b.name));
185
+ return out;
186
+ }
187
+
188
+ async function scanInstructions(
189
+ agentRoot: string,
190
+ diagnostics: DiscoveryDiagnostic[],
191
+ ): Promise<DiscoveredInstructions | undefined> {
192
+ const markdownPath = path.join(agentRoot, "instructions.md");
193
+ const typescriptPath = path.join(agentRoot, "instructions.ts");
194
+
195
+ const hasMarkdown = await pathExists(markdownPath);
196
+ const hasTypescript = await pathExists(typescriptPath);
197
+
198
+ if (hasMarkdown && hasTypescript) {
199
+ diagnostics.push({
200
+ level: "warning",
201
+ slot: "instructions",
202
+ path: agentRoot,
203
+ message: "both instructions.md and instructions.ts exist; markdown wins",
204
+ });
205
+ }
206
+
207
+ if (hasMarkdown) {
208
+ return { path: markdownPath, kind: "markdown" };
209
+ }
210
+ if (hasTypescript) {
211
+ return { path: typescriptPath, kind: "typescript" };
212
+ }
213
+
214
+ diagnostics.push({
215
+ level: "warning",
216
+ slot: "instructions",
217
+ path: agentRoot,
218
+ message: "no instructions.md or instructions.ts found",
219
+ });
220
+ return undefined;
221
+ }
222
+
223
+ /** Walk `agent/` and collect eve-shaped discovery metadata (catalogs replace tools/). */
224
+ export async function walkAgentProject(agentRoot: string): Promise<ProjectDiscovery> {
225
+ const resolvedRoot = path.resolve(agentRoot);
226
+ const rootStat = await stat(resolvedRoot).catch(() => null);
227
+ const diagnostics: DiscoveryDiagnostic[] = [];
228
+
229
+ if (!rootStat?.isDirectory()) {
230
+ diagnostics.push({
231
+ level: "error",
232
+ slot: "agent",
233
+ path: resolvedRoot,
234
+ message: "agent root is not a directory",
235
+ });
236
+ return {
237
+ agentRoot: resolvedRoot,
238
+ catalogs: [],
239
+ skills: [],
240
+ channels: [],
241
+ schedules: [],
242
+ hooks: [],
243
+ subagents: [],
244
+ diagnostics,
245
+ };
246
+ }
247
+
248
+ const [instructions, catalogs, skills, channels, schedules, hooks, subagents] =
249
+ await Promise.all([
250
+ scanInstructions(resolvedRoot, diagnostics),
251
+ scanCatalogs(resolvedRoot, diagnostics),
252
+ scanNamedFiles(path.join(resolvedRoot, "skills"), "skills"),
253
+ scanNamedFiles(path.join(resolvedRoot, "channels"), "channels"),
254
+ scanNamedFiles(path.join(resolvedRoot, "schedules"), "schedules"),
255
+ scanNamedFiles(path.join(resolvedRoot, "hooks"), "hooks"),
256
+ scanSubagents(resolvedRoot, diagnostics),
257
+ ]);
258
+
259
+ return {
260
+ agentRoot: resolvedRoot,
261
+ instructions,
262
+ catalogs,
263
+ skills,
264
+ channels,
265
+ schedules,
266
+ hooks,
267
+ subagents,
268
+ diagnostics,
269
+ };
270
+ }
271
+
272
+ export const authoredSlots: readonly AuthoredSlot[] = SLOT_DIRS;
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Vercel Connect broker for outbound catalog HTTP (per-request tokens; never stored).
3
+ */
4
+
5
+ export type ConnectTokenSubject =
6
+ | { type: "app" }
7
+ | { type: "user"; id: string };
8
+
9
+ export interface ConnectAuthOptions {
10
+ connector?: string;
11
+ subject?: ConnectTokenSubject;
12
+ installationId?: string;
13
+ scopes?: string[];
14
+ vercelToken?: string;
15
+ }
16
+
17
+ /** Raised when Connect requires an end-user OAuth consent flow. */
18
+ export class ConnectAuthorizationRequiredError extends Error {
19
+ readonly connector: string;
20
+ readonly authorizationUrl?: string;
21
+
22
+ constructor(message: string, connector: string, authorizationUrl?: string) {
23
+ super(message);
24
+ this.name = "ConnectAuthorizationRequiredError";
25
+ this.connector = connector;
26
+ this.authorizationUrl = authorizationUrl;
27
+ }
28
+ }
29
+
30
+ function entryEnvSuffix(entryId: string): string {
31
+ return entryId.toUpperCase().replace(/[^A-Z0-9]/g, "_");
32
+ }
33
+
34
+ /** Resolve connector UID for a catalog `entry_id` from env. */
35
+ export function connectorUidForEntry(entryId?: string): string | undefined {
36
+ const trimmed = entryId?.trim();
37
+ if (trimmed) {
38
+ const perEntry =
39
+ process.env[`PLASM_CONNECTOR_${entryEnvSuffix(trimmed)}`]?.trim() ??
40
+ process.env[`CONNECTOR_${entryEnvSuffix(trimmed)}`]?.trim();
41
+ if (perEntry) return perEntry;
42
+ }
43
+ return (
44
+ process.env.PLASM_CONNECTOR_DEFAULT?.trim() ??
45
+ process.env.CONNECTOR_DEFAULT?.trim()
46
+ );
47
+ }
48
+
49
+ function defaultSubject(): ConnectTokenSubject {
50
+ const type = process.env.PLASM_CONNECT_SUBJECT_TYPE?.trim().toLowerCase();
51
+ if (type === "user") {
52
+ const id = process.env.PLASM_CONNECT_USER_ID?.trim();
53
+ if (!id) {
54
+ throw new Error(
55
+ "PLASM_CONNECT_SUBJECT_TYPE=user requires PLASM_CONNECT_USER_ID",
56
+ );
57
+ }
58
+ return { type: "user", id };
59
+ }
60
+ return { type: "app" };
61
+ }
62
+
63
+ function parseScopes(raw?: string): string[] | undefined {
64
+ const value = raw?.trim();
65
+ if (!value) return undefined;
66
+ const parts = value.split(/[\s,]+/).map((s) => s.trim()).filter(Boolean);
67
+ return parts.length ? parts : undefined;
68
+ }
69
+
70
+ export function connectAuthOptionsForEntry(entryId?: string): ConnectAuthOptions | undefined {
71
+ const connector = connectorUidForEntry(entryId);
72
+ if (!connector) return undefined;
73
+ return {
74
+ connector,
75
+ subject: defaultSubject(),
76
+ installationId: process.env.PLASM_CONNECT_INSTALLATION_ID?.trim() || undefined,
77
+ scopes: parseScopes(process.env.PLASM_CONNECT_SCOPES),
78
+ vercelToken: process.env.VERCEL_OIDC_TOKEN?.trim() || undefined,
79
+ };
80
+ }
81
+
82
+ type ConnectModule = typeof import("@vercel/connect");
83
+
84
+ async function loadConnect(): Promise<ConnectModule> {
85
+ return import("@vercel/connect");
86
+ }
87
+
88
+ function isUserAuthRequired(err: unknown): err is { authorizationUrl?: string } {
89
+ return (
90
+ err != null &&
91
+ typeof err === "object" &&
92
+ (err as { name?: string }).name === "UserAuthorizationRequiredError"
93
+ );
94
+ }
95
+
96
+ /** Mint a short-lived Connect access token for `entryId`, or undefined when not configured. */
97
+ export async function resolveConnectBearer(
98
+ entryId?: string,
99
+ overrides?: Partial<ConnectAuthOptions>,
100
+ ): Promise<string | undefined> {
101
+ const base = connectAuthOptionsForEntry(entryId);
102
+ if (!base?.connector) return undefined;
103
+
104
+ const connector = overrides?.connector ?? base.connector;
105
+ const subject = overrides?.subject ?? base.subject ?? { type: "app" as const };
106
+ const installationId = overrides?.installationId ?? base.installationId;
107
+ const scopes = overrides?.scopes ?? base.scopes;
108
+ const vercelToken = overrides?.vercelToken ?? base.vercelToken;
109
+
110
+ try {
111
+ const { getToken } = await loadConnect();
112
+ return await getToken(
113
+ connector,
114
+ {
115
+ subject,
116
+ ...(installationId ? { installationId } : {}),
117
+ ...(scopes ? { scopes } : {}),
118
+ },
119
+ vercelToken ? { vercelToken } : undefined,
120
+ );
121
+ } catch (err: unknown) {
122
+ if (isUserAuthRequired(err)) {
123
+ const url =
124
+ typeof (err as { authorizationUrl?: string }).authorizationUrl === "string"
125
+ ? (err as { authorizationUrl: string }).authorizationUrl
126
+ : undefined;
127
+ throw new ConnectAuthorizationRequiredError(
128
+ `Vercel Connect authorization required for connector ${connector}`,
129
+ connector,
130
+ url,
131
+ );
132
+ }
133
+ throw err;
134
+ }
135
+ }
@@ -0,0 +1,7 @@
1
+ import { createDefaultHostTransport } from "./host-transport.js";
2
+ import type { HostTransportFn } from "./napi-binding.js";
3
+
4
+ /** Production host transport: env bearer → Vercel Connect `getToken()` → fetch. */
5
+ export function createProductionHostTransport(): HostTransportFn {
6
+ return createDefaultHostTransport({ useConnect: true });
7
+ }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Dev mock HTTP for fixture catalogs (`http://example.test` backends).
3
+ * Enables full `plasm_run` smoke without a live API.
4
+ */
5
+
6
+ import type { HostTransportFn } from "./napi-binding.js";
7
+
8
+ /** Mock product list/get responses for `execute_tiny` fixture paths. */
9
+ export function createFixtureMockTransport(): HostTransportFn {
10
+ return async (request) => {
11
+ const path = (() => {
12
+ try {
13
+ return new URL(request.url).pathname;
14
+ } catch {
15
+ return request.url;
16
+ }
17
+ })();
18
+
19
+ if (path.includes("/products/") && path !== "/products" && !path.includes("/search")) {
20
+ return {
21
+ status: 200,
22
+ body: JSON.stringify({ id: "p1", name: "Widget", category_id: "c1" }),
23
+ };
24
+ }
25
+ if (path.includes("/products")) {
26
+ return {
27
+ status: 200,
28
+ body: JSON.stringify([{ id: "p1", name: "Widget", category_id: "c1" }]),
29
+ };
30
+ }
31
+ if (path.includes("/categories")) {
32
+ return {
33
+ status: 200,
34
+ body: JSON.stringify({ id: "c1", name: "Gadgets" }),
35
+ };
36
+ }
37
+
38
+ return {
39
+ status: 404,
40
+ body: JSON.stringify({ error: `fixture mock: unhandled path ${path}` }),
41
+ };
42
+ };
43
+ }
44
+
45
+ export function fixtureMockTransportEnabled(): boolean {
46
+ const flag = process.env.PLASM_AGENT_MOCK_HTTP?.trim();
47
+ if (flag === "1" || flag === "true") return true;
48
+ if (flag === "0" || flag === "false") return false;
49
+ // Default on for local dev when no outbound bearer is configured.
50
+ return !(
51
+ process.env.PLASM_BEARER?.trim() ||
52
+ process.env.PLASM_AUTH_BEARER?.trim()
53
+ );
54
+ }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * NAPI bridge: Rust threadsafe transport awaits a JavaScript Promise return.
3
+ */
4
+
5
+ import type { HostTransportFn, HostTransportRequest, HostTransportResponse } from "./napi-binding.js";
6
+
7
+ function isPromise<T>(value: unknown): value is Promise<T> {
8
+ return (
9
+ value != null &&
10
+ typeof value === "object" &&
11
+ "then" in value &&
12
+ typeof (value as Promise<T>).then === "function"
13
+ );
14
+ }
15
+
16
+ /** Normalize sync or async host transport for `@plasm_lang/engine` `runPlanLive`. */
17
+ export function toNapiHostTransport(
18
+ transport: HostTransportFn,
19
+ ): (request: HostTransportRequest) => Promise<HostTransportResponse> {
20
+ return async (request) => {
21
+ const out = transport(request);
22
+ const resolved = isPromise<HostTransportResponse>(out) ? await out : await Promise.resolve(out);
23
+ if (resolved.status == null || resolved.body == null) {
24
+ throw new Error("host transport response must include `status` and `body`");
25
+ }
26
+ return {
27
+ status: resolved.status,
28
+ body: resolved.body,
29
+ nextUrl: resolved.nextUrl,
30
+ };
31
+ };
32
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Default outbound HTTP transport for local dev: `fetch` + bearer from env.
3
+ */
4
+
5
+ import type { HostTransportFn, HostTransportRequest } from "./napi-binding.js";
6
+ import { resolveConnectBearer } from "./connect-auth.js";
7
+ import { plasmSpans } from "../telemetry/plasm-spans.js";
8
+
9
+ export type HostTransportOptions = {
10
+ bearer?: string;
11
+ fetchImpl?: typeof fetch;
12
+ /** When true (default), resolve Vercel Connect tokens after env bearer misses. */
13
+ useConnect?: boolean;
14
+ };
15
+
16
+ function bearerFromEnv(entryId?: string): string | undefined {
17
+ const keys = [
18
+ entryId ? `PLASM_${entryId.toUpperCase().replace(/[^A-Z0-9]/g, "_")}_BEARER` : undefined,
19
+ "PLASM_BEARER",
20
+ "PLASM_AUTH_BEARER",
21
+ ].filter(Boolean) as string[];
22
+ for (const key of keys) {
23
+ const value = process.env[key]?.trim();
24
+ if (value) return value;
25
+ }
26
+ return undefined;
27
+ }
28
+
29
+ export function createDefaultHostTransport(options?: HostTransportOptions): HostTransportFn {
30
+ const fetchImpl = options?.fetchImpl ?? fetch;
31
+ const bearerOverride = options?.bearer;
32
+ const useConnect = options?.useConnect ?? true;
33
+
34
+ return async (request: HostTransportRequest) => {
35
+ const host = (() => {
36
+ try {
37
+ return new URL(request.url).host;
38
+ } catch {
39
+ return request.url;
40
+ }
41
+ })();
42
+
43
+ return plasmSpans.transportHttp(
44
+ {
45
+ entryId: request.entryId,
46
+ transportHost: host,
47
+ },
48
+ async (span) => {
49
+ const headers = new Headers(request.headers ?? {});
50
+ if (!headers.has("authorization")) {
51
+ let bearer = bearerOverride ?? bearerFromEnv(request.entryId);
52
+ if (!bearer && useConnect) {
53
+ bearer = await resolveConnectBearer(request.entryId);
54
+ }
55
+ if (bearer) {
56
+ headers.set("authorization", bearer.startsWith("Bearer ") ? bearer : `Bearer ${bearer}`);
57
+ }
58
+ }
59
+ if (request.body != null && !headers.has("content-type")) {
60
+ headers.set("content-type", "application/json; charset=utf-8");
61
+ }
62
+
63
+ const init: RequestInit = {
64
+ method: request.method,
65
+ headers,
66
+ };
67
+ if (request.body != null && request.method !== "GET" && request.method !== "HEAD") {
68
+ init.body = request.body;
69
+ }
70
+
71
+ const response = await fetchImpl(request.url, init);
72
+ const text = await response.text();
73
+ const nextUrl = response.headers.get("link")?.match(/<([^>]+)>;\s*rel="?next"?/i)?.[1];
74
+
75
+ span.setAttribute("plasm.transport.status", response.status);
76
+ return {
77
+ status: response.status,
78
+ body: text,
79
+ nextUrl: nextUrl ?? undefined,
80
+ };
81
+ },
82
+ );
83
+ };
84
+ }