@tt-a1i/openpi 0.4.0 → 0.6.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 (141) hide show
  1. package/README.md +116 -46
  2. package/SETUP.md +29 -7
  3. package/THIRD_PARTY_NOTICES.md +16 -0
  4. package/assets/openpi-launch-card-v1.webp +0 -0
  5. package/bin/openpi.js +155 -0
  6. package/extensions/ai-providers/LICENSE.upstream +23 -0
  7. package/extensions/ai-providers/README.md +59 -0
  8. package/extensions/ai-providers/antigravity/credentials.ts +52 -0
  9. package/extensions/ai-providers/antigravity/discovery.ts +130 -0
  10. package/extensions/ai-providers/antigravity/google-conversion.ts +455 -0
  11. package/extensions/ai-providers/antigravity/models.ts +84 -0
  12. package/extensions/ai-providers/antigravity/oauth.ts +700 -0
  13. package/extensions/ai-providers/antigravity/provider.ts +1116 -0
  14. package/extensions/ai-providers/antigravity/routing.ts +340 -0
  15. package/extensions/ai-providers/antigravity/with-resolvers.d.ts +19 -0
  16. package/extensions/ai-providers/cursor/constants.ts +5 -0
  17. package/extensions/ai-providers/cursor/credentials.ts +14 -0
  18. package/extensions/ai-providers/cursor/discovery.ts +291 -0
  19. package/extensions/ai-providers/cursor/input-images.ts +106 -0
  20. package/extensions/ai-providers/cursor/models.ts +45 -0
  21. package/extensions/ai-providers/cursor/oauth.ts +263 -0
  22. package/extensions/ai-providers/cursor/proto.ts +1064 -0
  23. package/extensions/ai-providers/cursor/protobuf.ts +1171 -0
  24. package/extensions/ai-providers/cursor/provider.ts +1175 -0
  25. package/extensions/ai-providers/cursor/proxy.ts +213 -0
  26. package/extensions/ai-providers/cursor/with-resolvers.d.ts +12 -0
  27. package/extensions/ai-providers/index.ts +86 -0
  28. package/extensions/ai-providers/oauth-adapter.ts +81 -0
  29. package/extensions/ai-providers/usage.ts +10 -0
  30. package/extensions/background-terminals/index.ts +38 -3
  31. package/extensions/background-terminals/src/domain.ts +2 -0
  32. package/extensions/background-terminals/src/manager.ts +484 -106
  33. package/extensions/background-terminals/src/output.ts +33 -0
  34. package/extensions/background-terminals/src/prompt.ts +13 -5
  35. package/extensions/background-terminals/src/result-delivery.ts +47 -24
  36. package/extensions/clear-context/index.ts +83 -0
  37. package/extensions/context-pivot/index.ts +16 -6
  38. package/extensions/cron/index.ts +68 -27
  39. package/extensions/cron/schedule.ts +12 -2
  40. package/extensions/file-mutation-display/render.ts +17 -257
  41. package/extensions/file-search/src/binaries.ts +57 -41
  42. package/extensions/git-read/index.ts +1 -3
  43. package/extensions/model-info/cache-diagnostics.ts +220 -0
  44. package/extensions/model-info/index.ts +65 -33
  45. package/extensions/model-info/session-metrics.ts +96 -0
  46. package/extensions/plan-mode/bash-policy.ts +54 -9
  47. package/extensions/plan-mode/index.ts +82 -6
  48. package/extensions/post-edit/index.ts +16 -6
  49. package/extensions/sessions/git-stats.ts +258 -72
  50. package/extensions/sessions/index.ts +153 -86
  51. package/extensions/sessions/preview-cache.ts +104 -0
  52. package/extensions/sessions/preview-loader.ts +856 -0
  53. package/extensions/sessions/sessions.ts +43 -4
  54. package/extensions/setup/index.ts +138 -130
  55. package/extensions/shared/activity-status.ts +30 -0
  56. package/extensions/shared/agent-session-page.ts +319 -0
  57. package/extensions/shared/agent-tool-renderer.ts +218 -0
  58. package/extensions/shared/agent-transcript.ts +524 -0
  59. package/extensions/shared/capability-intent.ts +1 -1
  60. package/extensions/shared/child-session.ts +457 -21
  61. package/extensions/shared/completion-inbox.ts +193 -0
  62. package/extensions/shared/result-delivery.ts +34 -0
  63. package/extensions/shared/setup-config.ts +83 -34
  64. package/extensions/shared/setup-episode-state.ts +1 -1
  65. package/extensions/shared/structured-output.ts +154 -0
  66. package/extensions/shared/terminal-text.ts +110 -23
  67. package/extensions/shared/text-projection.ts +72 -15
  68. package/extensions/shared/tool-activity.ts +382 -0
  69. package/extensions/shared/tool-surface.ts +29 -2
  70. package/extensions/shared/transcript-viewport.ts +46 -0
  71. package/extensions/shared/web-observer-registry.ts +390 -0
  72. package/extensions/shared/worktree.ts +11 -0
  73. package/extensions/subagents/index.ts +313 -62
  74. package/extensions/subagents/navigation.ts +34 -5
  75. package/extensions/subagents/src/backend.ts +12 -1
  76. package/extensions/subagents/src/backends/pi.ts +450 -70
  77. package/extensions/subagents/src/domain.ts +21 -1
  78. package/extensions/subagents/src/manager.ts +39 -2
  79. package/extensions/subagents/src/prompt.ts +49 -7
  80. package/extensions/subagents/src/result-artifact.ts +36 -0
  81. package/extensions/subagents/src/result-delivery.ts +39 -14
  82. package/extensions/subagents/src/runtime.ts +15 -1
  83. package/extensions/subagents/src/ui/takeover.ts +73 -257
  84. package/extensions/subagents/src/ui/transcript.ts +38 -535
  85. package/extensions/subagents/src/ui/wait-result.ts +103 -15
  86. package/extensions/suggestions/src/ui.ts +10 -4
  87. package/extensions/tasks/index.ts +0 -3
  88. package/extensions/ui-customization/footer.ts +16 -45
  89. package/extensions/ui-customization/index.ts +0 -4
  90. package/extensions/user-input-fold/index.ts +42 -6
  91. package/extensions/web/index.ts +257 -0
  92. package/extensions/workflows/acceptance.ts +43 -19
  93. package/extensions/workflows/artifacts.ts +137 -47
  94. package/extensions/workflows/completion-projection.ts +459 -0
  95. package/extensions/workflows/coordinator.ts +8 -10
  96. package/extensions/workflows/dashboard.ts +175 -228
  97. package/extensions/workflows/handoff.ts +70 -16
  98. package/extensions/workflows/index.ts +501 -198
  99. package/extensions/workflows/journal.ts +148 -13
  100. package/extensions/workflows/model.ts +79 -5
  101. package/extensions/workflows/navigation.ts +32 -8
  102. package/extensions/workflows/progress-projection.ts +306 -0
  103. package/extensions/workflows/prompt.ts +70 -16
  104. package/extensions/workflows/replay-safety.ts +42 -21
  105. package/extensions/workflows/result-delivery.ts +214 -76
  106. package/extensions/workflows/retention.ts +599 -0
  107. package/extensions/workflows/runner.ts +389 -345
  108. package/extensions/workflows/sandbox-child.cjs +25 -3
  109. package/extensions/workflows/sandbox.ts +62 -8
  110. package/extensions/workflows/serialization.ts +325 -17
  111. package/extensions/workflows/tool-renderer.ts +22 -0
  112. package/extensions/workflows/transcript.ts +149 -0
  113. package/extensions/workspace-cleanup-guard/index.ts +54 -0
  114. package/extensions/workspace-cleanup-guard/workspace-provenance.ts +563 -0
  115. package/package.json +34 -14
  116. package/skills/subagents/REFERENCE.md +190 -0
  117. package/skills/subagents/SKILL.md +2 -1
  118. package/skills/workflows/REFERENCE.md +6 -4
  119. package/skills/workflows/SKILL.md +1 -1
  120. package/web/adapter/pi-adapter.ts +664 -0
  121. package/web/host/browser-launcher.ts +20 -0
  122. package/web/host/pi-coding-agent-entry.ts +162 -0
  123. package/web/host/static-assets.ts +4 -0
  124. package/web/host/terminal-status.ts +38 -0
  125. package/web/host/web-host.ts +1069 -0
  126. package/web/http-dispatcher.ts +125 -0
  127. package/web/protocol/types.ts +467 -0
  128. package/web/runtime/pi-runtime.ts +1206 -0
  129. package/web/runtime/types.ts +102 -0
  130. package/web/runtime/web-host-lease.ts +497 -0
  131. package/web/trace.ts +18 -0
  132. package/web/ui/app.js +1700 -0
  133. package/web/ui/index.html +142 -0
  134. package/web/ui/styles.css +680 -0
  135. package/web/vite.config.mjs +34 -0
  136. package/extensions/execution-convergence/active-evidence.ts +0 -129
  137. package/extensions/execution-convergence/index.ts +0 -442
  138. package/extensions/execution-convergence/workspace-provenance.ts +0 -338
  139. package/extensions/setup/intercom-fs-helper.cjs +0 -130
  140. package/extensions/setup/intercom.ts +0 -603
  141. package/extensions/subagents/src/backends/stub.ts +0 -303
