@fungi.computer/oyster 0.5.0 → 0.7.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 (185) hide show
  1. package/README.md +155 -63
  2. package/dist/agent-archive-projection.d.ts +8 -0
  3. package/dist/agent-archive-projection.js +49 -0
  4. package/dist/agent-schedules.d.ts +50 -0
  5. package/dist/agent-schedules.js +17 -0
  6. package/dist/agents.d.ts +35 -14
  7. package/dist/agents.js +97 -144
  8. package/dist/app-builds.d.ts +7 -2
  9. package/dist/app-builds.js +19 -2
  10. package/dist/approval-capability.d.ts +1 -1
  11. package/dist/approval-capability.js +11 -16
  12. package/dist/approval-home.d.ts +2 -1
  13. package/dist/approval-home.js +5 -3
  14. package/dist/apps/catalog.js +1 -1
  15. package/dist/apps/gate.d.ts +4 -7
  16. package/dist/apps/gate.js +14 -29
  17. package/dist/apps/index.d.ts +1 -1
  18. package/dist/apps/installation.d.ts +4 -42
  19. package/dist/apps/installation.js +7 -75
  20. package/dist/apps/operations.js +0 -10
  21. package/dist/apps/permissions.d.ts +18 -8
  22. package/dist/apps/permissions.js +7 -1
  23. package/dist/auth.js +9 -1
  24. package/dist/authorization.d.ts +307 -0
  25. package/dist/authorization.js +542 -0
  26. package/dist/boot-host-script.js +1 -1
  27. package/dist/build/build-jobs.d.ts +2 -2
  28. package/dist/build/candidate-builder.d.ts +2 -0
  29. package/dist/build/candidate-builder.js +5 -1
  30. package/dist/build/candidate-cli.js +3 -0
  31. package/dist/build/candidate-cli.mjs +67 -22
  32. package/dist/build/job-log.d.ts +23 -0
  33. package/dist/build/job-log.js +88 -0
  34. package/dist/build/local-build.d.ts +2 -0
  35. package/dist/build/local-build.js +11 -9
  36. package/dist/channels/binding.d.ts +8 -1
  37. package/dist/channels/binding.js +12 -0
  38. package/dist/channels/conversations.d.ts +16 -5
  39. package/dist/channels/visitor-channel.d.ts +53 -0
  40. package/dist/channels/visitor-channel.js +124 -0
  41. package/dist/channels/visitor-edge.d.ts +115 -0
  42. package/dist/channels/visitor-edge.js +582 -0
  43. package/dist/channels/visitor-request.d.ts +7 -0
  44. package/dist/channels/visitor-request.js +17 -0
  45. package/dist/channels/visitor-stream-slots.d.ts +2 -0
  46. package/dist/channels/visitor-stream-slots.js +33 -0
  47. package/dist/channels/visitors.d.ts +47 -0
  48. package/dist/channels/visitors.js +181 -0
  49. package/dist/cloudflare.d.ts +11 -2
  50. package/dist/cloudflare.js +27 -1
  51. package/dist/{page/computer-target.d.ts → computer-target.d.ts} +2 -0
  52. package/dist/{page/computer-target.js → computer-target.js} +3 -1
  53. package/dist/files.js +9 -8
  54. package/dist/git-admission.d.ts +3 -3
  55. package/dist/git-admission.js +4 -6
  56. package/dist/git-door.d.ts +8 -7
  57. package/dist/git-door.js +18 -17
  58. package/dist/git.d.ts +1 -1
  59. package/dist/host.d.ts +38 -42
  60. package/dist/host.js +182 -239
  61. package/dist/installation-execution.js +10 -5
  62. package/dist/installation-operations.d.ts +4 -180
  63. package/dist/installation-operations.js +1 -5
  64. package/dist/installation-store-generation.d.ts +4 -1
  65. package/dist/installation-store-generation.js +59 -6
  66. package/dist/internal/delivery.js +0 -1
  67. package/dist/internal/host-gate.d.ts +9 -6
  68. package/dist/internal/host-gate.js +19 -25
  69. package/dist/internal/invocation-timing.d.ts +16 -0
  70. package/dist/internal/invocation-timing.js +23 -0
  71. package/dist/internal/registry-installations.js +1 -0
  72. package/dist/invocation.d.ts +19 -11
  73. package/dist/invocation.js +171 -96
  74. package/dist/keys.d.ts +2 -19
  75. package/dist/keys.js +1 -37
  76. package/dist/mcp.d.ts +15 -27
  77. package/dist/mcp.js +105 -155
  78. package/dist/page/agent-browser.d.ts +15 -0
  79. package/dist/page/agent-browser.js +162 -0
  80. package/dist/page/app-permissions-port.d.ts +3 -0
  81. package/dist/page/app-permissions-port.js +43 -0
  82. package/dist/page/app-permissions.d.ts +11 -0
  83. package/dist/page/app-permissions.js +296 -0
  84. package/dist/page/capabilities.js +1 -1
  85. package/dist/page/child-selection.d.ts +27 -0
  86. package/dist/page/child-selection.js +38 -0
  87. package/dist/page/isolated-page-theme.d.ts +2 -0
  88. package/dist/page/isolated-page-theme.js +11 -0
  89. package/dist/page/mount.d.ts +2 -13
  90. package/dist/page/mount.js +8 -94
  91. package/dist/page-script.d.ts +0 -3
  92. package/dist/page-script.js +3 -4
  93. package/dist/release.d.ts +13 -0
  94. package/dist/release.js +10 -0
  95. package/dist/request-origin.d.ts +4 -0
  96. package/dist/request-origin.js +10 -0
  97. package/dist/standalone/agent-archive-owner.d.ts +42 -0
  98. package/dist/standalone/agent-archive-owner.js +116 -0
  99. package/dist/standalone/agent-archive-schedules.d.ts +6 -0
  100. package/dist/standalone/agent-archive-schedules.js +76 -0
  101. package/dist/standalone/agent-archive.d.ts +31 -0
  102. package/dist/standalone/agent-archive.js +67 -0
  103. package/dist/standalone/agent-computers.d.ts +7 -6
  104. package/dist/standalone/agent-computers.js +4 -7
  105. package/dist/standalone/agent-mail.d.ts +2 -2
  106. package/dist/standalone/agents.d.ts +157 -20
  107. package/dist/standalone/agents.js +242 -38
  108. package/dist/standalone/app-builds-binding.js +1 -1
  109. package/dist/standalone/app-set.js +2 -54
  110. package/dist/standalone/app-source.d.ts +3 -4
  111. package/dist/standalone/app-source.js +12 -9
  112. package/dist/standalone/builds.d.ts +2 -14
  113. package/dist/standalone/builds.js +15 -13
  114. package/dist/standalone/computer-fleet.d.ts +4 -4
  115. package/dist/standalone/computer-fleet.js +20 -43
  116. package/dist/standalone/computer.d.ts +4 -3
  117. package/dist/standalone/computer.js +52 -53
  118. package/dist/standalone/dashboard-approval.d.ts +10 -0
  119. package/dist/standalone/dashboard-approval.js +66 -0
  120. package/dist/standalone/machine-link.d.ts +4 -221
  121. package/dist/standalone/machine-link.js +21 -15
  122. package/dist/standalone/media.d.ts +1 -0
  123. package/dist/standalone/media.js +20 -14
  124. package/dist/standalone/owner-routes.d.ts +16 -0
  125. package/dist/standalone/owner-routes.js +21 -0
  126. package/dist/standalone/permission-resources.d.ts +5 -0
  127. package/dist/standalone/permission-resources.js +31 -0
  128. package/dist/standalone/registry.d.ts +7 -0
  129. package/dist/standalone/registry.js +3 -0
  130. package/dist/standalone/tenancy.js +3 -1
  131. package/dist/standalone/worker-routes.d.ts +19 -0
  132. package/dist/standalone/worker-routes.js +9 -0
  133. package/dist/standalone/worker.d.ts +1 -1
  134. package/dist/standalone/worker.js +27 -35
  135. package/dist/standalone/workflow-binding.d.ts +8 -3
  136. package/dist/standalone/workflow-binding.js +14 -5
  137. package/dist/standalone/workflow-git-capture.js +2 -1
  138. package/dist/standalone/workflow-host.d.ts +8 -103
  139. package/dist/standalone/workflow-host.js +1 -1
  140. package/dist/standalone/workflow-node-capture.js +2 -2
  141. package/dist/standalone/workflow-root-authority.d.ts +1 -1
  142. package/dist/standalone/workflow-root-authority.js +1 -1
  143. package/dist/standalone/workflow-schedules.d.ts +14 -5
  144. package/dist/standalone/workflow-schedules.js +11 -5
  145. package/dist/standalone/workflow-webhooks.d.ts +4 -18
  146. package/dist/standalone/workflow-webhooks.js +5 -5
  147. package/dist/standalone/workflows.d.ts +36 -28
  148. package/dist/standalone/workflows.js +6 -3
  149. package/dist/storage-frame.d.ts +6 -6
  150. package/dist/testing/capabilities.d.ts +7 -6
  151. package/dist/testing/capabilities.js +3 -6
  152. package/dist/testing/installation.d.ts +0 -1
  153. package/dist/testing/installation.js +2 -5
  154. package/dist/turnstile.d.ts +27 -0
  155. package/dist/turnstile.js +51 -0
  156. package/dist/workers-git.d.ts +12 -4
  157. package/dist/workers-git.js +58 -36
  158. package/dist/workers.d.ts +9 -1
  159. package/dist/workers.js +59 -20
  160. package/dist/workflows.d.ts +2 -0
  161. package/dist/workflows.js +1 -0
  162. package/docs/api/README.md +7 -0
  163. package/docs/api/agent-schedules.md +175 -0
  164. package/docs/api/agents.md +34 -13
  165. package/docs/api/app-storage.md +1 -1
  166. package/docs/api/apps.md +323 -225
  167. package/docs/api/authorization.md +1300 -0
  168. package/docs/api/build-contract.md +2 -2
  169. package/docs/api/build.md +40 -37
  170. package/docs/api/cloudflare.md +27 -2
  171. package/docs/api/git.md +20 -11
  172. package/docs/api/host.md +80 -199
  173. package/docs/api/index.md +1 -1
  174. package/docs/api/invocation.md +15 -11
  175. package/docs/api/keys.md +1 -98
  176. package/docs/api/mcp.md +43 -40
  177. package/docs/api/modules.md +2 -0
  178. package/docs/api/page/mount.md +16 -19
  179. package/docs/api/workers-git.md +46 -12
  180. package/docs/api/workers.md +125 -235
  181. package/docs/site/agent-quickstart.mdx +6 -2
  182. package/examples/standalone/wrangler.celld.jsonc +66 -0
  183. package/package.json +64 -21
  184. package/dist/standalone/channel-session-door.d.ts +0 -4
  185. package/dist/standalone/channel-session-door.js +0 -42
