@fungi.computer/oyster 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +133 -78
- package/dist/agent-archive-projection.d.ts +8 -0
- package/dist/agent-archive-projection.js +49 -0
- package/dist/agent-schedules.d.ts +61 -0
- package/dist/agent-schedules.js +19 -0
- package/dist/agents.d.ts +35 -14
- package/dist/agents.js +93 -144
- package/dist/app-builds.d.ts +7 -2
- package/dist/app-builds.js +19 -2
- package/dist/approval-capability.d.ts +1 -1
- package/dist/approval-capability.js +11 -16
- package/dist/approval-home.d.ts +2 -1
- package/dist/approval-home.js +5 -3
- package/dist/apps/catalog.js +1 -1
- package/dist/apps/gate.d.ts +4 -7
- package/dist/apps/gate.js +14 -29
- package/dist/apps/index.d.ts +1 -1
- package/dist/apps/installation.d.ts +4 -42
- package/dist/apps/installation.js +7 -75
- package/dist/apps/operations.js +0 -10
- package/dist/apps/permissions.d.ts +18 -8
- package/dist/apps/permissions.js +7 -1
- package/dist/auth.d.ts +1 -24
- package/dist/auth.js +9 -39
- package/dist/authorization.d.ts +307 -0
- package/dist/authorization.js +542 -0
- package/dist/boot-host-script.js +1 -1
- package/dist/build/build-jobs.d.ts +2 -2
- package/dist/build/candidate-builder.d.ts +2 -0
- package/dist/build/candidate-builder.js +5 -1
- package/dist/build/candidate-cli.js +3 -0
- package/dist/build/candidate-cli.mjs +70 -22
- package/dist/build/job-log.d.ts +23 -0
- package/dist/build/job-log.js +88 -0
- package/dist/build/local-build.d.ts +9 -1
- package/dist/build/local-build.js +11 -9
- package/dist/cloudflare.d.ts +4 -1
- package/dist/cloudflare.js +1 -1
- package/dist/files.js +9 -8
- package/dist/git-admission.d.ts +3 -3
- package/dist/git-admission.js +4 -6
- package/dist/git-door.d.ts +8 -7
- package/dist/git-door.js +18 -17
- package/dist/git.d.ts +1 -1
- package/dist/host.d.ts +33 -40
- package/dist/host.js +156 -275
- package/dist/installation-execution.js +14 -5
- package/dist/installation-operations.d.ts +4 -180
- package/dist/installation-operations.js +1 -5
- package/dist/installation-store-generation.d.ts +4 -1
- package/dist/installation-store-generation.js +59 -6
- package/dist/internal/delivery.js +0 -1
- package/dist/internal/host-gate.d.ts +3 -2
- package/dist/internal/host-gate.js +5 -18
- package/dist/internal/invocation-timing.d.ts +16 -0
- package/dist/internal/invocation-timing.js +23 -0
- package/dist/internal/registry-installations.js +1 -0
- package/dist/invocation.d.ts +15 -14
- package/dist/invocation.js +58 -26
- package/dist/keys.d.ts +2 -19
- package/dist/keys.js +1 -37
- package/dist/mcp.d.ts +15 -27
- package/dist/mcp.js +105 -155
- package/dist/page/agent-browser.d.ts +15 -0
- package/dist/page/agent-browser.js +162 -0
- package/dist/page/app-permissions-port.d.ts +3 -0
- package/dist/page/app-permissions-port.js +43 -0
- package/dist/page/app-permissions.d.ts +11 -0
- package/dist/page/app-permissions.js +296 -0
- package/dist/page/isolated-page-theme.d.ts +2 -0
- package/dist/page/isolated-page-theme.js +11 -0
- package/dist/page/mount.d.ts +2 -13
- package/dist/page/mount.js +7 -93
- package/dist/page-script.d.ts +0 -3
- package/dist/page-script.js +3 -4
- package/dist/standalone/agent-archive-owner.d.ts +42 -0
- package/dist/standalone/agent-archive-owner.js +116 -0
- package/dist/standalone/agent-archive-schedules.d.ts +6 -0
- package/dist/standalone/agent-archive-schedules.js +76 -0
- package/dist/standalone/agent-archive.d.ts +31 -0
- package/dist/standalone/agent-archive.js +67 -0
- package/dist/standalone/agent-computers.d.ts +7 -6
- package/dist/standalone/agent-computers.js +4 -7
- package/dist/standalone/agent-mail.d.ts +2 -2
- package/dist/standalone/agents.d.ts +186 -20
- package/dist/standalone/agents.js +251 -30
- package/dist/standalone/app-builds-binding.js +1 -1
- package/dist/standalone/app-set.js +2 -54
- package/dist/standalone/app-source.d.ts +3 -4
- package/dist/standalone/app-source.js +12 -9
- package/dist/standalone/builds.d.ts +2 -14
- package/dist/standalone/builds.js +15 -13
- package/dist/standalone/computer-fleet.d.ts +4 -4
- package/dist/standalone/computer-fleet.js +13 -42
- package/dist/standalone/computer.d.ts +4 -3
- package/dist/standalone/computer.js +52 -53
- package/dist/standalone/dashboard-approval.d.ts +10 -0
- package/dist/standalone/dashboard-approval.js +66 -0
- package/dist/standalone/machine-link.d.ts +4 -221
- package/dist/standalone/machine-link.js +21 -15
- package/dist/standalone/media.d.ts +1 -0
- package/dist/standalone/media.js +20 -14
- package/dist/standalone/owner-routes.d.ts +16 -0
- package/dist/standalone/owner-routes.js +21 -0
- package/dist/standalone/permission-resources.d.ts +5 -0
- package/dist/standalone/permission-resources.js +31 -0
- package/dist/standalone/registry.d.ts +7 -0
- package/dist/standalone/registry.js +3 -0
- package/dist/standalone/tenancy.d.ts +0 -3
- package/dist/standalone/tenancy.js +2 -5
- package/dist/standalone/worker-routes.d.ts +19 -0
- package/dist/standalone/worker-routes.js +9 -0
- package/dist/standalone/worker.d.ts +1 -1
- package/dist/standalone/worker.js +27 -35
- package/dist/standalone/workflow-binding.d.ts +10 -3
- package/dist/standalone/workflow-binding.js +22 -5
- package/dist/standalone/workflow-git-capture.js +2 -1
- package/dist/standalone/workflow-host.d.ts +8 -103
- package/dist/standalone/workflow-host.js +1 -1
- package/dist/standalone/workflow-node-capture.js +2 -2
- package/dist/standalone/workflow-root-authority.d.ts +1 -1
- package/dist/standalone/workflow-root-authority.js +1 -1
- package/dist/standalone/workflow-schedules.d.ts +31 -4
- package/dist/standalone/workflow-schedules.js +18 -5
- package/dist/standalone/workflow-webhooks.d.ts +4 -18
- package/dist/standalone/workflow-webhooks.js +5 -5
- package/dist/standalone/workflows.d.ts +53 -27
- package/dist/standalone/workflows.js +9 -3
- package/dist/storage-frame.d.ts +6 -6
- package/dist/testing/capabilities.d.ts +7 -6
- package/dist/testing/capabilities.js +3 -6
- package/dist/testing/installation.d.ts +0 -1
- package/dist/testing/installation.js +2 -5
- package/dist/workers-git.d.ts +12 -4
- package/dist/workers-git.js +38 -35
- package/dist/workers.d.ts +9 -1
- package/dist/workers.js +44 -8
- package/dist/workflows.d.ts +4 -0
- package/dist/workflows.js +2 -0
- package/docs/api/README.md +7 -0
- package/docs/api/agent-schedules.md +176 -0
- package/docs/api/agents.md +34 -13
- package/docs/api/app-storage.md +1 -1
- package/docs/api/apps.md +323 -225
- package/docs/api/auth.md +0 -55
- package/docs/api/authorization.md +1300 -0
- package/docs/api/build.md +42 -38
- package/docs/api/cloudflare.md +3 -3
- package/docs/api/git.md +20 -11
- package/docs/api/host.md +64 -192
- package/docs/api/index.md +1 -1
- package/docs/api/invocation.md +14 -14
- package/docs/api/keys.md +1 -98
- package/docs/api/mcp.md +43 -40
- package/docs/api/modules.md +2 -0
- package/docs/api/page/mount.md +16 -19
- package/docs/api/workers-git.md +46 -12
- package/docs/api/workers.md +111 -222
- package/package.json +41 -22
- package/dist/page/computer-target.d.ts +0 -3
- package/dist/page/computer-target.js +0 -9
- package/dist/standalone/channel-session-door.d.ts +0 -4
- package/dist/standalone/channel-session-door.js +0 -72
|
@@ -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
|
+
}
|
|
@@ -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
|
+
}
|
package/dist/page/mount.d.ts
CHANGED
|
@@ -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;
|
package/dist/page/mount.js
CHANGED
|
@@ -6,8 +6,7 @@ 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 {
|
|
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
11
|
import { computerTargetUrl } from "../computer-target.js";
|
|
13
12
|
import * as Schema from "effect/Schema";
|
|
@@ -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,
|
|
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
|
|
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();
|
package/dist/page-script.d.ts
CHANGED
|
@@ -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
|
|