@@ -1,12 +1,22 @@
1
+ import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
1
2
  import * as path from "node:path";
3
+ import { fileURLToPath } from "node:url";
2
4
  import {
3
- type AgentSession,
5
+ DefaultPackageManager,
4
6
  DefaultResourceLoader,
5
7
  getAgentDir,
8
+ type LoadExtensionsResult,
9
+ type PackageSource,
6
10
  ProjectTrustStore,
11
+ type ResolvedPaths,
7
12
  type SessionShutdownEvent,
8
13
  SettingsManager,
9
14
  } from "@earendil-works/pi-coding-agent";
15
+ import {
16
+ OPENPI_OWNER_SOURCE_PATHS,
17
+ OPENPI_TOOL_SURFACE,
18
+ type OpenPiToolOwner,
19
+ } from "./tool-surface.ts";
10
20
 
11
21
  export const CHILD_SHUTDOWN_TIMEOUT_MS = 5_000;
12
22
 
@@ -37,16 +47,405 @@ export const CHILD_SAFE_PACKAGE_TOOL_NAMES = [
37
47
  * Loading it in children can therefore cross-wire identities; the parent
38
48
  * extension remains loaded and fully usable.
39
49
  */
40
- function isPiIntercomNpmResource(resource: {
41
- resolvedPath?: string;
42
- filePath?: string;
50
+ function manifestNamesPiIntercom(manifestPath: string) {
51
+ try {
52
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8")) as unknown;
53
+ return (
54
+ typeof manifest === "object" &&
55
+ manifest !== null &&
56
+ !Array.isArray(manifest) &&
57
+ (manifest as Record<string, unknown>).name === "pi-intercom"
58
+ );
59
+ } catch (error) {
60
+ throw new Error(
61
+ `Cannot verify child package identity from ${manifestPath}`,
62
+ { cause: error },
63
+ );
64
+ }
65
+ }
66
+
67
+ function installedPathNamesPiIntercom(installedPath: string) {
68
+ try {
69
+ const stats = statSync(installedPath);
70
+ if (stats.isDirectory()) {
71
+ const manifestPath = path.join(installedPath, "package.json");
72
+ if (existsSync(manifestPath)) {
73
+ return manifestNamesPiIntercom(manifestPath);
74
+ }
75
+ if (path.basename(installedPath) === "pi-intercom") {
76
+ throw new Error(
77
+ `Cannot verify child package identity from ${installedPath}`,
78
+ );
79
+ }
80
+ return false;
81
+ }
82
+ if (!stats.isFile()) {
83
+ throw new Error(
84
+ `Cannot verify child package identity from ${installedPath}`,
85
+ );
86
+ }
87
+
88
+ let current = path.dirname(installedPath);
89
+ let hasIntercomDirectoryName = false;
90
+ while (true) {
91
+ hasIntercomDirectoryName ||= path.basename(current) === "pi-intercom";
92
+ const manifestPath = path.join(current, "package.json");
93
+ if (existsSync(manifestPath)) {
94
+ return manifestNamesPiIntercom(manifestPath);
95
+ }
96
+ const parent = path.dirname(current);
97
+ if (parent === current) {
98
+ if (hasIntercomDirectoryName) {
99
+ throw new Error(
100
+ `Cannot verify child package identity from ${installedPath}`,
101
+ );
102
+ }
103
+ return false;
104
+ }
105
+ current = parent;
106
+ }
107
+ } catch (error) {
108
+ if (
109
+ error instanceof Error &&
110
+ error.message.startsWith("Cannot verify child package identity")
111
+ ) {
112
+ throw error;
113
+ }
114
+ throw new Error(
115
+ `Cannot verify child package identity from ${installedPath}`,
116
+ { cause: error },
117
+ );
118
+ }
119
+ }
120
+
121
+ function piMatchesPublishedIntercomSource(options: {
122
+ source: string;
123
+ cwd: string;
124
+ agentDir: string;
125
+ }) {
126
+ const settingsManager = SettingsManager.inMemory({
127
+ packages: [options.source],
128
+ });
129
+ const packageManager = new DefaultPackageManager({
130
+ cwd: options.cwd,
131
+ agentDir: options.agentDir,
132
+ settingsManager,
133
+ });
134
+ return (
135
+ packageManager.removeSourceFromSettings("npm:pi-intercom") ||
136
+ packageManager.removeSourceFromSettings(
137
+ "git:https://github.com/nicobailon/pi-intercom",
138
+ )
139
+ );
140
+ }
141
+
142
+ function createPiIntercomPackageMatcher(options: {
143
+ cwd: string;
144
+ agentDir: string;
145
+ }) {
146
+ const sourceMatches = new Map<string, boolean>();
147
+ const installedPathMatches = new Map<string, boolean>();
148
+ return (source: string, installedPath?: string) => {
149
+ let sourceMatch = sourceMatches.get(source);
150
+ if (sourceMatch === undefined) {
151
+ sourceMatch = piMatchesPublishedIntercomSource({
152
+ source,
153
+ cwd: options.cwd,
154
+ agentDir: options.agentDir,
155
+ });
156
+ sourceMatches.set(source, sourceMatch);
157
+ }
158
+ if (sourceMatch || installedPath === undefined) return sourceMatch;
159
+
160
+ const resolvedPath = path.resolve(installedPath);
161
+ let installedPathMatch = installedPathMatches.get(resolvedPath);
162
+ if (installedPathMatch === undefined) {
163
+ installedPathMatch = installedPathNamesPiIntercom(resolvedPath);
164
+ installedPathMatches.set(resolvedPath, installedPathMatch);
165
+ }
166
+ return installedPathMatch;
167
+ };
168
+ }
169
+
170
+ function packageSourceValue(source: PackageSource) {
171
+ return typeof source === "string" ? source : source.source;
172
+ }
173
+
174
+ const CHILD_DISABLED_OPENPI_EXTENSION =
175
+ "-extensions/git-info/index.ts" as const;
176
+
177
+ function canonicalExistingPath(value: string) {
178
+ try {
179
+ return realpathSync.native(value);
180
+ } catch {
181
+ return undefined;
182
+ }
183
+ }
184
+
185
+ /**
186
+ * ENOENT means git-info is truly absent (trimmed fork, partial install):
187
+ * nothing to exclude. Any other failure (permissions, symlink loops) means we
188
+ * cannot verify the Git-polling extension is present, so fail closed instead
189
+ * of running a child with an unverifiable extension.
190
+ */
191
+ export function resolveGitInfoPathOrThrow(
192
+ resolve: (value: string) => string = realpathSync.native,
193
+ ): string | undefined {
194
+ try {
195
+ return resolve(
196
+ fileURLToPath(new URL("../git-info/index.ts", import.meta.url)),
197
+ );
198
+ } catch (error) {
199
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
200
+ throw error;
201
+ }
202
+ }
203
+
204
+ function excludeOpenPiGitInfoExtension(
205
+ resources: LoadExtensionsResult,
206
+ resolve: (value: string) => string = realpathSync.native,
207
+ ): LoadExtensionsResult {
208
+ const gitInfoPath = resolveGitInfoPathOrThrow(resolve);
209
+ // undefined = absent (ENOENT): nothing to exclude. Non-ENOENT failures
210
+ // throw from resolveGitInfoPathOrThrow and fail child creation upstream.
211
+ if (gitInfoPath === undefined) return resources;
212
+ return {
213
+ ...resources,
214
+ extensions: resources.extensions.filter(
215
+ (extension) =>
216
+ canonicalExistingPath(extension.resolvedPath) !== gitInfoPath,
217
+ ),
218
+ };
219
+ }
220
+
221
+ function installedPathNamesOpenPi(installedPath: string) {
222
+ try {
223
+ const stats = statSync(installedPath);
224
+ if (stats.isDirectory()) {
225
+ const manifestPath = path.join(installedPath, "package.json");
226
+ if (!existsSync(manifestPath)) return false;
227
+ const manifest = JSON.parse(
228
+ readFileSync(manifestPath, "utf8"),
229
+ ) as unknown;
230
+ return (
231
+ typeof manifest === "object" &&
232
+ manifest !== null &&
233
+ !Array.isArray(manifest) &&
234
+ (manifest as Record<string, unknown>).name === "@tt-a1i/openpi"
235
+ );
236
+ }
237
+ let current = stats.isFile() ? path.dirname(installedPath) : undefined;
238
+ while (current) {
239
+ const manifestPath = path.join(current, "package.json");
240
+ if (existsSync(manifestPath)) {
241
+ const manifest = JSON.parse(
242
+ readFileSync(manifestPath, "utf8"),
243
+ ) as unknown;
244
+ return (
245
+ typeof manifest === "object" &&
246
+ manifest !== null &&
247
+ !Array.isArray(manifest) &&
248
+ (manifest as Record<string, unknown>).name === "@tt-a1i/openpi"
249
+ );
250
+ }
251
+ const parent = path.dirname(current);
252
+ if (parent === current) return false;
253
+ current = parent;
254
+ }
255
+ return false;
256
+ } catch {
257
+ return false;
258
+ }
259
+ }
260
+
261
+ function piMatchesPublishedOpenPiSource(options: {
262
+ source: string;
263
+ cwd: string;
264
+ agentDir: string;
265
+ }) {
266
+ const settingsManager = SettingsManager.inMemory({
267
+ packages: [options.source],
268
+ });
269
+ const packageManager = new DefaultPackageManager({
270
+ cwd: options.cwd,
271
+ agentDir: options.agentDir,
272
+ settingsManager,
273
+ });
274
+ return (
275
+ packageManager.removeSourceFromSettings("npm:@tt-a1i/openpi") ||
276
+ packageManager.removeSourceFromSettings(
277
+ "git:https://github.com/openpi-dev/openpi",
278
+ ) ||
279
+ packageManager.removeSourceFromSettings(
280
+ "git:https://github.com/tt-a1i/openpi",
281
+ )
282
+ );
283
+ }
284
+
285
+ function createOpenPiPackageMatcher(options: {
286
+ cwd: string;
287
+ agentDir: string;
288
+ }) {
289
+ const sourceMatches = new Map<string, boolean>();
290
+ const installedPathMatches = new Map<string, boolean>();
291
+ return (source: string, installedPath?: string) => {
292
+ let sourceMatch = sourceMatches.get(source);
293
+ if (sourceMatch === undefined) {
294
+ sourceMatch = piMatchesPublishedOpenPiSource({ source, ...options });
295
+ sourceMatches.set(source, sourceMatch);
296
+ }
297
+ if (sourceMatch || installedPath === undefined) return sourceMatch;
298
+
299
+ const resolvedPath = path.resolve(installedPath);
300
+ let installedPathMatch = installedPathMatches.get(resolvedPath);
301
+ if (installedPathMatch === undefined) {
302
+ installedPathMatch = installedPathNamesOpenPi(resolvedPath);
303
+ installedPathMatches.set(resolvedPath, installedPathMatch);
304
+ }
305
+ return installedPathMatch;
306
+ };
307
+ }
308
+
309
+ function openPiPackageSources(
310
+ packageManager: DefaultPackageManager,
311
+ options: { cwd: string; agentDir: string },
312
+ ) {
313
+ const isOpenPiPackage = createOpenPiPackageMatcher(options);
314
+ const sources = {
315
+ user: new Set<string>(),
316
+ project: new Set<string>(),
317
+ };
318
+ for (const configured of packageManager.listConfiguredPackages()) {
319
+ if (isOpenPiPackage(configured.source, configured.installedPath)) {
320
+ sources[configured.scope].add(configured.source);
321
+ }
322
+ }
323
+ return sources;
324
+ }
325
+
326
+ function disablesOpenPiGitInfo(pattern: string) {
327
+ return (
328
+ pattern.startsWith("-") &&
329
+ pattern.slice(1).replace(/^\.\//, "") ===
330
+ CHILD_DISABLED_OPENPI_EXTENSION.slice(1)
331
+ );
332
+ }
333
+
334
+ function disableOpenPiGitInfo(source: PackageSource): PackageSource {
335
+ if (typeof source !== "string") {
336
+ if (source.extensions?.length === 0) return source;
337
+ if (source.autoload === false && source.extensions === undefined) {
338
+ return source;
339
+ }
340
+ }
341
+ const configured = typeof source === "string" ? { source } : source;
342
+ const extensions = [...(configured.extensions ?? [])];
343
+ if (!extensions.some(disablesOpenPiGitInfo)) {
344
+ extensions.push(CHILD_DISABLED_OPENPI_EXTENSION);
345
+ }
346
+ return { ...configured, extensions };
347
+ }
348
+
349
+ function blockedPackageSources(
350
+ packageManager: DefaultPackageManager,
351
+ resolvedPaths: ResolvedPaths,
352
+ options: { cwd: string; agentDir: string },
353
+ ) {
354
+ const isPiIntercomPackage = createPiIntercomPackageMatcher(options);
355
+ const blocked = {
356
+ user: new Set<string>(),
357
+ project: new Set<string>(),
358
+ };
359
+ for (const configured of packageManager.listConfiguredPackages()) {
360
+ if (isPiIntercomPackage(configured.source, configured.installedPath)) {
361
+ blocked[configured.scope].add(configured.source);
362
+ }
363
+ }
364
+
365
+ const resources = [
366
+ ...resolvedPaths.extensions,
367
+ ...resolvedPaths.skills,
368
+ ...resolvedPaths.prompts,
369
+ ...resolvedPaths.themes,
370
+ ];
371
+ for (const resource of resources) {
372
+ const { metadata } = resource;
373
+ if (
374
+ metadata.origin !== "package" ||
375
+ metadata.scope === "temporary" ||
376
+ !isPiIntercomPackage(metadata.source, metadata.baseDir)
377
+ ) {
378
+ continue;
379
+ }
380
+ blocked[metadata.scope].add(metadata.source);
381
+ }
382
+ return blocked;
383
+ }
384
+
385
+ function createEphemeralChildSettings(
386
+ sourceSettings: SettingsManager,
387
+ blockedSources: { user: Set<string>; project: Set<string> },
388
+ openPiSources: { user: Set<string>; project: Set<string> },
389
+ projectTrusted: boolean,
390
+ ) {
391
+ const globalSettings = sourceSettings.getGlobalSettings();
392
+ const projectSettings = sourceSettings.getProjectSettings();
393
+ const settingsForScope = (
394
+ settings: typeof globalSettings,
395
+ scope: "user" | "project",
396
+ ) => ({
397
+ ...settings,
398
+ packages: settings.packages
399
+ ?.filter(
400
+ (source) => !blockedSources[scope].has(packageSourceValue(source)),
401
+ )
402
+ .map((source) =>
403
+ openPiSources[scope].has(packageSourceValue(source))
404
+ ? disableOpenPiGitInfo(source)
405
+ : source,
406
+ ),
407
+ });
408
+ const contents = {
409
+ global: JSON.stringify(settingsForScope(globalSettings, "user")),
410
+ project: JSON.stringify(settingsForScope(projectSettings, "project")),
411
+ };
412
+ const storage: Parameters<typeof SettingsManager.fromStorage>[0] = {
413
+ withLock(scope, update) {
414
+ const next = update(contents[scope]);
415
+ if (next !== undefined) contents[scope] = next;
416
+ },
417
+ };
418
+ return SettingsManager.fromStorage(storage, { projectTrusted });
419
+ }
420
+
421
+ async function createChildSettingsManager(options: {
422
+ cwd: string;
423
+ agentDir: string;
424
+ projectTrusted: boolean;
43
425
  }) {
44
- const resourcePath = resource.resolvedPath ?? resource.filePath;
45
- if (!resourcePath) return false;
46
- const segments = path.resolve(resourcePath).split(path.sep);
47
- return segments.some(
48
- (segment, index) =>
49
- segment === "node_modules" && segments[index + 1] === "pi-intercom",
426
+ const sourceSettings = SettingsManager.create(options.cwd, options.agentDir, {
427
+ projectTrusted: options.projectTrusted,
428
+ });
429
+ const packageManager = new DefaultPackageManager({
430
+ cwd: options.cwd,
431
+ agentDir: options.agentDir,
432
+ settingsManager: sourceSettings,
433
+ });
434
+ const resolvedPaths = await packageManager.resolve(async () => "skip");
435
+ const blockedSources = blockedPackageSources(packageManager, resolvedPaths, {
436
+ cwd: options.cwd,
437
+ agentDir: options.agentDir,
438
+ });
439
+ const openPiSources = openPiPackageSources(packageManager, {
440
+ cwd: options.cwd,
441
+ agentDir: options.agentDir,
442
+ });
443
+
444
+ return createEphemeralChildSettings(
445
+ sourceSettings,
446
+ blockedSources,
447
+ openPiSources,
448
+ options.projectTrusted,
50
449
  );
51
450
  }
52
451
 
@@ -96,6 +495,42 @@ export const CHILD_EXCLUDED_TOOL_NAMES = [
96
495
  "context_pivot",
97
496
  ] as const;
98
497
 
498
+ const PARENT_ONLY_OPENPI_EXTENSION_PATHS = new Set(
499
+ (Object.keys(OPENPI_TOOL_SURFACE) as OpenPiToolOwner[])
500
+ .filter((owner) => {
501
+ const { entry, deferred } = OPENPI_TOOL_SURFACE[owner];
502
+ const toolNames = [...entry, ...deferred];
503
+ return (
504
+ toolNames.length > 0 &&
505
+ toolNames.every((name) =>
506
+ CHILD_EXCLUDED_TOOL_NAMES.includes(name as never),
507
+ )
508
+ );
509
+ })
510
+ .map((owner) => canonicalExistingPath(OPENPI_OWNER_SOURCE_PATHS[owner]))
511
+ .filter(
512
+ (extensionPath): extensionPath is string => extensionPath !== undefined,
513
+ ),
514
+ );
515
+
516
+ function isVerifiedParentOnlyOpenPiExtension(extension: {
517
+ path: string;
518
+ resolvedPath: string;
519
+ sourceInfo: { path: string };
520
+ }) {
521
+ return [
522
+ extension.path,
523
+ extension.resolvedPath,
524
+ extension.sourceInfo.path,
525
+ ].some((candidate) => {
526
+ const canonicalPath = canonicalExistingPath(candidate);
527
+ return (
528
+ canonicalPath !== undefined &&
529
+ PARENT_ONLY_OPENPI_EXTENSION_PATHS.has(canonicalPath)
530
+ );
531
+ });
532
+ }
533
+
99
534
  /**
100
535
  * Fresh SDK options avoid turning the denylist into an accidental allowlist.
101
536
  *
@@ -133,26 +568,27 @@ export interface ChildResourceOptions {
133
568
  /** Load normal global/package resources and trust-gated project resources. */
134
569
  export async function createChildResources(options: ChildResourceOptions) {
135
570
  const agentDir = options.agentDir ?? getAgentDir();
136
- const settingsManager = SettingsManager.create(options.cwd, agentDir, {
571
+ const settingsManager = await createChildSettingsManager({
572
+ cwd: options.cwd,
573
+ agentDir,
137
574
  projectTrusted: options.projectTrusted,
138
575
  });
139
576
  const loader = new DefaultResourceLoader({
140
577
  cwd: options.cwd,
141
578
  agentDir,
142
579
  settingsManager,
580
+ extensionsOverride(base) {
581
+ const withoutGitInfo = excludeOpenPiGitInfoExtension(base);
582
+ return {
583
+ ...withoutGitInfo,
584
+ extensions: withoutGitInfo.extensions.filter(
585
+ (extension) => !isVerifiedParentOnlyOpenPiExtension(extension),
586
+ ),
587
+ };
588
+ },
143
589
  ...(options.appendSystemPrompt
144
590
  ? { appendSystemPrompt: options.appendSystemPrompt }
145
591
  : {}),
146
- extensionsOverride: (base) => ({
147
- ...base,
148
- extensions: base.extensions.filter(
149
- (extension) => !isPiIntercomNpmResource(extension),
150
- ),
151
- }),
152
- skillsOverride: (base) => ({
153
- ...base,
154
- skills: base.skills.filter((skill) => !isPiIntercomNpmResource(skill)),
155
- }),
156
592
  });
157
593
  await loader.reload();
158
594
  return { loader, settingsManager };
@@ -0,0 +1,193 @@
1
+ export type CompletionProducer = "subagent" | "workflow" | "background";
2
+ export type CompletionWakePolicy =
3
+ | "follow-up"
4
+ | "next-turn"
5
+ | "producer-policy";
6
+
7
+ export interface CompletionOwner {
8
+ readonly sessionId: string;
9
+ readonly epoch: number;
10
+ }
11
+
12
+ export interface CompletionSessionIdentity {
13
+ getSessionId(): string;
14
+ }
15
+
16
+ /** Transport metadata only; producer state remains the terminal authority. */
17
+ export interface CompletionEnvelope<T> {
18
+ readonly deliveryId: string;
19
+ readonly owner: CompletionOwner;
20
+ readonly producer: CompletionProducer;
21
+ readonly producerId: string;
22
+ readonly terminalRef: unknown;
23
+ readonly wake: CompletionWakePolicy;
24
+ readonly payload: T;
25
+ }
26
+
27
+ export interface CompletionDeadLetter {
28
+ readonly deliveryId: string;
29
+ readonly producer: CompletionProducer;
30
+ readonly producerId: string;
31
+ readonly failure: "owner-unavailable" | "stale-owner";
32
+ }
33
+
34
+ function sameOwner(left: CompletionOwner, right: CompletionOwner) {
35
+ return (
36
+ Boolean(left.sessionId) &&
37
+ left.sessionId !== "unowned" &&
38
+ left.sessionId === right.sessionId &&
39
+ left.epoch === right.epoch
40
+ );
41
+ }
42
+
43
+ let nextOwnerEpoch = 1;
44
+ const ownerBySessionIdentity = new WeakMap<object, CompletionOwner>();
45
+
46
+ /**
47
+ * Bind one process-local generation to a Pi SessionManager identity.
48
+ *
49
+ * All OpenPI producers observing the same manager share an owner. A replaced
50
+ * manager, or a manager whose Session id changes, receives a new epoch so late
51
+ * callbacks cannot target the replacement transcript.
52
+ */
53
+ export function completionOwnerFor(
54
+ identity: CompletionSessionIdentity,
55
+ ): CompletionOwner {
56
+ const sessionId = identity.getSessionId();
57
+ const existing = ownerBySessionIdentity.get(identity);
58
+ if (existing?.sessionId === sessionId) return existing;
59
+ const owner = { sessionId, epoch: nextOwnerEpoch++ };
60
+ ownerBySessionIdentity.set(identity, owner);
61
+ return owner;
62
+ }
63
+
64
+ /**
65
+ * One atomic consumption gate shared by all background producers.
66
+ *
67
+ * Claim removes before transport; a failed transport retries the exact
68
+ * envelopes. A successful transport leaves them consumed. No execution facts
69
+ * or result bytes are stored here.
70
+ */
71
+ export function createCompletionInbox<T>() {
72
+ const pending = new Map<string, CompletionEnvelope<T>>();
73
+ const inFlight = new Map<string, CompletionEnvelope<T>>();
74
+ const deadLetters: CompletionDeadLetter[] = [];
75
+
76
+ const reject = (
77
+ envelope: CompletionEnvelope<T>,
78
+ failure: CompletionDeadLetter["failure"],
79
+ ) => {
80
+ deadLetters.push({
81
+ deliveryId: envelope.deliveryId,
82
+ producer: envelope.producer,
83
+ producerId: envelope.producerId,
84
+ failure,
85
+ });
86
+ return false;
87
+ };
88
+
89
+ const admit = (
90
+ envelope: CompletionEnvelope<T>,
91
+ owner: CompletionOwner | undefined,
92
+ ) => {
93
+ if (!owner) return reject(envelope, "owner-unavailable");
94
+ if (!sameOwner(envelope.owner, owner)) {
95
+ return reject(envelope, "stale-owner");
96
+ }
97
+ pending.set(envelope.deliveryId, envelope);
98
+ return true;
99
+ };
100
+
101
+ /** Restore a failed attempt ahead of completions that arrived meanwhile. */
102
+ const retry = (
103
+ envelopes: readonly CompletionEnvelope<T>[],
104
+ owner: CompletionOwner | undefined,
105
+ ) => {
106
+ const current = [...pending.values()];
107
+ pending.clear();
108
+ for (const envelope of envelopes) {
109
+ inFlight.delete(envelope.deliveryId);
110
+ admit(envelope, owner);
111
+ }
112
+ for (const envelope of current) admit(envelope, owner);
113
+ };
114
+
115
+ return {
116
+ defer(envelope: CompletionEnvelope<T>, owner: CompletionOwner | undefined) {
117
+ return admit(envelope, owner);
118
+ },
119
+
120
+ /** Explicit status/wait and automatic delivery atomically race here. */
121
+ consume(producer: CompletionProducer, producerIds: Iterable<string>) {
122
+ const ids = new Set(producerIds);
123
+ for (const [deliveryId, envelope] of pending) {
124
+ if (envelope.producer === producer && ids.has(envelope.producerId)) {
125
+ pending.delete(deliveryId);
126
+ inFlight.delete(deliveryId);
127
+ }
128
+ }
129
+ },
130
+
131
+ consumeDeliveryIds(deliveryIds: Iterable<string>) {
132
+ for (const deliveryId of deliveryIds) {
133
+ pending.delete(deliveryId);
134
+ inFlight.delete(deliveryId);
135
+ }
136
+ },
137
+
138
+ claim(
139
+ owner: CompletionOwner | undefined,
140
+ maximum = Number.POSITIVE_INFINITY,
141
+ ) {
142
+ const claimed: CompletionEnvelope<T>[] = [];
143
+ for (const [deliveryId, envelope] of pending) {
144
+ if (claimed.length >= maximum) break;
145
+ pending.delete(deliveryId);
146
+ if (!owner) {
147
+ reject(envelope, "owner-unavailable");
148
+ continue;
149
+ }
150
+ if (!sameOwner(envelope.owner, owner)) {
151
+ reject(envelope, "stale-owner");
152
+ continue;
153
+ }
154
+ inFlight.set(deliveryId, envelope);
155
+ claimed.push(envelope);
156
+ }
157
+ return claimed;
158
+ },
159
+
160
+ retry,
161
+
162
+ retryClaimed(
163
+ producer: CompletionProducer,
164
+ producerIds: Iterable<string>,
165
+ owner: CompletionOwner | undefined,
166
+ ) {
167
+ const ids = new Set(producerIds);
168
+ const envelopes = [...inFlight.values()].filter(
169
+ (envelope) =>
170
+ envelope.producer === producer && ids.has(envelope.producerId),
171
+ );
172
+ retry(envelopes, owner);
173
+ },
174
+
175
+ acknowledge(deliveryIds: Iterable<string>) {
176
+ for (const deliveryId of deliveryIds) inFlight.delete(deliveryId);
177
+ },
178
+
179
+ size() {
180
+ return pending.size;
181
+ },
182
+
183
+ inspectDeadLetters() {
184
+ return [...deadLetters];
185
+ },
186
+
187
+ clear() {
188
+ pending.clear();
189
+ inFlight.clear();
190
+ deadLetters.length = 0;
191
+ },
192
+ };
193
+ }