@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.
- package/README.md +155 -63
- package/dist/agent-archive-projection.d.ts +8 -0
- package/dist/agent-archive-projection.js +49 -0
- package/dist/agent-schedules.d.ts +50 -0
- package/dist/agent-schedules.js +17 -0
- package/dist/agents.d.ts +35 -14
- package/dist/agents.js +97 -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.js +9 -1
- 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 +67 -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 +2 -0
- package/dist/build/local-build.js +11 -9
- package/dist/channels/binding.d.ts +8 -1
- package/dist/channels/binding.js +12 -0
- package/dist/channels/conversations.d.ts +16 -5
- package/dist/channels/visitor-channel.d.ts +53 -0
- package/dist/channels/visitor-channel.js +124 -0
- package/dist/channels/visitor-edge.d.ts +115 -0
- package/dist/channels/visitor-edge.js +582 -0
- package/dist/channels/visitor-request.d.ts +7 -0
- package/dist/channels/visitor-request.js +17 -0
- package/dist/channels/visitor-stream-slots.d.ts +2 -0
- package/dist/channels/visitor-stream-slots.js +33 -0
- package/dist/channels/visitors.d.ts +47 -0
- package/dist/channels/visitors.js +181 -0
- package/dist/cloudflare.d.ts +11 -2
- package/dist/cloudflare.js +27 -1
- package/dist/{page/computer-target.d.ts → computer-target.d.ts} +2 -0
- package/dist/{page/computer-target.js → computer-target.js} +3 -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 +38 -42
- package/dist/host.js +182 -239
- package/dist/installation-execution.js +10 -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 +9 -6
- package/dist/internal/host-gate.js +19 -25
- 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 +19 -11
- package/dist/invocation.js +171 -96
- 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/capabilities.js +1 -1
- package/dist/page/child-selection.d.ts +27 -0
- package/dist/page/child-selection.js +38 -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 +8 -94
- package/dist/page-script.d.ts +0 -3
- package/dist/page-script.js +3 -4
- package/dist/release.d.ts +13 -0
- package/dist/release.js +10 -0
- package/dist/request-origin.d.ts +4 -0
- package/dist/request-origin.js +10 -0
- 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 +157 -20
- package/dist/standalone/agents.js +242 -38
- 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 +20 -43
- 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.js +3 -1
- 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 +8 -3
- package/dist/standalone/workflow-binding.js +14 -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 +14 -5
- package/dist/standalone/workflow-schedules.js +11 -5
- package/dist/standalone/workflow-webhooks.d.ts +4 -18
- package/dist/standalone/workflow-webhooks.js +5 -5
- package/dist/standalone/workflows.d.ts +36 -28
- package/dist/standalone/workflows.js +6 -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/turnstile.d.ts +27 -0
- package/dist/turnstile.js +51 -0
- package/dist/workers-git.d.ts +12 -4
- package/dist/workers-git.js +58 -36
- package/dist/workers.d.ts +9 -1
- package/dist/workers.js +59 -20
- package/dist/workflows.d.ts +2 -0
- package/dist/workflows.js +1 -0
- package/docs/api/README.md +7 -0
- package/docs/api/agent-schedules.md +175 -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/authorization.md +1300 -0
- package/docs/api/build-contract.md +2 -2
- package/docs/api/build.md +40 -37
- package/docs/api/cloudflare.md +27 -2
- package/docs/api/git.md +20 -11
- package/docs/api/host.md +80 -199
- package/docs/api/index.md +1 -1
- package/docs/api/invocation.md +15 -11
- 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 +125 -235
- package/docs/site/agent-quickstart.mdx +6 -2
- package/examples/standalone/wrangler.celld.jsonc +66 -0
- package/package.json +64 -21
- package/dist/standalone/channel-session-door.d.ts +0 -4
- package/dist/standalone/channel-session-door.js +0 -42
package/dist/agents.js
CHANGED
|
@@ -1,17 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
* Agents for Apps on a standalone Oyster: one adapter shape and the routes
|
|
3
|
-
* the App page calls. Apps reach agents through Agar's `agent.list`,
|
|
4
|
-
* `agent.manage`, `agent.read` and `agent.send` contracts, so an App written
|
|
5
|
-
* for the Hub (Grove) runs unchanged. Each agent's own transport is
|
|
6
|
-
* Shiitake's HTTP routes (`/sessions…`, `/acp`) and Knapsack's Session browser
|
|
7
|
-
* transport (`/browser/…`), served by the adapter behind the same App grants.
|
|
8
|
-
*/
|
|
9
|
-
import { Hono } from "hono";
|
|
10
|
-
import { keyAgentAccess } from "./keys.js";
|
|
1
|
+
import { agent, agents as agentCollection, owner, routes, } from "./authorization.js";
|
|
11
2
|
import * as Schema from "effect/Schema";
|
|
12
3
|
import { z } from "zod";
|
|
13
|
-
import {
|
|
4
|
+
import {} from "./host.js";
|
|
14
5
|
import { agentAvatarSchema } from "./agent-avatars.js";
|
|
6
|
+
import { computerTargetParam } from "./computer-target.js";
|
|
15
7
|
import { agentAvatar, agentList, agentManage, agentRename, agentSessionRead, agentSessionWrite, agentToolPolicy, } from "@fungi.computer/agar/agents";
|
|
16
8
|
/** At most this many MCP servers per Agent. */
|
|
17
9
|
export const maxAgentMcpServers = 16;
|
|
@@ -45,6 +37,13 @@ const visitSchema = Schema.Struct({
|
|
|
45
37
|
sessionId: Schema.String,
|
|
46
38
|
});
|
|
47
39
|
const agentIdPattern = /^[a-z0-9_-]{1,64}$/u;
|
|
40
|
+
/** Explicit owner-addressed settings remain reachable after list hides an archive. */
|
|
41
|
+
async function knownAgent(agents, agentId) {
|
|
42
|
+
if (!agentIdPattern.test(agentId))
|
|
43
|
+
return false;
|
|
44
|
+
return ((await agents.list()).some((agent) => agent.agentId === agentId) ||
|
|
45
|
+
(await agents.list({ archived: true })).some((agent) => agent.agentId === agentId));
|
|
46
|
+
}
|
|
48
47
|
/** How often an open inbox stream re-reads the revision. */
|
|
49
48
|
const inboxPollMs = 2_000;
|
|
50
49
|
const maxAgentRequestBytes = 512 * 1024;
|
|
@@ -52,9 +51,8 @@ function refusal(status, error) {
|
|
|
52
51
|
return Response.json({ error }, { status });
|
|
53
52
|
}
|
|
54
53
|
/** Agent adapters admitted against each App's declared capabilities. */
|
|
55
|
-
export function agentCapabilities(agents
|
|
56
|
-
const
|
|
57
|
-
const shared = { base: "/", routes };
|
|
54
|
+
export function agentCapabilities(agents) {
|
|
55
|
+
const shared = { base: "/", routes: createAgentRoutes(agents) };
|
|
58
56
|
return [
|
|
59
57
|
{
|
|
60
58
|
...shared,
|
|
@@ -135,45 +133,22 @@ export function agentCapabilities(agents, liveKey) {
|
|
|
135
133
|
},
|
|
136
134
|
];
|
|
137
135
|
}
|
|
138
|
-
function createAgentRoutes(agents
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
if (
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
return
|
|
146
|
-
const
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
});
|
|
155
|
-
routes.use("/agent-policies/:agentId", async (context, next) => {
|
|
156
|
-
if (!(await access(context.env.principal, context.req.param("agentId"))))
|
|
157
|
-
return refusal(403, "Agent access denied");
|
|
158
|
-
await next();
|
|
159
|
-
return undefined;
|
|
160
|
-
});
|
|
161
|
-
routes.get("/agents", async (context) => {
|
|
162
|
-
const listed = await agents.list();
|
|
163
|
-
const actor = context.env.principal.actor;
|
|
164
|
-
if (oysterPrincipalIsOwner(context.env.principal))
|
|
165
|
-
return Response.json(listed);
|
|
166
|
-
if (actor.kind !== "team-key")
|
|
167
|
-
return Response.json([]);
|
|
168
|
-
const key = await liveKey(actor.keyId);
|
|
169
|
-
return Response.json(key === null
|
|
170
|
-
? []
|
|
171
|
-
: listed.filter((agent) => keyAgentAccess(key, agent.agentId)));
|
|
172
|
-
});
|
|
173
|
-
routes.post("/agents", async (context) => {
|
|
174
|
-
const { principal } = context.env;
|
|
175
|
-
if (!oysterPrincipalIsOwner(principal))
|
|
176
|
-
return refusal(403, "Only the owner creates agents");
|
|
136
|
+
function createAgentRoutes(agents) {
|
|
137
|
+
return (routes()
|
|
138
|
+
.get("/agents", agentCollection(), async (context, { reaches }) => {
|
|
139
|
+
const selected = context.req.query("archived");
|
|
140
|
+
if (selected !== undefined &&
|
|
141
|
+
selected !== "true" &&
|
|
142
|
+
selected !== "false")
|
|
143
|
+
return refusal(400, "Choose a valid archive view");
|
|
144
|
+
const listed = await agents.list({ archived: selected === "true" });
|
|
145
|
+
const reached = [];
|
|
146
|
+
for (const item of listed)
|
|
147
|
+
if (await reaches(item.agentId))
|
|
148
|
+
reached.push(item);
|
|
149
|
+
return Response.json(reached);
|
|
150
|
+
})
|
|
151
|
+
.post("/agents", owner("direct-or-app"), async (context, { principal }) => {
|
|
177
152
|
const body = Schema.decodeUnknownOption(createSchema)(await context.req.json());
|
|
178
153
|
if (body._tag === "None")
|
|
179
154
|
return refusal(400, "Name the agent");
|
|
@@ -185,11 +160,8 @@ function createAgentRoutes(agents, liveKey) {
|
|
|
185
160
|
// The owner's own authenticated request creates this Agent; its App tools
|
|
186
161
|
// are approved the same way direct owner approval is, with no second path.
|
|
187
162
|
return Response.json(await agents.create(body.value.name, body.value.requestId, principal, "owner-direct", avatar?.data));
|
|
188
|
-
})
|
|
189
|
-
|
|
190
|
-
const { principal } = context.env;
|
|
191
|
-
if (!oysterPrincipalIsOwner(principal))
|
|
192
|
-
return refusal(403, "Only the owner renames agents");
|
|
163
|
+
})
|
|
164
|
+
.put("/agent-names/:agentId", owner("direct-or-app"), async (context, { principal }) => {
|
|
193
165
|
const agentId = context.req.param("agentId");
|
|
194
166
|
if (!agentIdPattern.test(agentId))
|
|
195
167
|
return refusal(404, "No such agent");
|
|
@@ -204,11 +176,8 @@ function createAgentRoutes(agents, liveKey) {
|
|
|
204
176
|
? 409
|
|
205
177
|
: 403, result.error);
|
|
206
178
|
return Response.json(result);
|
|
207
|
-
})
|
|
208
|
-
|
|
209
|
-
const { principal } = context.env;
|
|
210
|
-
if (!oysterPrincipalIsOwner(principal))
|
|
211
|
-
return refusal(403, "Only the owner changes avatars");
|
|
179
|
+
})
|
|
180
|
+
.put("/agent-avatars/:agentId", owner("direct-or-app"), async (context, { principal }) => {
|
|
212
181
|
const agentId = context.req.param("agentId");
|
|
213
182
|
if (!agentIdPattern.test(agentId))
|
|
214
183
|
return refusal(404, "No such agent");
|
|
@@ -219,19 +188,19 @@ function createAgentRoutes(agents, liveKey) {
|
|
|
219
188
|
if ("error" in result)
|
|
220
189
|
return refusal(result.error === "not_found" ? 404 : 403, result.error);
|
|
221
190
|
return Response.json(result);
|
|
222
|
-
})
|
|
223
|
-
|
|
224
|
-
const agentId = context.req.param("agentId");
|
|
191
|
+
})
|
|
192
|
+
.get("/agent-policies/:agentId", agent(), async (_context, { agentId }) => {
|
|
225
193
|
if (!agentIdPattern.test(agentId))
|
|
226
194
|
return refusal(404, "No such agent");
|
|
227
195
|
const status = await agents.policyStatus(agentId);
|
|
228
196
|
return status === null
|
|
229
197
|
? refusal(404, "No such agent")
|
|
230
198
|
: Response.json(status);
|
|
231
|
-
})
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
199
|
+
})
|
|
200
|
+
.put("/agent-policies/:agentId", owner("direct-or-app"), async (context, { principal }) => approvePolicy(agents, principal, "owner-direct", context.req.param("agentId"), await context.req.text()))
|
|
201
|
+
.post("/agents/:agentId/archive", owner("direct-or-app"), (context, { principal }) => changeArchive(agents, context.req.param("agentId"), true, principal))
|
|
202
|
+
.post("/agents/:agentId/unarchive", owner("direct-or-app"), (context, { principal }) => changeArchive(agents, context.req.param("agentId"), false, principal))
|
|
203
|
+
.all("/agents/:agentId/*", agent(), async (context, { agentId }) => {
|
|
235
204
|
if (!agentIdPattern.test(agentId))
|
|
236
205
|
return refusal(404, "No such agent");
|
|
237
206
|
const incoming = context.req.raw;
|
|
@@ -246,51 +215,45 @@ function createAgentRoutes(agents, liveKey) {
|
|
|
246
215
|
if (length > maxAgentRequestBytes)
|
|
247
216
|
return refusal(413, "Request too large");
|
|
248
217
|
const target = new URL(`https://agent.internal${path}${url.search}`);
|
|
218
|
+
// A desk window's Computer routing hint is the host's to admit; the
|
|
219
|
+
// Agent's Session routes decode a closed query that refuses it.
|
|
220
|
+
target.searchParams.delete(computerTargetParam);
|
|
249
221
|
const response = await agents.fetch(agentId, new Request(target, incoming));
|
|
250
222
|
return response ?? refusal(404, "No such agent");
|
|
251
|
-
})
|
|
252
|
-
|
|
223
|
+
})
|
|
224
|
+
.get("/member/inbox", agentCollection(), async (_context, { authority, reaches }) => {
|
|
253
225
|
const inbox = await agents.inbox();
|
|
254
|
-
|
|
255
|
-
if (oysterPrincipalIsOwner(principal))
|
|
226
|
+
if (authority.kind === "owner")
|
|
256
227
|
return Response.json({
|
|
257
228
|
items: inbox.items,
|
|
258
229
|
next: null,
|
|
259
230
|
cursor: inbox.revision,
|
|
260
231
|
});
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
232
|
+
const items = [];
|
|
233
|
+
for (const item of inbox.items)
|
|
234
|
+
if ("agentId" in item && (await reaches(item.agentId)))
|
|
235
|
+
items.push(item);
|
|
264
236
|
return Response.json({
|
|
265
|
-
items
|
|
266
|
-
? []
|
|
267
|
-
: inbox.items.filter((item) => "agentId" in item && keyAgentAccess(key, item.agentId)),
|
|
237
|
+
items,
|
|
268
238
|
next: null,
|
|
269
239
|
// The global revision changes for ungranted Agents too. Key feeds have
|
|
270
240
|
// no scoped event stream, so expose neither that revision nor its events.
|
|
271
241
|
cursor: 0,
|
|
272
242
|
});
|
|
273
|
-
})
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
: refusal(403, "Agent access denied"));
|
|
277
|
-
routes.post("/member/inbox/visits", async (context) => {
|
|
243
|
+
})
|
|
244
|
+
.get("/member/inbox/notices", owner("direct-or-app"), () => Response.json({ items: [], next: null }))
|
|
245
|
+
.post("/member/inbox/visits", agent(async (facts) => (await facts.body(visitSchema))?.agentId ?? null), async (context, { agentId }) => {
|
|
278
246
|
const body = Schema.decodeUnknownOption(visitSchema)(await context.req.json());
|
|
279
247
|
if (body._tag === "None")
|
|
280
248
|
return refusal(400, "Invalid visit");
|
|
281
|
-
|
|
282
|
-
return refusal(403, "Agent access denied");
|
|
283
|
-
return (await agents.visit(body.value.agentId, body.value.sessionId))
|
|
249
|
+
return (await agents.visit(agentId, body.value.sessionId))
|
|
284
250
|
? new Response(null, { status: 204 })
|
|
285
251
|
: refusal(404, "No such Session");
|
|
286
|
-
})
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
// until a principal-scoped revision exists.
|
|
292
|
-
if (!oysterPrincipalIsOwner(context.env.principal))
|
|
293
|
-
return refusal(403, "Agent access denied");
|
|
252
|
+
})
|
|
253
|
+
// The feed is small (one person's agents), so a change is announced as a
|
|
254
|
+
// `reset` and the client re-reads the whole inbox. The unscoped revision
|
|
255
|
+
// also changes for other Agents, so only the owner reads it.
|
|
256
|
+
.get("/member/inbox/events", owner("direct-or-app"), (context) => {
|
|
294
257
|
const after = Number(context.req.query("after") ?? "0");
|
|
295
258
|
const signal = context.req.raw.signal;
|
|
296
259
|
const encoder = new TextEncoder();
|
|
@@ -317,20 +280,33 @@ function createAgentRoutes(agents, liveKey) {
|
|
|
317
280
|
"Cache-Control": "no-store",
|
|
318
281
|
},
|
|
319
282
|
});
|
|
320
|
-
});
|
|
321
|
-
|
|
283
|
+
}));
|
|
284
|
+
}
|
|
285
|
+
/** Owner-only archive transition; creator Agents use their own control tool. */
|
|
286
|
+
async function changeArchive(agents, agentId, archived, principal) {
|
|
287
|
+
if (!agentIdPattern.test(agentId))
|
|
288
|
+
return refusal(404, "No such agent");
|
|
289
|
+
const result = await agents.setArchived(agentId, archived, principal);
|
|
290
|
+
if ("error" in result)
|
|
291
|
+
return Response.json(result, {
|
|
292
|
+
status: result.error === "not_found"
|
|
293
|
+
? 404
|
|
294
|
+
: result.error === "not_granted"
|
|
295
|
+
? 403
|
|
296
|
+
: result.archived !== undefined
|
|
297
|
+
? 503
|
|
298
|
+
: 409,
|
|
299
|
+
});
|
|
300
|
+
return Response.json(result);
|
|
322
301
|
}
|
|
323
302
|
async function approvePolicy(agents, principal, actor, agentId, body) {
|
|
324
|
-
if (!oysterPrincipalIsOwner(principal))
|
|
325
|
-
return refusal(403, "Only the owner approves Agent tools");
|
|
326
303
|
const input = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Struct({
|
|
327
304
|
level: Schema.Literals(["read", "write"]),
|
|
328
305
|
expectedRevision: Schema.NullOr(Schema.NonEmptyString),
|
|
329
306
|
})), { onExcessProperty: "error" })(body);
|
|
330
307
|
if (input._tag === "None")
|
|
331
308
|
return refusal(400, "Choose read or write tools");
|
|
332
|
-
if (!
|
|
333
|
-
!(await agents.list()).some((agent) => agent.agentId === agentId))
|
|
309
|
+
if (!(await knownAgent(agents, agentId)))
|
|
334
310
|
return refusal(404, "No such agent");
|
|
335
311
|
const result = await agents.approveAppToolPolicy(agentId, principal, input.value.level, input.value.expectedRevision, actor);
|
|
336
312
|
const failureStatuses = {
|
|
@@ -344,11 +320,8 @@ async function approvePolicy(agents, principal, actor, agentId, body) {
|
|
|
344
320
|
}
|
|
345
321
|
/** Host-owned direct approval under the admitted owner; never an App capability. */
|
|
346
322
|
export function agentApprovalRoutes(agents) {
|
|
347
|
-
return
|
|
348
|
-
.get("/api/agent-models/:agentId", async (context) => {
|
|
349
|
-
if (context.req.raw.headers.has("X-Oyster-Installation") ||
|
|
350
|
-
!oysterPrincipalIsOwner(context.get("principal")))
|
|
351
|
-
return refusal(403, "Owner only");
|
|
323
|
+
return routes()
|
|
324
|
+
.get("/api/agent-models/:agentId", owner("direct"), async (context) => {
|
|
352
325
|
const agentId = context.req.param("agentId");
|
|
353
326
|
if (!agentIdPattern.test(agentId))
|
|
354
327
|
return refusal(404, "No such agent");
|
|
@@ -357,17 +330,14 @@ export function agentApprovalRoutes(agents) {
|
|
|
357
330
|
? refusal(404, "No such agent")
|
|
358
331
|
: Response.json(choice);
|
|
359
332
|
})
|
|
360
|
-
.put("/api/agent-models/:agentId", async (context) => {
|
|
361
|
-
if (context.req.raw.headers.has("X-Oyster-Installation") ||
|
|
362
|
-
!oysterPrincipalIsOwner(context.get("principal")))
|
|
363
|
-
return refusal(403, "Owner only");
|
|
333
|
+
.put("/api/agent-models/:agentId", owner("direct"), async (context, { principal }) => {
|
|
364
334
|
const agentId = context.req.param("agentId");
|
|
365
335
|
if (!agentIdPattern.test(agentId))
|
|
366
336
|
return refusal(404, "No such agent");
|
|
367
337
|
const input = agents.decodeModelChoice(await context.req.text());
|
|
368
338
|
if (input === null)
|
|
369
339
|
return refusal(400, "Invalid model choice");
|
|
370
|
-
const choice = await agents.setModelChoice(agentId, input,
|
|
340
|
+
const choice = await agents.setModelChoice(agentId, input, principal);
|
|
371
341
|
if ("error" in choice && choice.error === "invalid_model_choice")
|
|
372
342
|
return refusal(400, "Invalid model choice");
|
|
373
343
|
return "error" in choice
|
|
@@ -378,21 +348,15 @@ export function agentApprovalRoutes(agents) {
|
|
|
378
348
|
: 409, choice.error)
|
|
379
349
|
: Response.json(choice);
|
|
380
350
|
})
|
|
381
|
-
.get("/api/agent-policies/:agentId", async (context) => {
|
|
382
|
-
if (context.req.raw.headers.has("X-Oyster-Installation"))
|
|
383
|
-
return refusal(403, "Owner direct only");
|
|
384
|
-
if (!oysterPrincipalIsOwner(context.get("principal")))
|
|
385
|
-
return refusal(403, "Owner only");
|
|
351
|
+
.get("/api/agent-policies/:agentId", owner("direct"), async (context) => {
|
|
386
352
|
const agentId = context.req.param("agentId");
|
|
387
|
-
if (!(await agents
|
|
353
|
+
if (!(await knownAgent(agents, agentId)))
|
|
388
354
|
return refusal(404, "No such agent");
|
|
389
355
|
return Response.json({
|
|
390
356
|
expectedRevision: await agents.policyRevision(agentId),
|
|
391
357
|
});
|
|
392
358
|
})
|
|
393
|
-
.put("/api/agent-policies/:agentId", async (context) => context.req.
|
|
394
|
-
? refusal(403, "Missing admitted Agent installation")
|
|
395
|
-
: approvePolicy(agents, context.get("principal"), "owner-direct", context.req.param("agentId"), await context.req.text()));
|
|
359
|
+
.put("/api/agent-policies/:agentId", owner("direct"), async (context, { principal }) => approvePolicy(agents, principal, "owner-direct", context.req.param("agentId"), await context.req.text()));
|
|
396
360
|
}
|
|
397
361
|
const serverIdPattern = /^[a-z0-9_-]{1,64}$/u;
|
|
398
362
|
const mcpRefusalStatus = { not_found: 404, duplicate_name: 409, limit: 409 };
|
|
@@ -401,28 +365,18 @@ const mcpRefusalStatus = { not_found: 404, duplicate_name: 409, limit: 409 };
|
|
|
401
365
|
* capability. Bearer tokens go in and never come back out.
|
|
402
366
|
*/
|
|
403
367
|
export function agentMcpServerRoutes(agents) {
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
return refusal(403, "Owner direct only");
|
|
408
|
-
if (!oysterPrincipalIsOwner(context.get("principal")))
|
|
409
|
-
return refusal(403, "Only the owner connects MCP servers");
|
|
410
|
-
await next();
|
|
411
|
-
return undefined;
|
|
412
|
-
};
|
|
413
|
-
routes.use("/api/agent-mcp-servers", ownerOnly);
|
|
414
|
-
routes.use("/api/agent-mcp-servers/*", ownerOnly);
|
|
415
|
-
// One owner read for the settings panel: every Agent with its servers.
|
|
416
|
-
routes.get("/api/agent-mcp-servers", async () => {
|
|
368
|
+
return (routes()
|
|
369
|
+
// One owner read for the settings panel: every Agent with its servers.
|
|
370
|
+
.get("/api/agent-mcp-servers", owner("direct"), async () => {
|
|
417
371
|
const listed = await agents.list();
|
|
418
|
-
const rows = await Promise.all(listed.map(async (
|
|
419
|
-
agentId:
|
|
420
|
-
name:
|
|
421
|
-
servers: (await agents.mcpServers(
|
|
372
|
+
const rows = await Promise.all(listed.map(async (item) => ({
|
|
373
|
+
agentId: item.agentId,
|
|
374
|
+
name: item.name,
|
|
375
|
+
servers: (await agents.mcpServers(item.agentId)) ?? [],
|
|
422
376
|
})));
|
|
423
377
|
return Response.json({ agents: rows });
|
|
424
|
-
})
|
|
425
|
-
|
|
378
|
+
})
|
|
379
|
+
.post("/api/agent-mcp-servers/:agentId", owner("direct"), async (context) => {
|
|
426
380
|
if (!agentIdPattern.test(context.req.param("agentId")))
|
|
427
381
|
return refusal(404, "No such agent");
|
|
428
382
|
const length = Number(context.req.header("content-length") ?? "0");
|
|
@@ -435,8 +389,8 @@ export function agentMcpServerRoutes(agents) {
|
|
|
435
389
|
return "error" in added
|
|
436
390
|
? refusal(mcpRefusalStatus[added.error], added.error)
|
|
437
391
|
: Response.json(added, { status: 201 });
|
|
438
|
-
})
|
|
439
|
-
|
|
392
|
+
})
|
|
393
|
+
.delete("/api/agent-mcp-servers/:agentId/:serverId", owner("direct"), async (context) => {
|
|
440
394
|
const serverId = context.req.param("serverId");
|
|
441
395
|
if (!agentIdPattern.test(context.req.param("agentId")) ||
|
|
442
396
|
!serverIdPattern.test(serverId))
|
|
@@ -444,6 +398,5 @@ export function agentMcpServerRoutes(agents) {
|
|
|
444
398
|
return (await agents.removeMcpServer(context.req.param("agentId"), serverId))
|
|
445
399
|
? new Response(null, { status: 204 })
|
|
446
400
|
: refusal(404, "No such server");
|
|
447
|
-
});
|
|
448
|
-
return routes;
|
|
401
|
+
}));
|
|
449
402
|
}
|
package/dist/app-builds.d.ts
CHANGED
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import * as Schema from "effect/Schema";
|
|
20
20
|
import type { WatchdogSqliteOwner } from "@fungi.computer/watchdog";
|
|
21
|
+
import { type BuildJobLog } from "./build/job-log.js";
|
|
21
22
|
import { type Release } from "@fungi.computer/puffball";
|
|
22
23
|
import { type AppPublisherCoreConfigInput } from "./build/effect.js";
|
|
23
24
|
import { type TeamAppCandidateManifest } from "./build/submission-contract.js";
|
|
@@ -144,10 +145,12 @@ export type OysterBuildComputer = Readonly<{
|
|
|
144
145
|
command: string;
|
|
145
146
|
timeoutMs: number;
|
|
146
147
|
}): Promise<Readonly<{
|
|
147
|
-
output
|
|
148
|
+
output: Readonly<{
|
|
148
149
|
exitCode: number | null;
|
|
149
150
|
stderr?: string;
|
|
150
151
|
}> | null;
|
|
152
|
+
/** Raw combined events captured before the Computer reply's stream bounds. */
|
|
153
|
+
log: BuildJobLog;
|
|
151
154
|
}>>;
|
|
152
155
|
}>;
|
|
153
156
|
export type OysterAppBuildOptions = Readonly<{
|
|
@@ -208,6 +211,8 @@ export declare function createOysterAppBuilds(options: OysterAppBuildOptions): R
|
|
|
208
211
|
run: () => Promise<OysterBuildView>;
|
|
209
212
|
}>>;
|
|
210
213
|
read(jobId: string): OysterBuildView | null;
|
|
214
|
+
/** Full raw output; the host applies the same principal guard as `read`. */
|
|
215
|
+
readLog(jobId: string): Promise<BuildJobLog | null>;
|
|
211
216
|
/** Discover the current source's latest job from the caller's Git owner,
|
|
212
217
|
* never a job id remembered in a browser or another installation's repo. */
|
|
213
218
|
latestRepositoryBuild(installationId: string, repository: string): OysterBuildView | null;
|
|
@@ -280,4 +285,4 @@ export declare function createOysterAppBuilds(options: OysterAppBuildOptions): R
|
|
|
280
285
|
submitCandidate(jobId: string, manifest: TeamAppCandidateManifest, grant: string, credentialId: string): Promise<Response>;
|
|
281
286
|
}>;
|
|
282
287
|
export type OysterAppBuilds = ReturnType<typeof createOysterAppBuilds>;
|
|
283
|
-
export {};
|
|
288
|
+
export { buildJobLogMaxBytes, type BuildJobLog } from "./build/job-log.js";
|
package/dist/app-builds.js
CHANGED
|
@@ -20,6 +20,7 @@ import * as Schema from "effect/Schema";
|
|
|
20
20
|
import * as Data from "effect/Data";
|
|
21
21
|
import * as Option from "effect/Option";
|
|
22
22
|
import { candidateFailureDiagnostic } from "./build/candidate-diagnostic.js";
|
|
23
|
+
import { buildJobDiagnostic, buildJobLogSchema, } from "./build/job-log.js";
|
|
23
24
|
import { parseArtifactRef, parseReleaseId, } from "@fungi.computer/puffball";
|
|
24
25
|
import { appIdSchema, releaseSchema, sha256DigestSchema, sourceRevisionSchema, } from "@fungi.computer/puffball/effect";
|
|
25
26
|
import { capturedPinnedRecipeSchema, normalizeAppPublisherRecipe, digestPinnedRecipe, fitsPinnedRecipe, pinnedRecipeObjectBound, } from "./build/effect.js";
|
|
@@ -235,8 +236,9 @@ export function createOysterAppBuilds(options) {
|
|
|
235
236
|
detail: job.detail,
|
|
236
237
|
});
|
|
237
238
|
const settle = (jobId, status, detail) => {
|
|
239
|
+
const diagnostic = detail === null ? null : buildJobDiagnostic(jobId, detail);
|
|
238
240
|
owner.sql.exec(`UPDATE oyster_build_jobs SET installation=CASE WHEN ?='succeeded' THEN installation ELSE ${failedInstallation} END,
|
|
239
|
-
status=?, detail=?, expires_at=MIN(expires_at, ?) WHERE job_id=? AND status='running'`, status,
|
|
241
|
+
status=?, detail=?, expires_at=MIN(expires_at, ?) WHERE job_id=? AND status='running'`, status, diagnostic, status, diagnostic, now(), jobId);
|
|
240
242
|
};
|
|
241
243
|
const required = (jobId) => {
|
|
242
244
|
const job = readJob(jobId);
|
|
@@ -244,6 +246,10 @@ export function createOysterAppBuilds(options) {
|
|
|
244
246
|
throw new Error("Unknown build job");
|
|
245
247
|
return job;
|
|
246
248
|
};
|
|
249
|
+
// A job-scoped immutable object in the same artifact store as its build.
|
|
250
|
+
// It is read only through the owner build door, never exposed as a Release.
|
|
251
|
+
const logRef = (jobId) => parseArtifactRef(`artifact:build-log-${jobId}`);
|
|
252
|
+
const logCodec = Schema.fromJsonString(buildJobLogSchema);
|
|
247
253
|
const publisher = createTeamBuildPublisher({
|
|
248
254
|
teamId: options.tenantId,
|
|
249
255
|
now,
|
|
@@ -284,7 +290,8 @@ export function createOysterAppBuilds(options) {
|
|
|
284
290
|
command: input.command,
|
|
285
291
|
timeoutMs: input.timeoutMs,
|
|
286
292
|
});
|
|
287
|
-
|
|
293
|
+
await options.artifacts.put(logRef(input.jobId), new TextEncoder().encode(Schema.encodeUnknownSync(logCodec)(result.log)));
|
|
294
|
+
if (result.output === null)
|
|
288
295
|
throw new BuildOutcomeUnknown();
|
|
289
296
|
return result;
|
|
290
297
|
},
|
|
@@ -458,6 +465,15 @@ export function createOysterAppBuilds(options) {
|
|
|
458
465
|
const job = readJob(jobId);
|
|
459
466
|
return job === null ? null : view(job);
|
|
460
467
|
},
|
|
468
|
+
/** Full raw output; the host applies the same principal guard as `read`. */
|
|
469
|
+
async readLog(jobId) {
|
|
470
|
+
if (readJob(jobId) === null)
|
|
471
|
+
return null;
|
|
472
|
+
const bytes = await options.artifacts.get(logRef(jobId));
|
|
473
|
+
return bytes === null
|
|
474
|
+
? null
|
|
475
|
+
: Schema.decodeUnknownSync(logCodec)(new TextDecoder().decode(bytes));
|
|
476
|
+
},
|
|
461
477
|
/** Discover the current source's latest job from the caller's Git owner,
|
|
462
478
|
* never a job id remembered in a browser or another installation's repo. */
|
|
463
479
|
latestRepositoryBuild(installationId, repository) {
|
|
@@ -660,3 +676,4 @@ export function createOysterAppBuilds(options) {
|
|
|
660
676
|
},
|
|
661
677
|
});
|
|
662
678
|
}
|
|
679
|
+
export { buildJobLogMaxBytes } from "./build/job-log.js";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Approvals } from "@fungi.computer/approvals";
|
|
2
2
|
import type { HostActor } from "@fungi.computer/puffball/app";
|
|
3
3
|
import type { OysterCapability } from "./host.js";
|
|
4
|
-
/** Ordinary capability routes; the outer App gate supplies
|
|
4
|
+
/** Ordinary capability routes; the outer App gate supplies consent, and only members answer. */
|
|
5
5
|
export declare function approvalCapability(control: (actor: HostActor) => Approvals): OysterCapability;
|
|
@@ -1,19 +1,19 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { member, routes as routesFor } from "./authorization.js";
|
|
2
2
|
import { bodyLimit } from "hono/body-limit";
|
|
3
3
|
import { causeClass } from "./cause-class.js";
|
|
4
4
|
import * as Schema from "effect/Schema";
|
|
5
5
|
import { ApprovalListInputSchema, ApprovalGetInputSchema, ApprovalAnswerInputSchema, AutoApproveInputSchema, } from "@fungi.computer/approvals";
|
|
6
|
-
/** Ordinary capability routes; the outer App gate supplies
|
|
6
|
+
/** Ordinary capability routes; the outer App gate supplies consent, and only members answer. */
|
|
7
7
|
export function approvalCapability(control) {
|
|
8
|
-
const
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
await
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
8
|
+
const strict = { onExcessProperty: "error" };
|
|
9
|
+
const eligible = member(() => Response.json({ kind: "refused", reason: "not-eligible" }, { status: 403 }));
|
|
10
|
+
const options = { middleware: [bodyLimit({ maxSize: 4096 })] };
|
|
11
|
+
const routes = routesFor()
|
|
12
|
+
.post("/list", eligible, async (c, { principal }) => c.json(await control(principal.actor).list(Schema.decodeUnknownSync(ApprovalListInputSchema, strict)(await c.req.json()))), options)
|
|
13
|
+
.post("/get", eligible, async (c, { principal }) => c.json(await control(principal.actor).get(Schema.decodeUnknownSync(ApprovalGetInputSchema, strict)(await c.req.json()))), options)
|
|
14
|
+
.post("/answer", eligible, async (c, { principal }) => c.json(await control(principal.actor).answer(Schema.decodeUnknownSync(ApprovalAnswerInputSchema, strict)(await c.req.json()))), options)
|
|
15
|
+
.post("/set-auto-approve", eligible, async (c, { principal }) => c.json(await control(principal.actor).setAutoApprove(Schema.decodeUnknownSync(AutoApproveInputSchema, strict)(await c.req.json()))), options)
|
|
16
|
+
.onError((cause, context) => {
|
|
17
17
|
if (cause instanceof SyntaxError || cause instanceof Schema.SchemaError)
|
|
18
18
|
return context.json({ error: "Invalid approval request" }, 400);
|
|
19
19
|
console.warn("Native approval control failed", {
|
|
@@ -21,11 +21,6 @@ export function approvalCapability(control) {
|
|
|
21
21
|
});
|
|
22
22
|
return context.json({ error: "Approval home unavailable" }, 503);
|
|
23
23
|
});
|
|
24
|
-
const strict = { onExcessProperty: "error" };
|
|
25
|
-
routes.post("/list", async (c) => c.json(await control(c.env.principal.actor).list(Schema.decodeUnknownSync(ApprovalListInputSchema, strict)(await c.req.json()))));
|
|
26
|
-
routes.post("/get", async (c) => c.json(await control(c.env.principal.actor).get(Schema.decodeUnknownSync(ApprovalGetInputSchema, strict)(await c.req.json()))));
|
|
27
|
-
routes.post("/answer", async (c) => c.json(await control(c.env.principal.actor).answer(Schema.decodeUnknownSync(ApprovalAnswerInputSchema, strict)(await c.req.json()))));
|
|
28
|
-
routes.post("/set-auto-approve", async (c) => c.json(await control(c.env.principal.actor).setAutoApprove(Schema.decodeUnknownSync(AutoApproveInputSchema, strict)(await c.req.json()))));
|
|
29
24
|
return {
|
|
30
25
|
id: "approvals",
|
|
31
26
|
version: 1,
|
package/dist/approval-home.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { ApprovalLedger, type Approvals, type PendingApproval, type ApprovalApplyResult } from "@fungi.computer/approvals";
|
|
3
3
|
import type { WatchdogSqliteOwner } from "@fungi.computer/watchdog";
|
|
4
|
-
import {
|
|
4
|
+
import type { OysterHost } from "./host.js";
|
|
5
5
|
import type { HostActor } from "@fungi.computer/puffball/app";
|
|
6
6
|
import type { TenantScope } from "./tenant.js";
|
|
7
7
|
declare const callSchema: z.ZodObject<{
|
|
@@ -32,6 +32,7 @@ export type OysterApprovalCall = z.output<typeof callSchema>;
|
|
|
32
32
|
export declare class OysterApprovalHome {
|
|
33
33
|
private readonly host;
|
|
34
34
|
readonly ledger: ApprovalLedger;
|
|
35
|
+
private surfaces;
|
|
35
36
|
constructor(host: Readonly<{
|
|
36
37
|
owner: WatchdogSqliteOwner;
|
|
37
38
|
scope: TenantScope;
|
package/dist/approval-home.js
CHANGED
|
@@ -3,9 +3,8 @@ import * as Schema from "effect/Schema";
|
|
|
3
3
|
import { hostActorSchema } from "@fungi.computer/puffball/app-method-runtime";
|
|
4
4
|
import { ApprovalLedger, } from "@fungi.computer/approvals";
|
|
5
5
|
import { invokeModuleOperation } from "@fungi.computer/mycelium/remote";
|
|
6
|
-
import { oysterPrincipalIsOwner } from "./
|
|
6
|
+
import { keyAppAccess, oysterPrincipalIsOwner } from "./authorization.js";
|
|
7
7
|
import { sha256 } from "./release.js";
|
|
8
|
-
import { keyAppAccess } from "./keys.js";
|
|
9
8
|
const identity = z.string().min(1).max(256);
|
|
10
9
|
const targetSchema = z.strictObject({
|
|
11
10
|
actor: hostActorSchema,
|
|
@@ -25,6 +24,9 @@ const intentRow = Schema.decodeUnknownSync(Schema.Struct({ target_json: Schema.S
|
|
|
25
24
|
export class OysterApprovalHome {
|
|
26
25
|
host;
|
|
27
26
|
ledger;
|
|
27
|
+
// The home's artifact store and capability configuration are fixed, unlike
|
|
28
|
+
// the authority read fresh from current() for every recheck and dispatch.
|
|
29
|
+
surfaces;
|
|
28
30
|
constructor(host) {
|
|
29
31
|
this.host = host;
|
|
30
32
|
host.owner.sql.exec("CREATE TABLE IF NOT EXISTS oyster_approval_targets (record_id TEXT PRIMARY KEY, target_json TEXT NOT NULL) STRICT");
|
|
@@ -180,7 +182,7 @@ export class OysterApprovalHome {
|
|
|
180
182
|
byInstallation: new Map([[target.installationId, policy]]),
|
|
181
183
|
};
|
|
182
184
|
}
|
|
183
|
-
const snapshot = await oysterAppSnapshot(host, appSurfaceCache(host), principal, [installation], access, target.sessionId
|
|
185
|
+
const snapshot = await oysterAppSnapshot(host, (this.surfaces ??= appSurfaceCache(host)), principal, [installation], access, target.sessionId, "direct");
|
|
184
186
|
const module = snapshot.modules[record.module];
|
|
185
187
|
const fence = snapshot.fences[record.module];
|
|
186
188
|
const effect = snapshot.effects[record.module]?.[record.operation];
|
package/dist/apps/catalog.js
CHANGED
|
@@ -34,7 +34,7 @@ export const permissionCatalog = {
|
|
|
34
34
|
"apps.call": entry("apps", "Call declared App methods", "call the listed methods in other Apps as you. Renamed target methods need approval again.", "run", "install"),
|
|
35
35
|
"app.storage": entry("data", "Store App data", "save its App data.", "write", "install"),
|
|
36
36
|
"app.events.emit": entry("workflows", "Send events", "tell your Workflows when something happens.", "sendPrompts", "ask"),
|
|
37
|
-
"team.workbenchHost": entry("shell", "
|
|
37
|
+
"team.workbenchHost": entry("shell", "Shell", "Full access to your Team and every App", "list", "ask", null, "Only pick a shell you trust."),
|
|
38
38
|
"marketplace.browse": entry("bazaar", "Browse the marketplace", "browse Apps in the marketplace.", "browseBazaar", "install"),
|
|
39
39
|
"marketplace.purchase": entry("bazaar", "Purchase from the marketplace", "purchase Apps from the marketplace.", "buyApps", "install"),
|
|
40
40
|
"workflow.run": entry("workflows", "Run Workflows", "start Workflows.", "run", "install"),
|