@@ -0,0 +1,11 @@
1
+ import { type AppPermissionsPort } from "../apps/permissions.js";
2
+ /** The shared editor's host navigation guard, not an approval decision. */
3
+ export type PermissionLeaveState = "clean" | "dirty" | "pending" | "uncertain";
4
+ export type AppPermissionsViewProps = Readonly<{
5
+ port: AppPermissionsPort;
6
+ appKey: string;
7
+ onAccepted?: () => Promise<void>;
8
+ onStateChange?: (state: PermissionLeaveState) => void;
9
+ }>;
10
+ /** One editor; each App owns its draft and pending-request lifetime on reuse. */
11
+ export declare function AppPermissionsView(props: AppPermissionsViewProps): import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,296 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useCallback, useEffect, useId, useRef, useState } from "react";
3
+ import { Button } from "@fungi.computer/caps/components/button";
4
+ import { Card, CardContent } from "@fungi.computer/caps/components/card";
5
+ import { Checkbox } from "@fungi.computer/caps/components/checkbox";
6
+ import { DataList, DataRow } from "@fungi.computer/caps/components/data-list";
7
+ import { Form, FormFooter } from "@fungi.computer/caps/components/field";
8
+ import { IconCheckbox } from "@fungi.computer/caps/components/icon-checkbox";
9
+ import { Status } from "@fungi.computer/caps/components/status";
10
+ import { Display, ScreenCell, ScreenCells, } from "@fungi.computer/caps/components/surface";
11
+ import { Sigil } from "@fungi.computer/sigils";
12
+ import { permissionCatalog } from "../apps/catalog.js";
13
+ import { draftOf, editPermissions, permissionScreen, planGrantChange, rebaseDraft, } from "../apps/permissions.js";
14
+ /** One editor; each App owns its draft and pending-request lifetime on reuse. */
15
+ export function AppPermissionsView(props) {
16
+ return _jsx(PermissionEditor, { ...props }, props.appKey);
17
+ }
18
+ function PermissionEditor({ port, appKey, onAccepted, onStateChange, }) {
19
+ const [view, setView] = useState(null);
20
+ const [draft, setDraft] = useState(new Map());
21
+ const [saving, setSaving] = useState(false);
22
+ const [uncertain, setUncertain] = useState(false);
23
+ const [retired, setRetired] = useState(false);
24
+ const [refreshNeeded, setRefreshNeeded] = useState(false);
25
+ const [message, setMessage] = useState(null);
26
+ const [loadError, setLoadError] = useState(false);
27
+ const [reload, setReload] = useState(0);
28
+ const mounted = useRef(true);
29
+ const legendId = useId();
30
+ const retire = useCallback(() => {
31
+ setRetired(true);
32
+ setUncertain(false);
33
+ setMessage("This App is no longer available. Reload before trying again.");
34
+ }, []);
35
+ useEffect(() => {
36
+ mounted.current = true;
37
+ let current = true;
38
+ setLoadError(false);
39
+ void port.read(appKey).then((next) => {
40
+ if (!current)
41
+ return;
42
+ if (next === "unavailable") {
43
+ retire();
44
+ return;
45
+ }
46
+ setView(next);
47
+ setDraft(draftOf(next.permissions.authority));
48
+ }, () => {
49
+ if (current)
50
+ setLoadError(true);
51
+ });
52
+ return () => {
53
+ current = false;
54
+ mounted.current = false;
55
+ };
56
+ }, [port, appKey, reload, retire]);
57
+ const changes = view === null ? [] : planGrantChange(view.permissions.authority, draft);
58
+ const leaveState = saving
59
+ ? "pending"
60
+ : retired
61
+ ? "clean"
62
+ : uncertain
63
+ ? "uncertain"
64
+ : changes.length > 0
65
+ ? "dirty"
66
+ : "clean";
67
+ useEffect(() => onStateChange?.(leaveState), [onStateChange, leaveState]);
68
+ async function refreshAfterAcceptance() {
69
+ try {
70
+ await onAccepted?.();
71
+ if (!mounted.current)
72
+ return;
73
+ setRefreshNeeded(false);
74
+ setMessage("Permissions saved.");
75
+ }
76
+ catch (cause) {
77
+ console.error("App permissions surrounding-screen refresh failed", cause);
78
+ if (!mounted.current)
79
+ return;
80
+ setRefreshNeeded(true);
81
+ setMessage("Permissions saved. The surrounding screen could not refresh.");
82
+ }
83
+ }
84
+ async function retryRefresh() {
85
+ setSaving(true);
86
+ try {
87
+ await refreshAfterAcceptance();
88
+ }
89
+ finally {
90
+ if (mounted.current)
91
+ setSaving(false);
92
+ }
93
+ }
94
+ async function save() {
95
+ const isMounted = () => mounted.current;
96
+ if (view === null || saving || retired || !view.canEdit)
97
+ return;
98
+ setSaving(true);
99
+ setRefreshNeeded(false);
100
+ setMessage(null);
101
+ try {
102
+ if (!uncertain) {
103
+ const result = await port.submit(appKey, {
104
+ storeGeneration: view.storeGeneration,
105
+ authorityVersion: view.permissions.authority.authorityVersion,
106
+ artifactDigest: view.artifactDigest,
107
+ }, changes);
108
+ if (!isMounted())
109
+ return;
110
+ if (result === "unavailable") {
111
+ retire();
112
+ return;
113
+ }
114
+ if (result === "unconfirmed") {
115
+ setUncertain(true);
116
+ setMessage("The change could not be confirmed. Review current permissions before trying again.");
117
+ return;
118
+ }
119
+ if (result === "refused") {
120
+ setMessage("This permission change was refused. No approval was confirmed.");
121
+ return;
122
+ }
123
+ if (result === "accepted") {
124
+ // A failed read after a successful write must not replay the old draft.
125
+ setUncertain(true);
126
+ const next = await port.read(appKey);
127
+ if (!isMounted())
128
+ return;
129
+ if (next === "unavailable") {
130
+ retire();
131
+ return;
132
+ }
133
+ setView(next);
134
+ setDraft(draftOf(next.permissions.authority));
135
+ setUncertain(false);
136
+ await refreshAfterAcceptance();
137
+ return;
138
+ }
139
+ }
140
+ const next = await port.read(appKey);
141
+ if (!isMounted())
142
+ return;
143
+ if (next === "unavailable") {
144
+ retire();
145
+ return;
146
+ }
147
+ setView(next);
148
+ setDraft(rebaseDraft(draft, view.permissions.authority, next.permissions.authority));
149
+ setUncertain(false);
150
+ setMessage("Current permissions loaded. Review your edits before saving again.");
151
+ }
152
+ catch (cause) {
153
+ console.error("App permissions could not be confirmed", cause);
154
+ if (isMounted()) {
155
+ setUncertain(true);
156
+ setMessage("The change could not be confirmed. Review current permissions before trying again.");
157
+ }
158
+ }
159
+ finally {
160
+ if (isMounted())
161
+ setSaving(false);
162
+ }
163
+ }
164
+ if (loadError)
165
+ return (_jsxs("div", { role: "alert", children: ["Permissions could not be loaded.", _jsx(Button, { type: "button", onClick: () => {
166
+ setReload((value) => value + 1);
167
+ }, children: "Retry permissions" })] }));
168
+ if (view === null)
169
+ return _jsx("p", { role: "status", children: retired ? message : "Loading permissions…" });
170
+ const authority = {
171
+ ...view.permissions.authority,
172
+ approvedGrants: [...draft].flatMap(([capability, grant]) => grant === null
173
+ ? []
174
+ : [{ capability, capabilityApproved: true, ...grant }]),
175
+ };
176
+ const screen = permissionScreen({
177
+ ...view.permissions,
178
+ authority,
179
+ resources: view.resources,
180
+ canEdit: view.canEdit,
181
+ });
182
+ const baseline = permissionScreen({
183
+ ...view.permissions,
184
+ resources: view.resources,
185
+ canEdit: view.canEdit,
186
+ });
187
+ const disabled = !screen.canEdit || saving || uncertain || retired;
188
+ const edit = (change) => {
189
+ setDraft((current) => editPermissions(current, change, view.permissions.declaredOperations));
190
+ };
191
+ const shell = screen.groups
192
+ .flatMap((group) => group.switches)
193
+ .find((column) => column.capability === "team.workbenchHost");
194
+ const shellChanged = changes.some((change) => change.capability === "team.workbenchHost");
195
+ const groups = screen.groups.filter((group) => group.id !== "shell");
196
+ const pending = baseline.pending.filter((capability) => capability !== "team.workbenchHost");
197
+ return (_jsxs(Form, { style: { maxWidth: "46rem" }, onSubmit: (event) => {
198
+ event.preventDefault();
199
+ void save();
200
+ }, children: [!screen.canEdit && _jsx("p", { children: "Only an owner can change these permissions." }), pending.length > 0 && (_jsxs("p", { children: ["Needs approval:", " ", pending
201
+ .map((capability) => permissionCatalog[capability].label)
202
+ .join(", ")] })), shell !== undefined && (_jsx(ShellPermissionRow, { column: shell, changed: shellChanged, otherChanges: changes.length > 1, disabled: disabled, onToggle: () => {
203
+ edit({
204
+ kind: "switch",
205
+ capability: "team.workbenchHost",
206
+ on: !shell.on,
207
+ });
208
+ }, onCancel: () => {
209
+ setDraft((current) => new Map(current).set("team.workbenchHost", draftOf(view.permissions.authority).get("team.workbenchHost") ??
210
+ null));
211
+ } })), groups.length > 0 && (_jsx(Display, { size: "sm", "aria-label": `${view.appName} permissions`, children: _jsx(ScreenCells, { appearance: "compact", children: groups.map((group) => (_jsxs("section", { "aria-label": `${group.id} permissions`, children: [_jsx("h3", { children: group.id }), group.rows.map((row) => (_jsx(ScreenCell, { appearance: "compact", icon: _jsx(Sigil, { name: group.scope === "computer" ? "computer" : "listAgents" }), legend: row.label, children: row.cells.map((cell) => {
212
+ const column = group.columns.find((item) => item.capability === cell.capability);
213
+ if (column === undefined)
214
+ return null;
215
+ return (_jsx(IconCheckbox, { appearance: "compact", icon: _jsx(Sigil, { name: column.sigil }), caption: column.caption, label: `${row.label}: ${column.label}`, "aria-describedby": `${legendId}-${cell.capability}`, checked: cell.state !== "off", disabled: disabled || cell.state === "via-all", onChange: (event) => {
216
+ edit(row.resource === "all"
217
+ ? {
218
+ kind: "all",
219
+ capability: cell.capability,
220
+ on: event.target.checked,
221
+ }
222
+ : {
223
+ kind: "resource",
224
+ capability: cell.capability,
225
+ resource: row.resource,
226
+ on: event.target.checked,
227
+ });
228
+ } }, cell.capability));
229
+ }) }, row.resource === "all"
230
+ ? "all"
231
+ : `${row.resource.kind}:${row.resource.id}`))), group.switches.map((column) => (_jsx(ScreenCell, { appearance: "compact", legend: column.label, children: _jsx(IconCheckbox, { appearance: "compact", icon: _jsx(Sigil, { name: column.sigil }), caption: column.caption, label: column.label, "aria-describedby": `${legendId}-${column.capability}`, checked: column.on, disabled: disabled ||
232
+ permissionCatalog[column.capability].consent ===
233
+ "affordance", onChange: (event) => {
234
+ edit({
235
+ kind: "switch",
236
+ capability: column.capability,
237
+ on: event.target.checked,
238
+ });
239
+ } }) }, column.capability))), group.operations.map((operation) => (_jsxs("label", { children: [_jsx(Checkbox, { checked: operation.on, disabled: disabled, onChange: (event) => {
240
+ edit({
241
+ kind: "operation",
242
+ capability: operation.capability,
243
+ operation: operation.operation,
244
+ on: event.target.checked,
245
+ });
246
+ } }), operation.operation] }, operation.operation))), _jsx("dl", { children: [...group.columns, ...group.switches].map((column) => (_jsxs("div", { id: `${legendId}-${column.capability}`, children: [_jsx("dt", { children: column.label }), _jsxs("dd", { children: [column.description, column.warning !== null && _jsx("p", { children: column.warning })] })] }, column.capability))) })] }, group.id))) }) })), screen.groups.length === 0 && _jsx("p", { children: "This App requests no permissions." }), message !== null && (_jsx("p", { role: "status", "aria-live": "polite", children: message })), ((groups.length > 0 && !shellChanged) || uncertain || refreshNeeded) && (_jsxs(FormFooter, { divider: false, children: [refreshNeeded && (_jsx(Button, { type: "button", size: "xs", variant: "outline", disabled: saving, onClick: () => void retryRefresh(), children: "Retry screen refresh" })), _jsx(Button, { type: "submit", size: "xs", variant: "outline", disabled: saving ||
247
+ retired ||
248
+ !screen.canEdit ||
249
+ (!uncertain && changes.length === 0), children: saving
250
+ ? "Saving…"
251
+ : uncertain
252
+ ? "Review current permissions"
253
+ : "Save permissions" })] }))] }));
254
+ }
255
+ /** Shell is a single approval, not a resource/verb matrix. Confirmation still
256
+ * submits the editor's whole draft through its one atomic save. */
257
+ function ShellPermissionRow({ column, changed, otherChanges, disabled, onToggle, onCancel, }) {
258
+ const { on } = column;
259
+ return (_jsx(Card, { variant: "outline", density: "compact", "aria-label": "Shell permissions", children: _jsxs(CardContent, { children: [_jsx(DataList, { density: "compact", children: _jsxs(DataRow, { style: {
260
+ gridTemplateColumns: "20px minmax(0, 1fr) auto",
261
+ alignItems: "center",
262
+ gap: "var(--spacing-sm)",
263
+ padding: 0,
264
+ minHeight: 36,
265
+ }, children: [_jsx(Sigil, { name: column.sigil, size: 20 }), _jsxs("div", { style: {
266
+ display: "flex",
267
+ alignItems: "baseline",
268
+ gap: "var(--spacing-sm)",
269
+ minWidth: 0,
270
+ }, children: [_jsx("strong", { style: { whiteSpace: "nowrap", flexShrink: 0 }, children: column.label }), _jsx("span", { title: column.description, style: {
271
+ color: "var(--stipe-fg-muted)",
272
+ whiteSpace: "nowrap",
273
+ overflow: "hidden",
274
+ textOverflow: "ellipsis",
275
+ }, children: column.description })] }), _jsxs("div", { style: {
276
+ display: "flex",
277
+ alignItems: "center",
278
+ gap: "var(--spacing-sm)",
279
+ }, children: [_jsx(Status, { size: "compact", glyph: "dot", tone: changed ? "warning" : on ? "success" : "neutral", "aria-label": changed
280
+ ? "Shell change not saved"
281
+ : on
282
+ ? "Shell allowed"
283
+ : "Shell not allowed" }), _jsx(Button, { type: "button", size: "xs", variant: changed || !on ? "outline" : "ghost-destructive", disabled: disabled, onClick: changed ? onCancel : onToggle, children: changed ? "Cancel" : on ? "Revoke" : "Allow" })] })] }) }), _jsx("p", { style: {
284
+ margin: 0,
285
+ color: "var(--stipe-fg-muted)",
286
+ fontSize: "var(--text-sm)",
287
+ }, children: column.warning }), changed && (_jsx(FormFooter, { divider: false, status: otherChanges
288
+ ? "Save all permission edits, including Shell access?"
289
+ : on
290
+ ? "Allow Shell full access?"
291
+ : "Revoke Shell access?", children: _jsx(Button, { type: "submit", size: "xs", variant: "outline", disabled: disabled, children: otherChanges
292
+ ? "Save permissions"
293
+ : on
294
+ ? "Confirm allow"
295
+ : "Confirm revoke" }) }))] }) }));
296
+ }
@@ -3,7 +3,7 @@ import { compile, parse, pathToRegexp } from "path-to-regexp";
3
3
  import * as Schema from "effect/Schema";
