| subsystem | plugin-runtime | |||||||||
|---|---|---|---|---|---|---|---|---|---|---|
| owned_paths |
|
FlyBot's plugin API surface on top of the patched core: host hooks, client slots/registry, and types.
core/src/host/hooks.js— host-side plugin hooks.core/src/host/flybot-feed.js— the team activity feed (turn.*,tool.*,exchange.delivered,decision) folded fromsession/event(#241).core/src/client/flybot-slots.js— client slot registry.core/src/client/flybot-registry.js— plugin discovery/registration.core/types/flybot.d.ts— public types.- Flybot unit tests (
flybot.test.js,flybot-client.test.js,flybot-feed.test.js);flybot-client.test.jsalso pins the one name a mention wears on every surface — menu insert, bubble chip, sidebar preview, at-source row — and that two friends' same-named Bots stay apart while every alias still routes to its own handle (#967), the sidebar footer button (Connect apps opens Connectors), the account menu's Plugins row, the account menu's labels (Connect apps, Scheduled tasks opening Settings → Scheduled tasks, Settings, and the only classic Agent mode switch; #392-#394), and a face-room row that keeps its full title on hover (#304) while the number-first title wraps (#1102).
- Generic host/client code under
core/src/(→ ds-bot-core). - Concrete plugins
plugins/tree/,plugins/social/.
| File | Role |
|---|---|
core/src/host/hooks.js |
Host hook points: send middleware, transports, grants, the owner-bound held-send approvals (#52), task hold/decide/delete seams (#54), and bot.update({ name?, hidden? }) for plugin rename/hide (#452). A hold's waiting status is the shared rule src/shared/approval-waiting.js isApprovalWaiting(): decide(), heldOf() (the chat card), PENDING_MAX, the in-flight filter and the APPROVALS_KEPT cleanup all ask it instead of comparing to pending, so a pending_approval hold is decidable, drawn as waiting, counted and never cleaned up mid-wait (#754). It also exposes flybot.approvals.stats() — per-Bot, per-status counts of held sends, read-only (#396/#57 direction). A dispatch that fails keeps its error on the record, and an error the sender marks noRetry: true — a refusal nothing can change, such as a friend’s Bot no longer shared — reaches the chat view as retry: false so the card draws no Retry (#954); every other failure stays retryable. Whether a failure can still be retried is one rule in src/shared/held-retry.js (#968): canRetry(record) reads the flags or, for an older record, the stored sentence, and approvals.list marks a failed terminal approval retry: false so a dock card and the chat card answer the same question the same way. rooms.list() copies sharedFrom.remoteRoomId, ticket and inviterHandle when a room has them, so a mention of an inbound seat can name the host's room id inside the share line (#1126). |
core/src/host/flybot-feed.js |
Activity feed: folds a Bot Session's session/events into flybot.on('turn.start' | 'turn.end' | 'tool.start' | 'tool.end' | 'exchange.delivered' | 'decision') (#241). turn.end also carries the turn's token counts (summed from its assistant/message usage, floored and capped, counts only) under tokens (#357). decision is fired by decisions.js (#396), not folded from a Session event. |
core/src/client/flybot-slots.js |
Client slots, including the approval card queue (flybot.approvals, flybot/decide) and flybot.sidebar.structure (parentOf, optional include, menuItems, nodeActions). Every flybot.sidebar.section entry is wrapped in div.bt-flybot-section, and a wrapper a section leaves empty (its component renders null) draws no separator: the :empty rule zeroes that margin, border and padding, keyed on emptiness rather than on a section id, so the next null-rendering section is covered too (#818; core/test/flybot-section-empty.test.js renders the host entry and pins both rules). Its Approve / Reject and the held-reply card both ask approval-waiting.js isApprovalWaiting() (pending and pending_approval wait), so a hold carrying the spelling Bot Social's own outbound hold uses stays answerable (#734). Its dock card draws Retry only while src/shared/held-retry.js canRetry(approval) allows it — a terminal failure asks one shared predicate instead of status === 'failed' (#968) — and it publishes globalThis.__flybot__.canRetry so a classic-script plugin's own dock asks the same rule rather than keeping a second copy. |
core/src/client/flybot-registry.js |
Registry. A friend's Bot row in the @ menu carries remoteMentionLabel — the one name its bubble and the sidebar preview wear — so the menu inserts the name the user reads, while the row keeps the handle as its value and the send still routes by handle (#967). |
core/types/flybot.d.ts |
Types (BotView.hidden, bot.update, …). |
- Host:
hooks.jsloaded by the core host. - Client: slots/registry wired into the built client.
- Hooks are the only sanctioned extension surface; never monkey-patch DSH.
- Slots/registry must keep the client build green (
npm run build). - Types in
flybot.d.tsstay in sync with hooks/slots. A tool keeps one schema fromflybot.tools.registerto the model, and that schema namespropertiesandrequiredon every object node: the wrapper incore/src/host/tool-schema.jscompletes what DSH's compiler leaves out (a strict OpenAI-compatible gateway reads an absent member as null and rejects the whole request), so a plugin's parameter map needs no such member of its own. - Plugins report a host-visible failure with
flybot.warn(Settings → Feedback /diagnostics.jsonl). They must not depend onctx.logger: DSH may export none, andflybot.warndelegates tort.warn. A tool whosevisibleTosays no is taken out of that Bot's list withagent.ctx.tools.restrict({ deny })(hidePluginTools). DSH'stools.schemas(agent)lists only what the Agent sees, so the tools the current mask hides count as known when the mask is recomputed; otherwise every other resolve lifts the mask and the chat showsTool added/Tool removedon each turn (#276). The mask is recomputed when the Agent is created, on everyresolveAgent(delivery), and when a turn starts in the Session (asession/eventturn/start), so a Bot a plugin adopts only after its Agent was created — the tree taking a Main Bot made after the server started — gets its tools on the next turn, with no restart (#594). The names of these gated tools go to the client as the roster'spluginTools(rt.pluginToolNames), and the client'sContextCellhidesTool added/Tool removedrows for them in every chat, so the first mask leaves no line either (#419). The same mask also drops the names a run Bot may not call: for anephemeralBot the deny list addsEPHEMERAL_DENIED_TOOLSfromtools.js(the team-reaching toolsconnector-access.jsrefuses), naming only what the globaltools.schemas()serves; a refused name the Agent itself serves stays listed, with one warn line that names it, and the guard is still the refusal (#1445). - Reach (#90): the core's owner policy (
reach.js) is not a middleware.hooks.jsruns it before thebot.sendchain, on approval release and on every deliver that carries a sender or a remote origin; a plugin'salloworbypassnever skips it, and plugins see its denial as{ action: 'deny', by: 'owner-policy' }. Plugins read grants throughflybot.bot.hasGrant/hasAnyGrant,BotView.granted,flybot.grantsand thebot.grantsevent. A group's mention receipt (mentionReceipt) turns a not-shared refusal (cannot message their Bots, #273) into its short invite-or-add hint. flybot.roomApprovals.setProviderlets a plugin (social) attach pending outbound holds to a shared-room view. It does not enable team mode. Anallowverdict may sayautoSent: true(withrecipient) when it skips a hold only because the owner turned Auto-send on; the host then marks an auto-reply's turn Auto-sent (#417). It confers nothing. A remote turn that died is not that hold (#1068):replyRemotesends the host's fixed sentence withvia: 'turn-failed'andmeta.notice: 'turn-failed'(failure: true), and the social plugin allows only those two sentences. A model reply staysauto-reply. Aholdverdict may carryrecipient, who gets the message as the owner knows them (a person's name, trimmed, at most 120 characters); the record keeps it asrecipientNameand the cards name it (#418). A Bot's held sends are also visible to Bot coordination:list_botstags the waiting Bot withN to approve(a count only,src/host/tools.js, #587). Aflybot.approvalsentry withinline: trueand apolicyis not an overlay card: it draws the decision on a held reply's own bubble in the chat (held-reply.js, props{ held, decide, call, actions }), and the overlay skips it. Approval claims (claimApprovals,chatOpened) live inapproval-claims.jsand are re-exported byflybot-slots.js.cleanRemoteis the record boundary for a friend's granted Bot:name,homeNameandownerstrip@(so they cannot look like a mention of another Bot innameMentionschips/previews) and control characters/newlines, then cap at 60 characters. Handles stay on thehandlefield. Optional look fields (color,avatarobject,avatarUpdatedAt) pass through when present so remote chats, group lists, and headers can render a peer's chosen look; missing look stays handle-seeded. Those labels then flow into deliveryfromName(stillcleanName'd and the body fenced) and intolist_botsfriends lines via the remote-chat roster. Group room logs use local team names, not peer labels.- A user's @ of a friend's Bot in a group is answered in that group (#825): the line reaches the Bot as a room line and its reply comes back as one, which is why
mentionReceipt(hooks.js, relay rolemention) posts only what went wrong — why the send was denied, held, or failed — and no "sent" line at all. The receipt's role stays outside the room round trigger (groups.jsopens), so it never opens a round; peer names go throughcleanName. A human line that names a friend's Bot already in that room is not 1:1-relayed as the group's admin (Chief): the room message is the delivery, so Chief's missing grant does not block it (#143). A Chief-originatedbot.sendto an ungranted friend Bot is still deny. A remaining grant deny on a mention that is not in the room says to invite the Bot or add it to the group. flybot.roomshosts a Bot Social face-to-face matching room as a native room (upsertFace/applyFaceLine/ofFace/dropFace/onFaceLine(fn(room, line, { mentions }): for the owner's typed line, the handles its@names, #293) /postNotice/postHuman(quiet; it reads the friend's line for@of a local Bot by label or handle and passescallIds, so only those Bots answer, #287; an optionalmentionshandle list from an attested line is resolved first, #293;attested: true, passed only for a line the server stampstyped_by_owner, lets a plain or@allline start this home's Bots in one capped pass, #295; a plain line is answered by the Bots that judge it theirs,@allis not routed, #299) /postRemote;postHumandraws another member's typed line as a chat bubble from that person, #180). Slotflybot.room.overviewis a block at the top of a group's overview: while joining, a tile grid (avatar above name + owner) with Leave and Enter chat on one row; once open, compact Leave and Auto-send — not Open/Start/Leave together. Slotflybot.room.membersadds rows at the foot of a local group's Members card (not a face or shared room), withhost(endpoint, payload)for core endpoints (#876). Slotflybot.new-chat.actionadds a row to the sidebar + short action menu (after New group chat;label/hintgo through the samet()map). Still not team mode.upsertFacealso takeslead({ handle, picked, canPick, supported }, #476) and a memberrolelabel.- Held sends are owner-bound (#52): a hold stamps server-derived provenance on the record —
requestId, a sha256versionover the request content,origin(via/hop/audience) and the sender Bot'sownerId; message claims (meta, text) never confer authority.flybot/decidepasses authz for any signed-in actor and hooks binds the decision to the sender Bot's owner's session;releaserevalidates and marks recordsstale(changed request, deleted sender/target Bot, a target Bot retired while the hold waited —the target Bot was retired— changed owner —deleteBotinvalidates eagerly; a demoted Chief's sender-side holds are invalidated eagerly onsetMain, not insidestaleReason).release()re-checks immediately before dispatch so an in-flight approve cannot go out after a concurrent invalidation. UnknowninvalidateForfield values throw. The pending list is filtered per actor under member enforcement. The default approval card showssenderName/targetNameand a delegated marker (Approval.origin). - Task lifecycle (#54):
bot.sendstampsmeta.taskIdfrom the working task (rt.taskIdForSend); a held send then callsrt.taskHeld(blocks that task). The owner'sflybot/decidecallsrt.taskDecided(approve resumes, reject cancels);deleteBotof a local Bot callsrt.taskBotDeleted(open tasks fail), and so doesarchive_botwiththe responsible Bot was retired, because retirement means what deletion does to the board;task-create, a plugin'srouteOpenTaskandtask-send-backrefuse a retired owner withNo such Bot, since the last one opens a rework card on that same owner. Incoming holds stay the owner's. The state machine lives intasks.js; hooks only fire those three seams. Clarification andworkBotIdwait on #55.flybot.tasks.onWrite(fn)(#1318) hears the saved view aftertask-createandtask-update. The first object returned may nameownerBotId. The host applies it when it is a different local Bot, records the move, and saves again without calling listeners. A throw is logged and the next listener still runs. Starting a workflow run stays inside the plugin; this seam only moves the card's owner.flybot.tasks.onProjectProvider(fn)(#1696) is the read side a plugin exposes for the Project board: the host calls it with{ hasTask }and reads back{ taskId, runId, nodeId? }refs throughrt.projectRunRefs({ hasTask }), so core never readsworkflows.json; a provider that throws, or names ataskIdthe core task record no longer has, is skipped instead of failing the read. - Activity feed (#241): plugins that want to see where team time goes subscribe through
flybot.on(...)and never to raw DSHsession/event(the maintainer decision on #241). Events:turn.start/turn.end(a Bot's own chatplace: 'chat', its hidden group Session'group', the group's own Session'room', whose turn is one round, or a friend's Bot chat'remote'(#248: its Session runs no model and only carries lines to and from the friend, so those turns are transport, not a team Bot's work;remote: {handle, name}names the friend as the sidebar does (Helper · Bob's Bot,Bob's Chief; a{kind: 'remote'}party carries the samenamewhen the roster knows it), and the friend's line into the chat is anexchange.deliveredwithrole: 'remote')),tool.start/tool.end(paired bycallId, closedinterruptedwhen their turn ends first) andexchange.delivered(a message arrived in a Session's log:exchangeIdas instate.exchanges,replyToof the answer,froma user / Bot / group / friend's Bot). The feed is metadata only: message text, tool arguments, tool output and model output never leaveflybot-feed.js, so a consumer that stores it cannot persist a secret. Strings from outside the team (tool names, remote handles) go throughcleanLabel(which also removesuntrustedfence markers when spaced out or split), error outcomes carry an upper-case code (RATE_LIMIT, elseUNKNOWN) and no message.turn.endcarriesmodel(provider/model, cleaned) when the turn made a model call (#366): the model of the Session's latestrequest/header(DSH logs one only when it changes, so the last one seen applies; before the feed saw one, the Session's ownrequestHeader()). A turn with no step, and every'remote'turn, has none;turn.startnever has one, since the model is known only once the first request goes out. Delivered means "in the recipient's log", not read; the feed cannot see read. Host nudges (retry, resume, deferred and re-sent deliveries) are not deliveries. Not replayed: a listener hears events after it subscribes. Listeners get copies and a throwing one cannot reach the Session or the other listeners. Events fire in a read-only host too, since they only observe. A friend's Bot in a feed entry is named as the sidebar names it,Helper (Bob)(remoteTitle→remoteBotTitle, #384); the approval overlay's recipient uses the same name. One event is not folded from a Session:decision(#396) is fired directly bydecisions.jsthroughrt.fireFeedwhen the host enablesconfig.decisions, carrying{ botId, option, choice, why?, time }for one considered-and-rejected choice. Every new local Bot is announced withflybot.on('bot.created', …): the first Main Bot onboarding creates (on the firststaterequest) and aduplicate-botcopy included, so a plugin that adopts new Bots (the tree root) never misses one;bot.removedfires after a Bot is deleted.
flybot.bot.create({ ephemeral: true }) makes a run Bot for the calling plugin. The host stores hidden: true and ephemeral: { plugin } before the first save. The marker is set only there: fields.set cannot add it, and a spec cannot name another plugin. bot.list, bot.find, flybot/bots and bot.created omit it. bot.get still returns it (hidden, systemPrompt). bot.remove(id) deletes it only when ephemeral.plugin is the caller; any other Bot throws, and a missing id returns false. CreateBotSpec also takes hidden, model, and — for a run Bot only — tools, permission, createdBy and cappedBy. createdBy is the Bot whose tool call started the run; cappedBy lists the Bots whose connector switches cap it (the caller and the Bot that saved the workflow graph), and the host reads each id at call time, so a switch flipped later applies at once and a Bot that is gone stops capping (#1544). tools is stored as stepTools (github grants mcp__github__*). permission is a DSH preset (workspace-write or danger-full-access) applied to that Bot's Session; a failed apply deletes the Bot. A team Bot create ignores both. A run Bot has no visible chat, so an approval card would stall it: a granted tool is allowed, a connector it did not name is refused, and a shell escalation to full access is allowed only when bash, shell or danger-full-access is in stepTools. Covered by core/test/flybot.test.js.
flybot.chatApprovals.setProviderattaches pending Bot Social holds to a 1:1 friend's-Bot chat the same wayroomApprovalsdoes for groups, and to a local Bot for its own held sends (#371). The team view putspendingCounton that bot; the sidebar paints the gold wait dot.flybot.chatApprovals.changed()tells the host the list changed (a hold arrived or was decided), so the badge refreshes at once instead of on the next unrelated revision (#226). Still not team mode.flybot.watched()is true while a FlyBot window on screen reads the team (its roster poll, within the last 8 s; a hidden window does not count, #497). A plugin that polls a remote inbox reads it at its live cadence then, so what arrives reaches the open window within seconds; with no window it keeps its idle cadence. It only observes the client polls and changes nothing.
hooks.jshands a transportapproved: true(never read from the message) when the owner approved that send on a FlyBot card; social uses it to finish Bot Social's own hold with the same click (#258).- A
flybot.session.dockentry may setpolicy(thebyof the approvals it draws; social registerspolicy: 'social') and receives the host translatortandclaim(ids): call it with the approval ids the dock shows so the overlay draws no second card for them (#260). The host also marks the chat each mounted session dock belongs to as open (chatOpened), and the overlay draws no pointer for an approval of the open chat (#271). A dock entry also getsblank: true only while the host is drawing no session header at all — the session has said nothing (session.blank) and nothing has been sent to it (!session.promptAttempted; the host's own condition issession.blank && conversationPhase(...) === "blank", and a sent-to session readsengaging), and a failed first send never turns it blank again. Social's group strip (social-group-strip) stands in for that missing header exactly then and is gone at the first send (#998). The dock itself stands in for a Bot chat's missing header on that sameblankfact, drawingBotChatStripabove the composer with the Bot's name, its role, "No messages yet. Say something to {name}." and a Details button — for a local Bot only (!bot.remote), because a friend's chat has its own empty hint (#1347). The pointer's chat is the approval's room, else the friend's chat it is addressed to, else its sender Bot (#278). - Social registers a second
flybot.session.dockentry,social-room-end(FaceRoomStatus, order 40): the plain ended-room line for a left/closed face room (#191). - Social registers
flybot.settings.page(SocialServerSettings, label Bot Social): the server address, its status and the switch form (#337). The host side issocial/server(a read) andsocial/server-check/social/server-set(writes: blocked on a read-only host and, like every plugin write, while the team has more than one member). - Social also registers
flybot.account.header(MyHandle): my own Bot Social handle plus Copy at the top of the account menu (#181), hidden while not connected. - Social registers three
flybot.new-chat.actionrows (Join by number or link, Start a face-to-face room, Add friend; the two face rows pass which one opened the screen, #1021). Eachruncloses the picker. Copying an invite and seeing your own handle happen inside the Add friend panel (#181). Still not team mode. - A friend's Bot opens Bot settings through core
remote-bot-settings(openSettings('bots', id)): a compact sheet (data-flybot=remote-bot-sheet) withFlyBotDetailsfrom slotflybot.bot.details, not the local Bot editor. The sheet stacks above its scrim like the host settings overlay (#166). - In an own Bot's settings the host frames
flybot.bot.detailsin its titled Sharing card (FlyBotDetails({ wrap })), so an entry draws no title of its own; the friend's-Bot sheet and the details drawer draw the entries without a frame (#406).
flybot.rooms.applyInboundturns a Bot Social inbox envelope into the owner's inbound-share projection;leaveInboundhides those rooms when the invited Bot is removed or its grant is revoked. Face-to-face methods are unchanged. Still not team mode.flybot.rooms.serveInvites({ identify, mint, preview, claim, verify, revoke })registers Bot Social's record of a local room's invite tickets (#884;rt.roomInviteServer, see core-host's "A local room's invite link"); it returns the unregister function. Registering it also runsrt.identifyInviteBotsonce (#899), so a link minted on a room-only Bot gets that Bot's full identity throughidentify.mintanswersundefinedwhen the server has nolocal_room_invites, and a refusal throws an Error whosecodeis Bot Social's.flybot.rooms.seatOpenClaims(opts)reads those claims and seats them without waiting for the accepting knock (#1064;rt.seatOpenClaims).flybot.rooms.ticketPeers()lists the pairs a link opens ({ from, to, scope, ticket }, reach.jsticketPeers), so a transport can let a room share through to exactly those, underscope, and to nobody else off its roster.flybot.rooms.takeLineSeq(roomId)advances that room'sseqthe same way a stored line does and returns the new number (#1126), so a shared-room mention's envelope is a new remote line.list()copies an inbound mirror'ssharedFrom.remoteRoomId,ticketandinviterHandlefor that envelope.
onFaceLinereceives every appended room line, including the owner's typed human lines. Channelsendposts those throughpostRoomMessage(notsessionController.prompt) so they reach this hook immediately; the social plugin sends them through Bot Social as the owner's local room Bot with aFLYBOT-FACE-HUMANenvelope so every human in the room sees the text; incoming envelopes apply asauthorKind: 'human'and the stored body is fenced (#98). A held owner line stays on the in-chat approval card (human text, never the raw envelope) unless face Auto-send is on. Bot replies stay bot-authored. For a Bot line the hook may answer{ held: true, approvalId }when Bot Social holds it; the host then keeps the line off the room's bubbles untilflybot.rooms.settleHeld(approvalId, 'approved' | 'rejected')(#456). The displayed sender is the Bot Socialfromsnapshot (trustedFaceWho); the envelopewhois ignored so a peer cannot impersonate another member. An event with nofromfalls back to the authenticated handle's home label, then Someone. Still not team mode.
- Classic-script plugins cannot import
src/client/mark.js. The host clientregistry()therefore also setsglobalThis.__flybot__.BotAvatar,BotMarkandlookOfto the same components the sidebar / session list / chat header use (configured image / shape / color / emoji, else a stable look from the Bot id or a remote handle). Plugins typically renderBotAvatarat 24px withbadge: false; face-to-face tiles pass 48. A storedcolor/avataron the record still wins, except a remote preset that draws the same look as a local Bot of the same display name, whichBotMarksteps (#1069); a remote Bot without a stored look is seeded byremoteSeed(handle). A face-roommembers[]look overlays the chat record (lookBotForRemote). For the same reasonregistry()setsglobalThis.__flybot__.namesto{ friendBotTitle, friendBotOwner, remoteMentionLabel }fromshared/remote-label.js(typed intypes/flybot.d.ts), so a plugin names a friend's Bot (Chief (SelfB1233)) or its owner exactly as the sidebar and approval cards do (#498), and a plugin's@row inserts the one name a mention wears — the one the bubble and the sidebar preview chip — instead of a bare handle (#967). For the same reasonregistry()setsglobalThis.__flybot__.RoomInvitePreview,invitePreviewOfandinviteCountdownfromsrc/client/room-invite-preview.js(#876): the invite card is one component for the face-room paste flow and the member-invite dialog, and a classic-script plugin renders it instead of keeping a second copy of the markup.kindonly renames thedata-flybothooks (face-link-preview/room-invite-preview), andinvitePreviewOfsays only what the server reply can honestly say (a name cleaned at fixed point, the lead, the seat count, the deadline).
-
flybot.model.ask({ prompt, system?, maxTokens?, model? })is the one pattern for a plugin to make a short model call (a classification, a title, a one-line summary).src/host/plugin-model.jsbuilds it;hooks.jsexposes it per scope asmodel.ask. -
Off unless named. The host config
flybot.modelPluginsis an allow-list; an unnamed plugin gets{ ok: false, code: 'not-allowed' }, and the list is empty by default, so nothing calls a model unless the deployment opts in. -
A per-plugin default. The host config
flybot.modelFor({ '<plugin>': '<provider/model>' }) picks the model that plugin's calls use when the call does not name one (#579): the order isinput.model, thenmodelFor[plugin], then the team default. A plugin the host did not name still runs on the team default, a truthy model in the call always wins, and an entry that is not a non-empty string is ignored rather than passed through, somodelFor: { tree: '' }does not turn a working call intono-model. The team default is not asked for a plugin that has its own entry. -
Capped and safe. Input and system length, output tokens and calls-per-minute are bounded (
MODEL_LIMITS); the call streams through the host's own model selection and returns{ ok: true, text }or{ ok: false, code }— a short token, never provider text (a provider may echo a key). It refuses on a read-only host. It answers with text only: it creates or changes no Bot, room or memory, so a plugin's model call has no side effect the owner cannot see. -
Tests:
core/test/plugin-model.test.js(limits, allow-list, rate, error mapping, and the three-step model precedence of #579) and theflybot.modelcase incore/test/flybot.test.js. -
Host writes through the same field store (#638).
rt.setBotField(botId, plugin, key, value)is the host's own writer for a plugin's fields: it runs the samefieldsFor(plugin).setpath and fires the samechangeevent a plugin'sflybot.fields.setdoes, so a subscriber (the tree, the sidebar) hears a host write exactly as it hears the plugin's own. Core'sarchive_botmarks a Bot retired through it.
flybot.memory.read(botId)returns a Bot's own saved entries ({ text, meta }, capped), andflybot.memory.append(botId, text, meta?)writes one through the ordinary memory path (secret-scanned, size-capped, journaled). It is the seam the tree uses to carry a merged Bot's memory into the one that keeps it.- Off unless named. The host config
flybot.memoryPluginsis an allow-list; an unnamed plugin'sreadreturns[]and itsappendresolves{ ok: false, code: 'not-allowed' }. The list is empty by default. - Bounded and safe: at most 200 entries are read; an entry with a secret or over the entry size is refused;
appendnever throws into the caller (it resolves a shortcode). A read-only host refuses the write. Tests: theflybot.memorycase incore/test/flybot.test.js.
core/test/flybot.test.js, core/test/flybot-client.test.js, core/test/plugin-model.test.js via npm test.
- Reaching into DSH internals instead of using hooks/slots.
- Changing slots without updating types or client tests.
flybot.remote.chats.deliver({ handle, text, name?, home?, human?, ownerName? })(#227):human: trueis the plugin's server-attested "the friend typed this line themselves" (Bot Socialtyped_by_owner, never derived from text) andownerNamethe authenticated owner label; the host then draws that turn's reply as the person. Plugins that cannot attest leave both out and the line stays the Bot's.flybot.bot.send({ from: { kind: 'remote', handle, home?, typedBy? }, ... }):typedByis the plugin's server-attested owner name for a line the friend typed (Bot Socialtyped_by_owner);toRemote(src/host/hooks.js) cleans it and the host stores it as the message source'sremote.typedByso the chat draws the person, not the Bot. Never derive it from text; plugins that cannot attest omit it.- A remote sender's line that reaches a local Bot's own chat through
flybot.bot.sendstamps that Bot'sfriendLineAtin the roster (its row turns unread in a window where the chat is not open) and marks the sender's first lineremote.firstContact(#420,src/host/delivery.js). A refused send stamps nothing. Covered bycore/test/flybot.test.js. - The approval card queue (
FlyBotApprovals) runs all its hooks on every render, before its empty-queue early return: a hook after it makes React throw when the first hold arrives and no card ever draws (#254;core/test/overlay-approvals.test.js).