@fungi.computer/agar 0.0.0-stage → 0.0.1
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/LICENSE +21 -0
- package/README.md +766 -2
- package/dist/agent-message-meta.d.ts +74 -0
- package/dist/agent-message-meta.js +63 -0
- package/dist/agents.d.ts +170 -0
- package/dist/agents.js +218 -0
- package/dist/app-build-contract.d.ts +43 -0
- package/dist/app-build-contract.js +24 -0
- package/dist/apps.d.ts +66 -0
- package/dist/apps.js +47 -0
- package/dist/backend-declaration.d.ts +6 -0
- package/dist/backend-declaration.js +65 -0
- package/dist/backend-definition.d.ts +88 -0
- package/dist/backend-definition.js +158 -0
- package/dist/backend-refusal.d.ts +10 -0
- package/dist/backend-refusal.js +10 -0
- package/dist/backend.d.ts +30 -0
- package/dist/backend.js +28 -0
- package/dist/capabilities.d.ts +30 -0
- package/dist/capabilities.js +50 -0
- package/dist/channels.d.ts +142 -0
- package/dist/channels.js +97 -0
- package/dist/commands.d.ts +33 -0
- package/dist/commands.js +46 -0
- package/dist/computer-contracts.d.ts +73 -0
- package/dist/computer-contracts.js +57 -0
- package/dist/computer.d.ts +294 -0
- package/dist/computer.js +319 -0
- package/dist/connect.d.ts +11 -0
- package/dist/connect.js +211 -0
- package/dist/errors.d.ts +34 -0
- package/dist/errors.js +106 -0
- package/dist/failure.d.ts +2 -0
- package/dist/failure.js +9 -0
- package/dist/files.d.ts +25 -0
- package/dist/files.js +84 -0
- package/dist/game-servers.d.ts +530 -0
- package/dist/game-servers.js +244 -0
- package/dist/gills.d.ts +43 -0
- package/dist/gills.js +174 -0
- package/dist/git-commit.d.ts +143 -0
- package/dist/git-commit.js +251 -0
- package/dist/git-rpc.d.ts +310 -0
- package/dist/git-rpc.js +166 -0
- package/dist/git-steps.d.ts +579 -0
- package/dist/git-steps.js +804 -0
- package/dist/git-wire.d.ts +33 -0
- package/dist/git-wire.js +240 -0
- package/dist/git.d.ts +322 -0
- package/dist/git.js +98 -0
- package/dist/host.d.ts +97 -0
- package/dist/host.js +732 -0
- package/dist/inbox.d.ts +99 -0
- package/dist/inbox.js +184 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +15 -0
- package/dist/inference.d.ts +17 -0
- package/dist/inference.js +115 -0
- package/dist/internal/app-error.d.ts +8 -0
- package/dist/internal/app-error.js +36 -0
- package/dist/internal/app-session.d.ts +8 -0
- package/dist/internal/app-session.js +551 -0
- package/dist/internal/back-inputs.d.ts +22 -0
- package/dist/internal/back-inputs.js +93 -0
- package/dist/internal/browser-bridge.d.ts +30 -0
- package/dist/internal/browser-bridge.js +203 -0
- package/dist/internal/capabilities.d.ts +13 -0
- package/dist/internal/capabilities.js +138 -0
- package/dist/internal/commands.d.ts +9 -0
- package/dist/internal/commands.js +257 -0
- package/dist/internal/disconnect-host.d.ts +7 -0
- package/dist/internal/disconnect-host.js +19 -0
- package/dist/internal/git-edit-tree.d.ts +6 -0
- package/dist/internal/git-edit-tree.js +46 -0
- package/dist/internal/memory-fs.d.ts +39 -0
- package/dist/internal/memory-fs.js +154 -0
- package/dist/internal/protocol.d.ts +116 -0
- package/dist/internal/protocol.js +6 -0
- package/dist/internal/query-feed.d.ts +23 -0
- package/dist/internal/query-feed.js +111 -0
- package/dist/internal/result.d.ts +22 -0
- package/dist/internal/result.js +61 -0
- package/dist/internal/rpc-contract.d.ts +24 -0
- package/dist/internal/rpc-contract.js +78 -0
- package/dist/internal/schemas.d.ts +307 -0
- package/dist/internal/schemas.js +524 -0
- package/dist/internal/store.d.ts +8 -0
- package/dist/internal/store.js +30 -0
- package/dist/internal/terminal-gate.d.ts +17 -0
- package/dist/internal/terminal-gate.js +94 -0
- package/dist/lifecycle.d.ts +44 -0
- package/dist/lifecycle.js +1 -0
- package/dist/marketplace.d.ts +159 -0
- package/dist/marketplace.js +102 -0
- package/dist/media-json.d.ts +181 -0
- package/dist/media-json.js +204 -0
- package/dist/media-library.d.ts +11 -0
- package/dist/media-library.js +15 -0
- package/dist/media-request-id.d.ts +7 -0
- package/dist/media-request-id.js +16 -0
- package/dist/media.d.ts +354 -0
- package/dist/media.js +654 -0
- package/dist/method-refusal.d.ts +64 -0
- package/dist/method-refusal.js +48 -0
- package/dist/pending.d.ts +10 -0
- package/dist/pending.js +10 -0
- package/dist/query.d.ts +76 -0
- package/dist/query.js +195 -0
- package/dist/react-mount.d.ts +23 -0
- package/dist/react-mount.js +108 -0
- package/dist/react.d.ts +41 -0
- package/dist/react.js +111 -0
- package/dist/read-copy.d.ts +103 -0
- package/dist/read-copy.js +195 -0
- package/dist/roles/files.d.ts +295 -0
- package/dist/roles/files.js +184 -0
- package/dist/root.d.ts +21 -0
- package/dist/root.js +1 -0
- package/dist/rpc.d.ts +1 -0
- package/dist/rpc.js +1 -0
- package/dist/status.d.ts +17 -0
- package/dist/status.js +50 -0
- package/dist/storage-wire.d.ts +167 -0
- package/dist/storage-wire.js +115 -0
- package/dist/storage.d.ts +72 -0
- package/dist/storage.js +143 -0
- package/dist/store.d.ts +11 -0
- package/dist/store.js +1 -0
- package/dist/test-kit/git.d.ts +31 -0
- package/dist/test-kit/git.js +174 -0
- package/dist/ui.d.ts +169 -0
- package/dist/ui.js +10 -0
- package/dist/window.d.ts +41 -0
- package/dist/window.js +15 -0
- package/dist/world.d.ts +16 -0
- package/dist/world.js +17 -0
- package/docs/api/README.md +28 -0
- package/docs/api/app-build-contract.md +113 -0
- package/docs/api/backend-declaration.md +62 -0
- package/docs/api/backend-definition.md +215 -0
- package/docs/api/backend.md +269 -0
- package/docs/api/errors.md +169 -0
- package/docs/api/files.md +91 -0
- package/docs/api/gills.md +164 -0
- package/docs/api/git-commit.md +279 -0
- package/docs/api/git-rpc.md +243 -0
- package/docs/api/git-steps.md +1010 -0
- package/docs/api/git-wire.md +197 -0
- package/docs/api/git.md +579 -0
- package/docs/api/index.md +2880 -0
- package/docs/api/media-json.md +507 -0
- package/docs/api/media-request-id.md +32 -0
- package/docs/api/modules.md +32 -0
- package/docs/api/query.md +253 -0
- package/docs/api/react.md +216 -0
- package/docs/api/read-copy.md +204 -0
- package/docs/api/rpc.md +128 -0
- package/docs/api/storage.md +620 -0
- package/package.json +197 -3
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
import { RpcStub, RpcTarget } from "@fungi.computer/capnweb";
|
|
2
|
+
/**
|
|
3
|
+
* The protocol owns its boundary parsing. Values leave this module as copied,
|
|
4
|
+
* frozen app data; live Cap'n Web references remain outside these schemas.
|
|
5
|
+
*/
|
|
6
|
+
import * as Schema from "effect/Schema";
|
|
7
|
+
import * as Result from "effect/Result";
|
|
8
|
+
import { flow } from "effect/Function";
|
|
9
|
+
import { parseCommandDefinition, parseJsonValue, } from "@fungi.computer/whistle/wire";
|
|
10
|
+
import { isUnsafeStatusCodeUnit } from "../status.js";
|
|
11
|
+
import { APP_BROWSER_SAVE_MAX_BYTES, APP_BROWSER_COPY_MAX_BYTES, APP_STATUS_ITEMS_MAX, APP_STATUS_OPTIONS_MAX, APP_STATUS_TEXT_MAX, } from "../ui.js";
|
|
12
|
+
import { APP_NAVIGATION_CONTRACT_VERSION, APP_NAVIGATION_TITLE_MAX, } from "../window.js";
|
|
13
|
+
import { AppError } from "../errors.js";
|
|
14
|
+
import { parseFailure } from "@fungi.computer/whistle/wire";
|
|
15
|
+
import { APP_CONNECT_TYPE, APP_PROTOCOL_VERSION, APP_READY_TYPE, } from "./protocol.js";
|
|
16
|
+
const errorCodes = [
|
|
17
|
+
"PROTOCOL",
|
|
18
|
+
"IDENTITY",
|
|
19
|
+
"INSUFFICIENT_CREDIT",
|
|
20
|
+
"FILE_TOO_LARGE",
|
|
21
|
+
"CAPABILITY_DENIED",
|
|
22
|
+
"CAPABILITY_REVOKED",
|
|
23
|
+
"COMMAND_NOT_DECLARED",
|
|
24
|
+
"CONFLICT",
|
|
25
|
+
"INVALID_ARGUMENT",
|
|
26
|
+
"TIMEOUT",
|
|
27
|
+
"CANCELLED",
|
|
28
|
+
"DISCONNECTED",
|
|
29
|
+
"HOST_FAILURE",
|
|
30
|
+
"SELECTION_CHANGED",
|
|
31
|
+
"SAFETY_FILTER_REFUSED",
|
|
32
|
+
];
|
|
33
|
+
const boundedText = (maximum) => Schema.String.pipe(Schema.check(Schema.isMinLength(1)), Schema.check(Schema.isMaxLength(maximum)));
|
|
34
|
+
const boundedOptionalText = (maximum) => Schema.optional(boundedText(maximum));
|
|
35
|
+
const safeInteger = Schema.Number.pipe(Schema.check(Schema.isInt()), Schema.check(Schema.makeFilter(Number.isSafeInteger, {
|
|
36
|
+
message: "Expected a safe integer",
|
|
37
|
+
})));
|
|
38
|
+
const safeVersion = safeInteger.pipe(Schema.check(Schema.isGreaterThanOrEqualTo(0)));
|
|
39
|
+
const positiveVersion = safeInteger.pipe(Schema.check(Schema.isGreaterThan(0)));
|
|
40
|
+
const identitySchema = Schema.Struct({
|
|
41
|
+
appId: boundedText(160),
|
|
42
|
+
releaseId: boundedText(160),
|
|
43
|
+
surfaceId: boundedText(160),
|
|
44
|
+
});
|
|
45
|
+
const focusSchema = Schema.Struct({
|
|
46
|
+
editing: Schema.Boolean,
|
|
47
|
+
surface: Schema.Union([boundedText(160), Schema.Null]),
|
|
48
|
+
});
|
|
49
|
+
const appOpenRequestSchema = Schema.Union([
|
|
50
|
+
Schema.Struct({
|
|
51
|
+
kind: Schema.Literal("compose"),
|
|
52
|
+
requestId: boundedText(160),
|
|
53
|
+
text: boundedText(2_000),
|
|
54
|
+
}),
|
|
55
|
+
Schema.Struct({
|
|
56
|
+
kind: Schema.Literal("navigate"),
|
|
57
|
+
requestId: boundedText(160),
|
|
58
|
+
params: Schema.Array(Schema.Struct({ key: boundedText(64), value: boundedText(4_096) })).pipe(Schema.check(Schema.isMaxLength(16))),
|
|
59
|
+
}),
|
|
60
|
+
]);
|
|
61
|
+
const contextSchema = Schema.Struct({
|
|
62
|
+
computerId: Schema.Union([boundedText(256), Schema.Null]),
|
|
63
|
+
teamHasComputers: Schema.optional(Schema.NullOr(Schema.Boolean)),
|
|
64
|
+
computerAccessGranted: Schema.optional(Schema.Boolean),
|
|
65
|
+
focus: focusSchema,
|
|
66
|
+
openRequest: Schema.Union([Schema.Null, appOpenRequestSchema]),
|
|
67
|
+
visibility: Schema.Literals(["visible", "hidden"]),
|
|
68
|
+
});
|
|
69
|
+
const lifecycleSchema = Schema.Literals([
|
|
70
|
+
"connecting",
|
|
71
|
+
"connected",
|
|
72
|
+
"suspended",
|
|
73
|
+
"disconnected",
|
|
74
|
+
"disposing",
|
|
75
|
+
"disposed",
|
|
76
|
+
]);
|
|
77
|
+
const chordSchema = Schema.Struct({
|
|
78
|
+
altKey: Schema.Boolean,
|
|
79
|
+
ctrlKey: Schema.Boolean,
|
|
80
|
+
id: boundedText(160),
|
|
81
|
+
key: boundedText(64),
|
|
82
|
+
metaKey: Schema.Boolean,
|
|
83
|
+
shiftKey: Schema.Boolean,
|
|
84
|
+
});
|
|
85
|
+
const commandSchema = Schema.Struct({
|
|
86
|
+
action: Schema.optional(Schema.Unknown),
|
|
87
|
+
availability: Schema.optional(Schema.Unknown),
|
|
88
|
+
category: boundedText(160),
|
|
89
|
+
description: boundedOptionalText(512),
|
|
90
|
+
id: boundedText(160),
|
|
91
|
+
projections: Schema.optional(Schema.Unknown),
|
|
92
|
+
title: boundedText(160),
|
|
93
|
+
});
|
|
94
|
+
const commandInvocationSchema = Schema.Struct({
|
|
95
|
+
arguments: Schema.optional(Schema.Unknown),
|
|
96
|
+
commandId: boundedText(321),
|
|
97
|
+
origin: Schema.Literals([
|
|
98
|
+
"agent",
|
|
99
|
+
"browser",
|
|
100
|
+
"keyboard",
|
|
101
|
+
"menu",
|
|
102
|
+
"palette",
|
|
103
|
+
"programmatic",
|
|
104
|
+
"slash",
|
|
105
|
+
"terminal",
|
|
106
|
+
]),
|
|
107
|
+
requestId: boundedText(160),
|
|
108
|
+
});
|
|
109
|
+
const commandResultWireSchema = Schema.Union([
|
|
110
|
+
Schema.Struct({
|
|
111
|
+
result: Schema.optional(Schema.Unknown),
|
|
112
|
+
status: Schema.Literal("handled"),
|
|
113
|
+
}),
|
|
114
|
+
Schema.Struct({
|
|
115
|
+
error: Schema.Unknown,
|
|
116
|
+
status: Schema.Literal("failed"),
|
|
117
|
+
}),
|
|
118
|
+
]);
|
|
119
|
+
/** One line of plain text an App puts in host chrome. */
|
|
120
|
+
const statusText = (max) => boundedText(max).pipe(Schema.check(Schema.makeFilter((text) => {
|
|
121
|
+
for (let index = 0; index < text.length; index += 1)
|
|
122
|
+
if (isUnsafeStatusCodeUnit(text.charCodeAt(index)))
|
|
123
|
+
return false;
|
|
124
|
+
return true;
|
|
125
|
+
}, { message: "Expected one line of plain text" })));
|
|
126
|
+
const statusBase = {
|
|
127
|
+
id: boundedText(64),
|
|
128
|
+
text: statusText(APP_STATUS_TEXT_MAX),
|
|
129
|
+
end: Schema.optionalKey(Schema.Boolean),
|
|
130
|
+
};
|
|
131
|
+
const statusCount = Schema.optionalKey(Schema.Number.pipe(Schema.check(Schema.isInt()), Schema.check(Schema.isBetween({ minimum: 0, maximum: 9_999 }))));
|
|
132
|
+
const statusItemsSchema = Schema.Array(Schema.Union([
|
|
133
|
+
Schema.Struct({
|
|
134
|
+
...statusBase,
|
|
135
|
+
kind: Schema.Literal("text"),
|
|
136
|
+
tone: Schema.optionalKey(Schema.Literals(["neutral", "success", "warning", "error"])),
|
|
137
|
+
}),
|
|
138
|
+
Schema.Struct({
|
|
139
|
+
...statusBase,
|
|
140
|
+
kind: Schema.Literal("button"),
|
|
141
|
+
icon: Schema.optionalKey(Schema.Literals(["folder", "ports", "processes", "access"])),
|
|
142
|
+
count: statusCount,
|
|
143
|
+
title: Schema.optionalKey(statusText(1_024)),
|
|
144
|
+
disabled: Schema.optionalKey(Schema.Boolean),
|
|
145
|
+
opens: Schema.optionalKey(Schema.Literal("dialog")),
|
|
146
|
+
expanded: Schema.optionalKey(Schema.Boolean),
|
|
147
|
+
}),
|
|
148
|
+
Schema.Struct({
|
|
149
|
+
...statusBase,
|
|
150
|
+
kind: Schema.Literal("toggle"),
|
|
151
|
+
pressed: Schema.Boolean,
|
|
152
|
+
}),
|
|
153
|
+
Schema.Struct({
|
|
154
|
+
...statusBase,
|
|
155
|
+
kind: Schema.Literal("menu"),
|
|
156
|
+
count: statusCount,
|
|
157
|
+
title: Schema.optionalKey(statusText(1_024)),
|
|
158
|
+
options: Schema.Array(Schema.Struct({
|
|
159
|
+
id: boundedText(64),
|
|
160
|
+
text: statusText(APP_STATUS_TEXT_MAX),
|
|
161
|
+
checked: Schema.optionalKey(Schema.Boolean),
|
|
162
|
+
disabled: Schema.optionalKey(Schema.Boolean),
|
|
163
|
+
})).pipe(Schema.check(Schema.isMaxLength(APP_STATUS_OPTIONS_MAX)),
|
|
164
|
+
// An option id names one choice; repeats would make actions ambiguous.
|
|
165
|
+
Schema.check(Schema.makeFilter((options) => new Set(options.map((option) => option.id)).size ===
|
|
166
|
+
options.length, { message: "Expected unique status option ids" }))),
|
|
167
|
+
}),
|
|
168
|
+
])).pipe(Schema.check(Schema.isMaxLength(APP_STATUS_ITEMS_MAX)),
|
|
169
|
+
// Ids key the host's rendering and name actions; each names one item.
|
|
170
|
+
Schema.check(Schema.makeFilter((items) => new Set(items.map((item) => item.id)).size === items.length, { message: "Expected unique status item ids" })));
|
|
171
|
+
const navigationSchema = Schema.Struct({
|
|
172
|
+
computerId: boundedText(256),
|
|
173
|
+
path: boundedText(1024),
|
|
174
|
+
});
|
|
175
|
+
const navigationTitleSchema = boundedText(APP_NAVIGATION_TITLE_MAX).pipe(Schema.check(Schema.makeFilter((title) => title.trim() === title, {
|
|
176
|
+
message: "Expected a trimmed title",
|
|
177
|
+
})));
|
|
178
|
+
const navigationStateWireSchema = Schema.Struct({
|
|
179
|
+
contractVersion: Schema.Literal(APP_NAVIGATION_CONTRACT_VERSION),
|
|
180
|
+
canGoBack: Schema.Boolean,
|
|
181
|
+
title: Schema.optional(navigationTitleSchema),
|
|
182
|
+
});
|
|
183
|
+
const navigationStateInputSchema = Schema.Struct({
|
|
184
|
+
canGoBack: Schema.Boolean,
|
|
185
|
+
title: Schema.optional(Schema.String),
|
|
186
|
+
});
|
|
187
|
+
const agentSessionSelectionSchema = Schema.NullOr(Schema.Struct({
|
|
188
|
+
agentId: boundedText(256),
|
|
189
|
+
sessionId: boundedText(256),
|
|
190
|
+
}));
|
|
191
|
+
const permissionRequestSchema = Schema.Struct({
|
|
192
|
+
agents: Schema.optional(Schema.Array(Schema.Literals([
|
|
193
|
+
"agent.list",
|
|
194
|
+
"agent.manage",
|
|
195
|
+
"agent.read",
|
|
196
|
+
"agent.send",
|
|
197
|
+
])).pipe(Schema.check(Schema.isMaxLength(4)))),
|
|
198
|
+
computers: Schema.optional(Schema.Array(Schema.Literals([
|
|
199
|
+
"computer.list",
|
|
200
|
+
"computer.readFile",
|
|
201
|
+
"computer.writeFile",
|
|
202
|
+
"computer.execute",
|
|
203
|
+
])).pipe(Schema.check(Schema.isMaxLength(4)))),
|
|
204
|
+
});
|
|
205
|
+
const permissionOutcomeSchema = Schema.Literals([
|
|
206
|
+
"granted",
|
|
207
|
+
"declined",
|
|
208
|
+
"customize",
|
|
209
|
+
]);
|
|
210
|
+
const toastSchema = Schema.Struct({
|
|
211
|
+
message: boundedText(512),
|
|
212
|
+
tone: Schema.optional(Schema.Literals(["info", "success", "warning", "error"])),
|
|
213
|
+
});
|
|
214
|
+
const errorSchema = Schema.Struct({
|
|
215
|
+
code: Schema.Literals(errorCodes),
|
|
216
|
+
message: boundedText(512),
|
|
217
|
+
retryable: Schema.Boolean,
|
|
218
|
+
});
|
|
219
|
+
const readySchema = Schema.Struct({
|
|
220
|
+
protocolVersion: Schema.Literal(APP_PROTOCOL_VERSION),
|
|
221
|
+
type: Schema.Literal(APP_READY_TYPE),
|
|
222
|
+
});
|
|
223
|
+
const connectSchema = Schema.Struct({
|
|
224
|
+
epoch: boundedText(160),
|
|
225
|
+
protocolVersion: Schema.Literal(APP_PROTOCOL_VERSION),
|
|
226
|
+
type: Schema.Literal(APP_CONNECT_TYPE),
|
|
227
|
+
});
|
|
228
|
+
const connectRequestSchema = Schema.Struct({
|
|
229
|
+
epoch: boundedText(160),
|
|
230
|
+
protocolVersion: Schema.Literal(APP_PROTOCOL_VERSION),
|
|
231
|
+
});
|
|
232
|
+
// Native Cap’n Web decorates received object envelopes with this resource owner.
|
|
233
|
+
// Schema owns strict key validation; the returned domain projection omits the symbol.
|
|
234
|
+
const rpcOwner = { [Symbol.dispose]: Schema.optional(Schema.Unknown) };
|
|
235
|
+
const rpcEnvelopeSchema = Schema.Union([
|
|
236
|
+
Schema.Struct({
|
|
237
|
+
...rpcOwner,
|
|
238
|
+
ok: Schema.Literal(true),
|
|
239
|
+
value: Schema.Unknown,
|
|
240
|
+
}),
|
|
241
|
+
Schema.Struct({ ...rpcOwner, error: errorSchema, ok: Schema.Literal(false) }),
|
|
242
|
+
]);
|
|
243
|
+
/** Existing protocol decoder: maintained parsing, then owned domain projection. */
|
|
244
|
+
function decoder(schema, project, code = "PROTOCOL") {
|
|
245
|
+
return flow(Schema.decodeUnknownResult(schema, { onExcessProperty: "error" }), Result.match({
|
|
246
|
+
onFailure: (cause) => {
|
|
247
|
+
throw new AppError("The peer sent malformed protocol data", {
|
|
248
|
+
cause,
|
|
249
|
+
code,
|
|
250
|
+
retryable: false,
|
|
251
|
+
});
|
|
252
|
+
},
|
|
253
|
+
onSuccess: project,
|
|
254
|
+
}));
|
|
255
|
+
}
|
|
256
|
+
function frozen(value) {
|
|
257
|
+
return Object.freeze(value);
|
|
258
|
+
}
|
|
259
|
+
export function appInvocation(value, signal) {
|
|
260
|
+
const argumentsValue = value.arguments === undefined ? undefined : parseJsonValue(value.arguments);
|
|
261
|
+
const invocation = { origin: value.origin, signal };
|
|
262
|
+
if (argumentsValue !== undefined)
|
|
263
|
+
Object.assign(invocation, { arguments: argumentsValue });
|
|
264
|
+
return frozen(invocation);
|
|
265
|
+
}
|
|
266
|
+
export const parseCommand = decoder(commandSchema, (parsed) => {
|
|
267
|
+
const command = {
|
|
268
|
+
category: parsed.category,
|
|
269
|
+
id: parsed.id,
|
|
270
|
+
title: parsed.title,
|
|
271
|
+
};
|
|
272
|
+
if (parsed.action !== undefined)
|
|
273
|
+
Object.assign(command, {
|
|
274
|
+
action: parsed.action,
|
|
275
|
+
});
|
|
276
|
+
if (parsed.availability !== undefined)
|
|
277
|
+
Object.assign(command, {
|
|
278
|
+
availability: parsed.availability,
|
|
279
|
+
});
|
|
280
|
+
if (parsed.description !== undefined)
|
|
281
|
+
Object.assign(command, { description: parsed.description });
|
|
282
|
+
if (parsed.projections !== undefined)
|
|
283
|
+
Object.assign(command, {
|
|
284
|
+
projections: parsed.projections,
|
|
285
|
+
});
|
|
286
|
+
return frozen(parseSemantic(() => parseCommandDefinition(command)));
|
|
287
|
+
}, "INVALID_ARGUMENT");
|
|
288
|
+
export const parseCommandId = decoder(boundedText(321), (value) => value);
|
|
289
|
+
export const parseCommandInvocation = decoder(commandInvocationSchema, (value) => {
|
|
290
|
+
const argumentsValue = value.arguments === undefined
|
|
291
|
+
? undefined
|
|
292
|
+
: parseJsonValue(value.arguments);
|
|
293
|
+
const invocation = {
|
|
294
|
+
commandId: value.commandId,
|
|
295
|
+
origin: value.origin,
|
|
296
|
+
requestId: value.requestId,
|
|
297
|
+
};
|
|
298
|
+
if (argumentsValue !== undefined)
|
|
299
|
+
Object.assign(invocation, { arguments: argumentsValue });
|
|
300
|
+
return frozen(invocation);
|
|
301
|
+
});
|
|
302
|
+
/** Decode an action result without carrying executable or host-private data. */
|
|
303
|
+
export const parseCommandResult = decoder(Schema.Union([Schema.Undefined, Schema.Unknown]), (value) => value === undefined ? undefined : parseJsonValue(value));
|
|
304
|
+
export const parseConnect = decoder(connectSchema, (value) => frozen(value));
|
|
305
|
+
export const parseConnectRequest = decoder(connectRequestSchema, (value) => frozen(value));
|
|
306
|
+
export const parseContext = decoder(contextSchema, (parsed) => {
|
|
307
|
+
const context = {
|
|
308
|
+
computerId: parsed.computerId,
|
|
309
|
+
focus: frozen({ ...parsed.focus }),
|
|
310
|
+
openRequest: parsed.openRequest === null ? null : frozen({ ...parsed.openRequest }),
|
|
311
|
+
visibility: parsed.visibility,
|
|
312
|
+
};
|
|
313
|
+
if (parsed.teamHasComputers !== undefined)
|
|
314
|
+
context.teamHasComputers = parsed.teamHasComputers;
|
|
315
|
+
if (parsed.computerAccessGranted !== undefined)
|
|
316
|
+
context.computerAccessGranted = parsed.computerAccessGranted;
|
|
317
|
+
return frozen(context);
|
|
318
|
+
});
|
|
319
|
+
export const parseErrorData = decoder(errorSchema, (value) => frozen({ ...value }));
|
|
320
|
+
export const parseFocus = decoder(focusSchema, (value) => frozen({ ...value }), "INVALID_ARGUMENT");
|
|
321
|
+
export const parseIdentity = decoder(identitySchema, (value) => frozen({ ...value }));
|
|
322
|
+
export const parseLifecycle = decoder(lifecycleSchema, (value) => value);
|
|
323
|
+
/** Both edges: one window's status items, bounded and copied. */
|
|
324
|
+
export const parseStatusItems = decoder(statusItemsSchema, (items) => frozen(items.map((item) => item.kind === "menu"
|
|
325
|
+
? frozen({
|
|
326
|
+
...item,
|
|
327
|
+
options: frozen(item.options.map((option) => frozen(option))),
|
|
328
|
+
})
|
|
329
|
+
: frozen(item))), "INVALID_ARGUMENT");
|
|
330
|
+
/** App edge: the status item the person used, as the host relays it. */
|
|
331
|
+
export const parseStatusAction = decoder(Schema.Struct({
|
|
332
|
+
itemId: boundedText(64),
|
|
333
|
+
optionId: Schema.optionalKey(boundedText(64)),
|
|
334
|
+
anchor: Schema.optionalKey(Schema.Struct({
|
|
335
|
+
start: Schema.Number.pipe(Schema.check(Schema.isBetween({ minimum: 0, maximum: 100_000 }))),
|
|
336
|
+
width: Schema.Number.pipe(Schema.check(Schema.isBetween({ minimum: 0, maximum: 100_000 }))),
|
|
337
|
+
})),
|
|
338
|
+
}), (action) => frozen(action.anchor === undefined
|
|
339
|
+
? { ...action }
|
|
340
|
+
: { ...action, anchor: frozen({ ...action.anchor }) }), "INVALID_ARGUMENT");
|
|
341
|
+
export const parseNavigation = decoder(navigationSchema, (value) => frozen({ ...value }), "INVALID_ARGUMENT");
|
|
342
|
+
/** Host edge: one App report, projected without its contract version. */
|
|
343
|
+
export const parseNavigationState = decoder(navigationStateWireSchema, (value) => frozen(value.title === undefined
|
|
344
|
+
? { canGoBack: value.canGoBack }
|
|
345
|
+
: { canGoBack: value.canGoBack, title: value.title }), "INVALID_ARGUMENT");
|
|
346
|
+
/** App edge: trim the title, drop an empty one, then check the wire bounds. */
|
|
347
|
+
export const parseNavigationStateInput = decoder(navigationStateInputSchema, (value) => {
|
|
348
|
+
const title = value.title?.trim();
|
|
349
|
+
return parseNavigationStateWire(title === undefined || title.length === 0
|
|
350
|
+
? {
|
|
351
|
+
contractVersion: APP_NAVIGATION_CONTRACT_VERSION,
|
|
352
|
+
canGoBack: value.canGoBack,
|
|
353
|
+
}
|
|
354
|
+
: {
|
|
355
|
+
contractVersion: APP_NAVIGATION_CONTRACT_VERSION,
|
|
356
|
+
canGoBack: value.canGoBack,
|
|
357
|
+
title,
|
|
358
|
+
});
|
|
359
|
+
}, "INVALID_ARGUMENT");
|
|
360
|
+
const parseNavigationStateWire = decoder(navigationStateWireSchema, (value) => frozen({ ...value }), "INVALID_ARGUMENT");
|
|
361
|
+
export const parseAgentSessionSelection = decoder(agentSessionSelectionSchema, (value) => value === null ? null : frozen({ ...value }), "INVALID_ARGUMENT");
|
|
362
|
+
export const parsePermissionRequest = decoder(permissionRequestSchema, (value) => frozen({
|
|
363
|
+
agents: frozen([...new Set(value.agents ?? [])]),
|
|
364
|
+
computers: frozen([...new Set(value.computers ?? [])]),
|
|
365
|
+
}), "INVALID_ARGUMENT");
|
|
366
|
+
export const parseReady = decoder(readySchema, (value) => frozen(value));
|
|
367
|
+
export const parseTheme = decoder(boundedText(512), (value) => value);
|
|
368
|
+
export const parseToast = decoder(toastSchema, (value) => frozen({ ...value }), "INVALID_ARGUMENT");
|
|
369
|
+
export const parseChordInvocation = decoder(Schema.Struct({ id: boundedText(160), repeat: Schema.Boolean }), (value) => frozen(value));
|
|
370
|
+
export const parseReservedChords = decoder(Schema.Array(chordSchema).pipe(Schema.check(Schema.isMaxLength(64))), (parsed) => {
|
|
371
|
+
if (new Set(parsed.map((chord) => chord.id)).size !== parsed.length) {
|
|
372
|
+
throw new AppError("Reserved chords are invalid", {
|
|
373
|
+
code: "PROTOCOL",
|
|
374
|
+
retryable: false,
|
|
375
|
+
});
|
|
376
|
+
}
|
|
377
|
+
return frozen(parsed.map((chord) => frozen({ ...chord })));
|
|
378
|
+
});
|
|
379
|
+
/** Compose a fact decoder from the maintained decoder of its domain value. */
|
|
380
|
+
export function parseFact(parseValue) {
|
|
381
|
+
return decoder(Schema.Struct({ value: Schema.Unknown, version: safeVersion }), (parsed) => frozen({ value: parseValue(parsed.value), version: parsed.version }));
|
|
382
|
+
}
|
|
383
|
+
export const parseContextFact = parseFact(parseContext);
|
|
384
|
+
export const parseHostReconfiguration = decoder(Schema.Struct({
|
|
385
|
+
capabilities: Schema.Array(Schema.Unknown).pipe(Schema.check(Schema.isMaxLength(128))),
|
|
386
|
+
context: Schema.Unknown,
|
|
387
|
+
}), (parsed) => frozen({
|
|
388
|
+
capabilities: frozen(parsed.capabilities.map((grant) => parseCapabilityGrant(grant))),
|
|
389
|
+
context: parseContextFact(parsed.context),
|
|
390
|
+
}));
|
|
391
|
+
const parseLifecycleFact = parseFact(parseLifecycle);
|
|
392
|
+
export const parseReservedChordsFact = parseFact(parseReservedChords);
|
|
393
|
+
export const parseThemeFact = parseFact(parseTheme);
|
|
394
|
+
export const parseHostLifecycleFact = flow(parseLifecycleFact, (fact) => {
|
|
395
|
+
if (fact.value !== "connected" && fact.value !== "suspended") {
|
|
396
|
+
throw new AppError("The host lifecycle is invalid", {
|
|
397
|
+
code: "PROTOCOL",
|
|
398
|
+
retryable: false,
|
|
399
|
+
});
|
|
400
|
+
}
|
|
401
|
+
return frozen({ value: fact.value, version: fact.version });
|
|
402
|
+
});
|
|
403
|
+
function capabilityGrantDecoder(allowLocalTarget) {
|
|
404
|
+
return decoder(Schema.Struct({
|
|
405
|
+
handle: Schema.Unknown,
|
|
406
|
+
id: boundedText(160),
|
|
407
|
+
version: positiveVersion,
|
|
408
|
+
}), (parsed) => {
|
|
409
|
+
if (!(parsed.handle instanceof RpcStub) &&
|
|
410
|
+
!(allowLocalTarget && parsed.handle instanceof RpcTarget)) {
|
|
411
|
+
throw new AppError("A capability handle is invalid", {
|
|
412
|
+
code: "PROTOCOL",
|
|
413
|
+
retryable: false,
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
return frozen({
|
|
417
|
+
handle: parsed.handle,
|
|
418
|
+
id: parsed.id,
|
|
419
|
+
version: parsed.version,
|
|
420
|
+
});
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
export const parseCapabilityGrant = capabilityGrantDecoder(false);
|
|
424
|
+
const parseLocalCapabilityGrant = capabilityGrantDecoder(true);
|
|
425
|
+
function hostBootstrapDecoder(allowLocalTarget) {
|
|
426
|
+
const parseGrant = allowLocalTarget
|
|
427
|
+
? parseLocalCapabilityGrant
|
|
428
|
+
: parseCapabilityGrant;
|
|
429
|
+
return decoder(Schema.Struct({
|
|
430
|
+
capabilities: Schema.Array(Schema.Unknown).pipe(Schema.check(Schema.isMaxLength(128))),
|
|
431
|
+
context: Schema.Unknown,
|
|
432
|
+
identity: Schema.Unknown,
|
|
433
|
+
lifecycle: Schema.Unknown,
|
|
434
|
+
reservedChords: Schema.Unknown,
|
|
435
|
+
theme: Schema.Unknown,
|
|
436
|
+
}), (parsed) => frozen({
|
|
437
|
+
capabilities: frozen(parsed.capabilities.map((grant) => parseGrant(grant))),
|
|
438
|
+
context: parseContextFact(parsed.context),
|
|
439
|
+
identity: parseIdentity(parsed.identity),
|
|
440
|
+
lifecycle: parseHostLifecycleFact(parsed.lifecycle),
|
|
441
|
+
reservedChords: parseReservedChordsFact(parsed.reservedChords),
|
|
442
|
+
theme: parseThemeFact(parsed.theme),
|
|
443
|
+
}));
|
|
444
|
+
}
|
|
445
|
+
const parseHostBootstrap = hostBootstrapDecoder(false);
|
|
446
|
+
export const parseLocalHostBootstrap = hostBootstrapDecoder(true);
|
|
447
|
+
function rpcDecoder(parseSuccess) {
|
|
448
|
+
return decoder(rpcEnvelopeSchema, (envelope) => envelope.ok
|
|
449
|
+
? frozen({ ok: true, value: parseSuccess(envelope.value) })
|
|
450
|
+
: frozen({ ok: false, error: parseErrorData(envelope.error) }));
|
|
451
|
+
}
|
|
452
|
+
export const parseBootstrapResult = rpcDecoder(parseHostBootstrap);
|
|
453
|
+
export const parseRegisteredCommandResult = rpcDecoder(decoder(Schema.Struct({ commandId: boundedText(321) }), (value) => frozen(value)));
|
|
454
|
+
export const parseCommandResultRpc = rpcDecoder(decoder(commandResultWireSchema, (value) => {
|
|
455
|
+
if (value.status === "failed")
|
|
456
|
+
return frozen({
|
|
457
|
+
status: "failed",
|
|
458
|
+
error: parseSemantic(() => parseFailure(value.error)),
|
|
459
|
+
});
|
|
460
|
+
const result = value.result === undefined ? undefined : parseJsonValue(value.result);
|
|
461
|
+
return result === undefined
|
|
462
|
+
? frozen({ status: "handled" })
|
|
463
|
+
: frozen({ status: "handled", result });
|
|
464
|
+
}));
|
|
465
|
+
export const parseVoidResult = rpcDecoder(decoder(Schema.Undefined, () => undefined));
|
|
466
|
+
export const parsePermissionOutcomeResult = rpcDecoder(decoder(permissionOutcomeSchema, (value) => value));
|
|
467
|
+
export const parseCloseDecisionResult = rpcDecoder(decoder(Schema.Boolean, (value) => value));
|
|
468
|
+
export const parseBackDecisionResult = rpcDecoder(decoder(Schema.Boolean, (value) => value));
|
|
469
|
+
function parseSemantic(parse) {
|
|
470
|
+
try {
|
|
471
|
+
return parse();
|
|
472
|
+
}
|
|
473
|
+
catch (cause) {
|
|
474
|
+
throw new AppError("The command metadata is invalid", {
|
|
475
|
+
cause,
|
|
476
|
+
code: "INVALID_ARGUMENT",
|
|
477
|
+
retryable: false,
|
|
478
|
+
});
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
const browserActionSchema = Schema.Union([
|
|
482
|
+
Schema.Struct({
|
|
483
|
+
kind: Schema.Literal("saveFile"),
|
|
484
|
+
name: boundedText(255).pipe(Schema.check(Schema.isPattern(/^[^\p{Cc}/\\]+$/u))),
|
|
485
|
+
mime: boundedText(255),
|
|
486
|
+
bytes: Schema.Uint8Array,
|
|
487
|
+
}),
|
|
488
|
+
Schema.Struct({ kind: Schema.Literal("copyText"), text: Schema.String }),
|
|
489
|
+
]);
|
|
490
|
+
/** Decode before effects, and reject oversized data before copying or sending it. */
|
|
491
|
+
export function parseBrowserAction(input) {
|
|
492
|
+
const parsed = Schema.decodeUnknownResult(browserActionSchema, {
|
|
493
|
+
onExcessProperty: "error",
|
|
494
|
+
})(input);
|
|
495
|
+
if (Result.isFailure(parsed))
|
|
496
|
+
return { kind: "refused", reason: "invalid-request" };
|
|
497
|
+
const value = parsed.success;
|
|
498
|
+
if (value.kind === "saveFile") {
|
|
499
|
+
if (value.name === "." || value.name === "..")
|
|
500
|
+
return { kind: "refused", reason: "invalid-request" };
|
|
501
|
+
if (value.bytes.byteLength > APP_BROWSER_SAVE_MAX_BYTES)
|
|
502
|
+
return { kind: "refused", reason: "too-large" };
|
|
503
|
+
return { ...value, bytes: new Uint8Array(value.bytes) };
|
|
504
|
+
}
|
|
505
|
+
if (value.text.length > APP_BROWSER_COPY_MAX_BYTES ||
|
|
506
|
+
new TextEncoder().encode(value.text).byteLength > APP_BROWSER_COPY_MAX_BYTES)
|
|
507
|
+
return { kind: "refused", reason: "too-large" };
|
|
508
|
+
return value;
|
|
509
|
+
}
|
|
510
|
+
export const parseBrowserActionOutcomeResult = rpcDecoder(decoder(Schema.Union([
|
|
511
|
+
Schema.Struct({ kind: Schema.Literal("saved") }),
|
|
512
|
+
Schema.Struct({ kind: Schema.Literal("copied") }),
|
|
513
|
+
Schema.Struct({
|
|
514
|
+
kind: Schema.Literal("refused"),
|
|
515
|
+
reason: Schema.Literals([
|
|
516
|
+
"no-user-gesture",
|
|
517
|
+
"too-large",
|
|
518
|
+
"invalid-request",
|
|
519
|
+
"unavailable",
|
|
520
|
+
"denied",
|
|
521
|
+
"busy",
|
|
522
|
+
]),
|
|
523
|
+
}),
|
|
524
|
+
]), (value) => frozen(value)));
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WHY: stores retain semantic snapshots and isolate untrusted subscribers so
|
|
3
|
+
* one callback cannot wedge publication to the host-facing UI (2026-08-27).
|
|
4
|
+
*/
|
|
5
|
+
import type { ReadonlyStore } from "../store.js";
|
|
6
|
+
export declare function createReadonlyStore<T>(initial: T): {
|
|
7
|
+
update(value: T): void;
|
|
8
|
+
} & ReadonlyStore<T>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export function createReadonlyStore(initial) {
|
|
2
|
+
let value = initial;
|
|
3
|
+
const listeners = new Set();
|
|
4
|
+
return {
|
|
5
|
+
get: () => value,
|
|
6
|
+
subscribe(listener) {
|
|
7
|
+
listeners.add(listener);
|
|
8
|
+
let active = true;
|
|
9
|
+
return () => {
|
|
10
|
+
if (active) {
|
|
11
|
+
active = false;
|
|
12
|
+
listeners.delete(listener);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
},
|
|
16
|
+
update(next) {
|
|
17
|
+
if (Object.is(value, next))
|
|
18
|
+
return;
|
|
19
|
+
value = next;
|
|
20
|
+
for (const listener of Array.from(listeners)) {
|
|
21
|
+
try {
|
|
22
|
+
listener(value);
|
|
23
|
+
}
|
|
24
|
+
catch (cause) {
|
|
25
|
+
console.warn("App store subscriber failed", cause);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { AppError } from "../errors.js";
|
|
2
|
+
/** Private per-epoch terminal ownership; never exposed through the SDK. */
|
|
3
|
+
export type TerminalGate = {
|
|
4
|
+
failBeforePublication(error: AppError): void;
|
|
5
|
+
graceful(): Promise<void>;
|
|
6
|
+
isPublished(): boolean;
|
|
7
|
+
publish(): boolean;
|
|
8
|
+
release(): void;
|
|
9
|
+
unexpected(): void;
|
|
10
|
+
};
|
|
11
|
+
export type TerminalGateOptions = {
|
|
12
|
+
readonly dispose: () => void;
|
|
13
|
+
readonly onDisconnect: () => Promise<void>;
|
|
14
|
+
readonly reject: (error: AppError) => void;
|
|
15
|
+
readonly release: () => void;
|
|
16
|
+
};
|
|
17
|
+
export declare function createTerminalGate(options: TerminalGateOptions): TerminalGate;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { AppError } from "../errors.js";
|
|
2
|
+
/** Owns every terminal side effect for one published mount epoch. */
|
|
3
|
+
class MountTerminal {
|
|
4
|
+
options;
|
|
5
|
+
#disconnect;
|
|
6
|
+
#published = false;
|
|
7
|
+
#settled = false;
|
|
8
|
+
constructor(options) {
|
|
9
|
+
this.options = options;
|
|
10
|
+
}
|
|
11
|
+
failBeforePublication(error) {
|
|
12
|
+
if (this.#settled)
|
|
13
|
+
return;
|
|
14
|
+
this.#settled = true;
|
|
15
|
+
this.#disposeAnd((options) => {
|
|
16
|
+
options.reject(error);
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
graceful() {
|
|
20
|
+
if (!this.#claimPublished())
|
|
21
|
+
return Promise.resolve();
|
|
22
|
+
return this.#disconnectAndDispose();
|
|
23
|
+
}
|
|
24
|
+
isPublished() {
|
|
25
|
+
return this.#published;
|
|
26
|
+
}
|
|
27
|
+
publish() {
|
|
28
|
+
if (this.#settled || this.#published)
|
|
29
|
+
return false;
|
|
30
|
+
this.#published = true;
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
release() {
|
|
34
|
+
if (this.#settled)
|
|
35
|
+
return;
|
|
36
|
+
this.#settled = true;
|
|
37
|
+
this.#warned(this.options.release, "App host mount release failed");
|
|
38
|
+
}
|
|
39
|
+
unexpected() {
|
|
40
|
+
if (this.#settled)
|
|
41
|
+
return;
|
|
42
|
+
if (!this.#published) {
|
|
43
|
+
this.#settled = true;
|
|
44
|
+
this.#disposeAnd((options) => {
|
|
45
|
+
options.reject(new AppError("The app host disconnected", {
|
|
46
|
+
code: "DISCONNECTED",
|
|
47
|
+
retryable: true,
|
|
48
|
+
}));
|
|
49
|
+
});
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
// Claim before scheduling effects. A later break cannot re-enter this path.
|
|
53
|
+
this.#settled = true;
|
|
54
|
+
void this.#disconnectAndDispose();
|
|
55
|
+
}
|
|
56
|
+
#claimPublished() {
|
|
57
|
+
if (this.#settled || !this.#published)
|
|
58
|
+
return false;
|
|
59
|
+
this.#settled = true;
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
#disconnectAndDispose() {
|
|
63
|
+
if (!this.#disconnect) {
|
|
64
|
+
this.#disconnect = Promise.resolve()
|
|
65
|
+
.then(this.options.onDisconnect)
|
|
66
|
+
.catch((cause) => {
|
|
67
|
+
console.warn("App host disconnect callback failed", cause);
|
|
68
|
+
})
|
|
69
|
+
.finally(() => {
|
|
70
|
+
this.#warned(this.options.dispose, "App host terminal cleanup failed");
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
return this.#disconnect;
|
|
74
|
+
}
|
|
75
|
+
#disposeAnd(after) {
|
|
76
|
+
try {
|
|
77
|
+
this.#warned(this.options.dispose, "App host terminal cleanup failed");
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
after(this.options);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
#warned(effect, message) {
|
|
84
|
+
try {
|
|
85
|
+
effect();
|
|
86
|
+
}
|
|
87
|
+
catch (cause) {
|
|
88
|
+
console.warn(message, cause);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
export function createTerminalGate(options) {
|
|
93
|
+
return new MountTerminal(options);
|
|
94
|
+
}
|