4
4
  import { AppError } from "@fungi.computer/agar";
5
5
  import { parseJsonValue } from "@fungi.computer/whistle/wire";
6
- import { computerTargetUrl } from "./computer-target.js";
6
+ import { computerTargetUrl } from "../computer-target.js";
7
7
  const queryInput = Schema.Record(Schema.String, Schema.Union([Schema.String, Schema.Number, Schema.Boolean]));
8
8
  const capabilityInputSchema = Schema.Union([Schema.Json, Schema.Undefined]);
9
9
  function resolveRoute(path, input) {
@@ -0,0 +1,27 @@
1
+ /** The launch fields a window names; none of them is authority. */
2
+ export type ChildLaunch = Readonly<{
3
+ appId: string;
4
+ releaseId: string;
5
+ surfaceId: string;
6
+ href: string;
7
+ }>;
8
+ /** The identity the App's own configuration route answers with. */
9
+ export type ChildIdentity = Readonly<{
10
+ installationId: string;
11
+ appId: string;
12
+ releaseId: string;
13
+ computerId: string | null;
14
+ }>;
15
+ /**
16
+ * The installation page a launch names, or null when the address is not one.
17
+ * Only an `/apps/<installationId>/` page is ever asked for its configuration.
18
+ */
19
+ export declare function childInstallationAddress(href: string, origin: string): URL | null;
20
+ /**
21
+ * The launch still names what the host serves at its address: the same
22
+ * installation, App and Release, under the admitted Computer. The host's
23
+ * configuration read is the one fresh answer; no directory read precedes it.
24
+ */
25
+ export declare function childMatchesLaunch(launch: ChildLaunch, pathname: string, config: ChildIdentity, computerTarget: string | undefined): boolean;
26
+ /** Concurrent callers share the one read in flight; a settled read is never reused. */
27
+ export declare function sharedRead<A>(read: () => Promise<A>): () => Promise<A>;
@@ -0,0 +1,38 @@
1
+ import { addressKey } from "@fungi.computer/nacre/app-directory";
2
+ const installationPath = /^\/apps\/[^/]+\/$/u;
3
+ /**
4
+ * The installation page a launch names, or null when the address is not one.
5
+ * Only an `/apps/<installationId>/` page is ever asked for its configuration.
6
+ */
7
+ export function childInstallationAddress(href, origin) {
8
+ const address = new URL(href, origin);
9
+ return address.origin === origin && installationPath.test(address.pathname)
10
+ ? address
11
+ : null;
12
+ }
13
+ /**
14
+ * The launch still names what the host serves at its address: the same
15
+ * installation, App and Release, under the admitted Computer. The host's
16
+ * configuration read is the one fresh answer; no directory read precedes it.
17
+ */
18
+ export function childMatchesLaunch(launch, pathname, config, computerTarget) {
19
+ return (pathname === `/apps/${encodeURIComponent(config.installationId)}/` &&
20
+ launch.surfaceId ===
21
+ addressKey({
22
+ kind: "installation",
23
+ installationId: config.installationId,
24
+ }) &&
25
+ config.appId === launch.appId &&
26
+ config.releaseId === launch.releaseId &&
27
+ (computerTarget === undefined || config.computerId === computerTarget));
28
+ }
29
+ /** Concurrent callers share the one read in flight; a settled read is never reused. */
30
+ export function sharedRead(read) {
31
+ let inFlight;
32
+ return () => {
33
+ inFlight ??= read().finally(() => {
34
+ inFlight = undefined;
35
+ });
36
+ return inFlight;
37
+ };
38
+ }
@@ -0,0 +1,2 @@
1
+ /** Called once by an isolated page entry for its document's lifetime. */
2
+ export declare function bootstrapIsolatedPageTheme(): void;
@@ -0,0 +1,11 @@
1
+ /** Called once by an isolated page entry for its document's lifetime. */
2
+ export function bootstrapIsolatedPageTheme() {
3
+ const appearance = matchMedia("(prefers-color-scheme: dark)");
4
+ const setTheme = () => {
5
+ document.documentElement.dataset.theme = appearance.matches
6
+ ? "mocha"
7
+ : "latte";
8
+ };
9
+ setTheme();
10
+ appearance.addEventListener("change", setTheme);
11
+ }
@@ -1,21 +1,10 @@
1
1
  import { type AppOpenInput, type AppRoleChoice } from "@fungi.computer/agar/apps";
2
+ import { OysterAgentBrowserHost } from "./agent-browser.js";
3
+ export type { OysterAgentBrowserHost } from "./agent-browser.js";
2
4
  import type { WhistleRuntime } from "@fungi.computer/whistle";
3
5
  import * as Schema from "effect/Schema";
4
- import { type CapabilityDirectory, type AgentSessionSelection } from "@fungi.computer/agar";
5
6
  import { type AppHost, type AppHostLaunch, type AppHostHandlers } from "@fungi.computer/agar/host";
6
7
  import type { WhistleJsonValue } from "@fungi.computer/whistle";
7
- /** One page-wide selected Session, using only its reporting App's admitted view. */
8
- export declare class OysterAgentBrowserHost {
9
- #private;
10
- /** End the selected connection with the page's own lifetime. */
11
- constructor(signal: AbortSignal);
12
- /** A departing or deselecting mount cannot close a different active mount. */
13
- release(frame: HTMLIFrameElement): void;
14
- /** Release the page's current selection; later reports cannot reconnect it. */
15
- dispose(): void;
16
- /** Bind the reporting mount's current command view to its explicitly selected Session. */
17
- report(frame: HTMLIFrameElement, selection: AgentSessionSelection | null, capabilities: CapabilityDirectory, whistle: Pick<WhistleRuntime, "snapshot" | "execute">): Promise<void>;
18
- }
19
8
  /** Current server-admitted configuration for one installed App page. */
20
9
  export declare const oysterAppConfigSchema: Schema.Struct<{
21
10
  readonly workbench: Schema.Boolean;
@@ -6,10 +6,9 @@ import { createAppSurfaceCommandHost } from "@fungi.computer/nacre/command-host"
6
6
  // App page: mounts one installed App's sandboxed frame and gives it the
7
7
  // capabilities this Oyster host provides. Bundled into `shellScript`.
8
8
  import { RpcTarget, RpcStub } from "@fungi.computer/capnweb";
9
- import { EventSource } from "eventsource";
10
- import { connectBrowser, UiBrowserError, } from "@fungi.computer/knapsack/ui/browser";
9
+ import { OysterAgentBrowserHost } from "./agent-browser.js";
11
10
  import { createPageLifetime } from "./lifetime.js";
12
- import { computerTargetUrl } from "./computer-target.js";
11
+ import { computerTargetUrl } from "../computer-target.js";
13
12
  import * as Schema from "effect/Schema";
14
13
  import { AppError, createCapabilityRegistry, } from "@fungi.computer/agar";
15
14
  import { appStorage } from "@fungi.computer/agar/storage";
@@ -17,7 +16,7 @@ import { approvals } from "@fungi.computer/agar/approvals";
17
16
  import { oysterStorageHandle } from "./storage.js";
18
17
  import { appBackend, appMethods } from "@fungi.computer/agar/backend";
19
18
  import { computerList, computerPorts, computerProcesses, computerReadFile, computerTerminal, computerWriteFile, } from "@fungi.computer/agar/computer";
20
- import { agentList, agentManage, agentSessionRead, agentSessionWrite, openAppAgent, } from "@fungi.computer/agar/agents";
19
+ import { agentList, agentManage, agentSessionRead, agentSessionWrite, } from "@fungi.computer/agar/agents";
21
20
  import { mediaGeneration } from "@fungi.computer/agar/media";
22
21
  import { MediaGeneration, mediaGenerationDescriptionSchema, } from "@fungi.computer/agar/media-json";
23
22
  import { registeredCapabilityHandle, } from "./capabilities.js";
@@ -27,92 +26,6 @@ import { parseJsonValue } from "@fungi.computer/whistle/wire";
27
26
  import { appDeclaredCommandSchema } from "@fungi.computer/puffball/app";
28
27
  import { TerminalCapability } from "./terminal.js";
29
28
  import { ComputerPortsHandle, ComputerProcessesHandle, } from "./computer-handles.js";
30
- /** One page-wide selected Session, using only its reporting App's admitted view. */
31
- export class OysterAgentBrowserHost {
32
- #active;
33
- #disposed = false;
34
- /** End the selected connection with the page's own lifetime. */
35
- constructor(signal) {
36
- if (signal.aborted)
37
- this.dispose();
38
- else
39
- signal.addEventListener("abort", () => {
40
- this.dispose();
41
- }, { once: true });
42
- }
43
- /** A departing or deselecting mount cannot close a different active mount. */
44
- release(frame) {
45
- if (this.#active?.frame !== frame)
46
- return;
47
- const previous = this.#active;
48
- this.#active = undefined;
49
- previous.close();
50
- }
51
- /** Release the page's current selection; later reports cannot reconnect it. */
52
- dispose() {
53
- this.#disposed = true;
54
- if (this.#active !== undefined)
55
- this.release(this.#active.frame);
56
- }
57
- /** Bind the reporting mount's current command view to its explicitly selected Session. */
58
- async report(frame, selection, capabilities, whistle) {
59
- if (this.#disposed)
60
- return;
61
- if (selection === null) {
62
- this.release(frame);
63
- return;
64
- }
65
- if (this.#active !== undefined)
66
- this.release(this.#active.frame);
67
- // Read-only Apps may observe Sessions, but cannot claim or settle UI work.
68
- if (!capabilities.has(agentSessionWrite) && !capabilities.has(agentManage))
69
- return;
70
- let transport;
71
- let connection;
72
- const failure = () => {
73
- console.warn("Oyster Agent browser connection failed");
74
- };
75
- const active = {
76
- frame,
77
- close: () => {
78
- connection?.close();
79
- if (transport !== undefined)
80
- void transport.close().catch(failure);
81
- },
82
- };
83
- this.#active = active;
84
- try {
85
- transport = await openAppAgent(capabilities, {
86
- agentId: selection.agentId,
87
- });
88
- if (this.#active !== active) {
89
- await transport.close();
90
- return;
91
- }
92
- const admitted = transport;
93
- connection = connectBrowser({
94
- baseUrl: `${admitted.baseUrl}/browser/sessions/${encodeURIComponent(selection.sessionId)}`,
95
- whistle,
96
- transport: {
97
- fetch: admitted.fetch,
98
- openEvents: (url) => new EventSource(url, { fetch: admitted.fetch }),
99
- },
100
- });
101
- void connection.closed.catch((cause) => {
102
- if (this.#active === active)
103
- this.release(frame);
104
- console.warn("Oyster Agent browser connection failed", cause instanceof UiBrowserError
105
- ? cause.message
106
- : "Transport unavailable");
107
- });
108
- }
109
- catch (cause) {
110
- if (this.#active === active)
111
- this.release(frame);
112
- throw cause;
113
- }
114
- }
115
- }
116
29
  function agentDirectory(capabilities) {
117
30
  const directory = createCapabilityRegistry();
118
31
  const read = capabilities.find(({ id, version }) => id === agentSessionRead.id && version === agentSessionRead.version);
@@ -345,9 +258,7 @@ export async function connectOysterApp(options) {
345
258
  throw new Error("App frame is unavailable");
346
259
  const capabilities = await oysterAppCapabilities(options);
347
260
  const agents = agentDirectory(capabilities);
348
- const releaseBrowser = () => {
349
- options.agentBrowser.release(frame);
350
- };
261
+ const releaseBrowser = options.agentBrowser.mount(frame, options.agentCommands);
351
262
  options.signal.addEventListener("abort", releaseBrowser, { once: true });
352
263
  const host = await connectAppHost({
353
264
  appWindow: frame.contentWindow,
@@ -377,7 +288,7 @@ export async function connectOysterApp(options) {
377
288
  handlers: {
378
289
  ...options.handlers,
379
290
  reportAgentSessionSelection: async (selection) => {
380
- await options.agentBrowser.report(frame, selection, agents, options.agentCommands);
291
+ await options.agentBrowser.report(frame, selection, agents);
381
292
  await options.handlers.reportAgentSessionSelection(selection);
382
293
  },
383
294
  onDisconnect: async () => {
@@ -390,6 +301,9 @@ export async function connectOysterApp(options) {
390
301
  signal: options.signal,
391
302
  }),
392
303
  },
304
+ }).catch((cause) => {
305
+ releaseBrowser();
306
+ throw cause;
393
307
  });
394
308
  if (options.signal.aborted)
395
309
  host.dispose();
@@ -1,9 +1,6 @@
1
1
  /** The bundled App page script; the host serves it at a content-hashed path. */
2
2
  export declare const pageScript: string;
3
3
 
4
- /** The bundled public read-only launcher served at a content-hashed path. */
5
- export declare const publicHomeScript: string;
6
-
7
4
  /** Shared host fonts, served once as a digest-pinned stylesheet. */
8
5
  export declare const fontStyles: string;
9
6