Sign in as yourself
Use your company login or a linked passkey. The Hub connects that sign-in to your work profile and checks which apps you can use.
| Taken | Size | Note |
|---|
entity=LLC).field=value = exact, field^=prefix = starts with. Matching accounts are hidden from THIS tool only. Empty = no filtering.npx kebab-mcp connect <code>). First time? Setup guide.Your sign-in, apps and notifications work together. Here’s how.
One place to find your tools, move between them and stay on top of what needs you. Here’s what to expect.
Use your company login or a linked passkey. The Hub connects that sign-in to your work profile and checks which apps you can use.
Choose a connected app and the Hub signs you into it. Website links open separately and may have their own login.
The Apps button follows you between tools. The bell brings together Hub notifications, with links back to the work that needs your attention.
Star an app to add it to Your favourites. Search by name or description; on a keyboard, press / to search and Enter to open the first result. Favourites and recent-use hints stay in this browser; they do not sync to other devices.
Open the bell from any connected app. For Hub notifications, you can enable Slack direct messages in the notification panel if your organisation has connected Slack and your account can be matched. Some tools also have their own channels, such as Desk ticket threads.
Your menu reflects the apps your organisation has made available to you. If Request access is shown, use it to explain what you need. Otherwise, contact your IT team. Access to an app does not automatically give you access to every record, teamspace or administrative setting inside it.
Open your profile at the top right to manage the options available to you, including your picture and sign-out. Company-managed profile information comes from your organisation’s directory. If a session expires, sign in again through the Hub; your work stays in the app.
The Hub shares your identity and the directory fields allowed for that app. Your administrator controls additional access, such as profile information, groups, roles and pictures. Each app controls access to its own records. Public features, such as the game leaderboard, have their own sharing rules.
For bundled apps, the Hub issues a single-use ticket valid for 90 seconds, bound to the intended app. That app redeems the ticket with the Hub and creates its own session. External software connected through OpenID Connect follows its configured sign-in flow; ordinary website links receive no Hub sign-in ticket.
Once the Hub records an account or access change, it sends a revocation update. Bundled apps also recheck their access data and deny access when it is 60 seconds old. Changes in an upstream HR or identity system take effect after they reach the Hub. External software has its own session policies.
Opening connected apps requires the Hub. Bundled apps also stop accepting access when their authorisation data becomes too old. Try again once the connection returns; contact IT if the problem continues. A saved menu is a convenience, not a guarantee that an app is currently reachable.
Kebabstack’s Hub and bundled apps run as application services on a Cloud Engine. Your organisation chooses and operates its setup with its infrastructure provider. External websites, identity providers, Slack and optional AI services keep their own hosting and data flows.
Your people, your apps and your backups in one place — on your own cloud engine, infrastructure under your organization’s control, with your chosen platform and operator. How things stand, and what to do next.
Everyone in your company, in one list — typed in here or synced from your HR system or identity provider. Click a person to invite them, change their role, or lock them out of everything at once.
| Name | Source | Status | Can sign in? | Role | Updated |
|---|
| Name | Source | Members | Note |
|---|
Where your people come from. Small company: type them in under People and you are done. Bigger company: let your HR system or identity provider (Okta, Entra, Workday, Personio …) push them here — when someone leaves there, they are out of every app here within seconds.
userName, HTTP Header auth with the source's connection key (bearer token). Entra: Enterprise app → Provisioning → Tenant URL = the address, Secret Token = the key. Users and groups (Okta "Push Groups", Entra group provisioning). Two identity providers? Two sources, two keys — the same address can only be provisioned by one of them, and a group name must be unique across the hub. Attributes that arrive (title, department, division, organization, cost centre, employee number, manager, the work address as city/state/country, and any custom attribute your provider maps into its SCIM schema) show on the person card and can drive an app's exclude filters (Apps → the app → Who may use it → Advanced → Attribute filters, e.g. city^=Remote).The apps your people use. Connect one and it gets three things from the hub: who is signing in, the people directory, and access revocation within 60 seconds in suite apps when someone leaves. You decide who may use it, what it may know, and whether it is on the menu.
| App | Who may use it | What it may know | On the menu |
|---|
Who may use what, for how long, and who checks. Requests come in from your people's menu; temporary access ends by itself; a review asks each app's owner to confirm every entry — and a removal takes effect at once. How it works →
| Who | App | Why | Asked for | When |
|---|
| Who | App | Outcome | By | When |
|---|
| Who | To | Ends | Given by | Why |
|---|
| Name | Apps | Progress | Removed | Due | State |
|---|
When a person leaves, their forms, documents and projects in the connected apps should not leave with them. Pick the person, pick the app, choose who takes over. Also part of the guided offboarding (People → person → Offboard).
Everything that happened in the hub — people added or locked out, apps connected, sign-ins refused, syncs. Newest first. Your audit trail.
The reference for the hub — written for the owner who wants to understand it and for the expert who wants to check it. Plain words on the working pages, precise words here. Numbers on this page are read from the code, not from memory; where the hub falls short, it says so.
Current access rule: every bundled app (desk, assets, watch, trust, forms, contracts, …) refreshes a complete directory every 30 seconds, revoke missing people and fail closed when its request-start age reaches 60 seconds. Pushes are best effort. External IdP changes must first reach the Hub.
hub/tools/archsvg.py: boxes are sized from their text and a checker refuses any layout in which a label touches a box or another label — the diagram cannot drift out of shape when the content changes.| On the pages | Technically | Meaning |
|---|---|---|
| service, service id | canister, canister id | one running program on your engine; every app is one or two of them |
| app / connected app | connector | an app that trusts the hub for sign-in, people and lock-outs |
| what the app may know | lanes | which data about people the hub releases to that app |
| the menu, menu entry | portal, tile | the page your people open; each line on it |
| sign-in key / passkey | passkey via Internet Identity → principal | Face ID, Touch ID, Windows Hello on the person's own device |
| key id | principal | the technical id of one sign-in key (or of a canister) |
| lock out | kill switch / deactivate | bundled app access ends within 60 seconds of the Hub change; data stays |
| sync from HR / sign-in system | SCIM 2.0 (RFC 7643/7644) | the standard Okta, Entra, Google, Workday, Personio … use to push people |
| company sign-in | OIDC + PKCE (Okta SPA, Google web client) | optional; the hub is the client, your provider is the server |
| sign-in for other apps | OpenID Connect provider (code + PKCE, RS256/ES256) | the hub is the server; Grafana, GitLab, Nextcloud … are its clients |
| ask for access · give access for a while · access review | access request · time-boxed grant · access review campaign (IGA) | the three governance moves; all of them edit the one access rule per app |
| AI for your apps | lane ai · hub_aiCredentials | the one AI key of the company, handed to apps that may use it |
| who answers for this app | app owner (connector owner) | the reviewer of that app's access lines; without one, the hub admins |
| who runs this hub | owner / admin / helpdesk | the three staff roles, held by people |
| cloud engine | dedicated Internet Computer subnet | the infrastructure your canisters run on; you choose it, you pay for it |
| kitchen · pantry · recipe | installer canister · its asset canister · a manifest with wasm + frontend files | how new layers get onto the skewer without a terminal |
/scim/v2/Users and /Groups. Since 0.20 every pushing system is a source with its own bearer token (512-bit, generated here, shown once, stored as a hash) and its own scope: it sees, changes, deactivates and groups only the people it pushed itself; optionally it may only provision addresses of its own mail domains. One address is provisioned by one source at a time (a second source gets 409), and a group name stays unique across the hub (a second source pushing the same name gets 409 — rename it in that IdP). Pausing a source refuses its pushes and makes its groups editable here; removing one drops its people like an Okta connection's. Creates, updates and deactivations land in seconds; a SCIM DELETE deactivates and never deletes. Changing an existing userName is a rename of the same person: every hub reference follows the new address in one step (see Identity and roles). Filter support is userName eq only; pagination is not implemented yet. Groups pushed this way are read-only in the hub while the token is active — revoke the token and they become ordinary groups.private_key_jwt, keys generated in-canister) on a schedule: default hourly, owner-configurable, can be off; the scheduler ticks every 5 minutes, so a due run starts within ≤ 5 min. Okta event hooks can deliver deactivations between scheduled runs; the hook is authenticated by a 256-bit secret in its URL path only (Okta's HMAC header is not verified).p_…, minted here, never reused). The lowercased e-mail address is the working key: it is how sources describe a person and how apps show one, and it may change. When it does — a name change at the source, a SCIM rename, an edited local entry — the hub moves roles, sign-in keys, seats, access rules, grants, notifications and the sign-in subject to the new address in one step; apps see the same id under a new address. When an address is handed to a new person, the previous holder's role, keys, seats, grants and connected assistants are dropped and their history is parked as address#id, so nothing transfers to the newcomer — whether the newcomer arrives active or still staged. Two accounts count as the same person only while the holder of the address still has an active account somewhere; a source that wants to move an account onto an address another active person holds is refused (the old address stays, the journal says so). The People page shows the id and the address history on every person card. Display names are never used to merge anything.hubRole) if you grant them the roles lane. Roles are enforced per method in the canister, not in the UI.tools/setup-code.py) or by the canister's controller — there is no open first-visitor window. The claim sets the company name and the first owner.
.well-known/ii-alternative-origins) — set a custom domain up before the first sign-ins; retrofitting orphans accounts.email_verified=true. Unverified email addresses are refused. Rate limit on the exchange: 300 attempts per 5 minutes for the whole hub (transient, resets on upgrade) — it protects the provider's quota, not seats.
hub_usesGroup, 10 s timeout, apps without the method are skipped).
example.com (departments, managers, one already locked out), three groups and two menu entries, and remove them again in one click. Honest limits: sample people are ordinary local entries — they do appear in the directory your connected apps receive and count on Home; they can not get an invite (the hub refuses), so nobody ever signs in as one. Remove the sample before real people arrive.
hub_ping and hub_manifest (name, version, the lanes it needs and wants), pins the needed lanes on the skewer, lets you grant more, choose who may use it and create the bound menu entry — one call at the end; apps without a manifest are connected by hand. Other software with OpenID Connect: pick the preset, paste the address (details). Just a link: name and address. Both sides verify each other by canister principal — no shared secret, no API key. Edit opens one panel per app: who may use it, what it may know, on the menu (switch + address), technical (service id or client id, redirect URIs, secret, remove).
hub_aiCredentials, Settings → AI) for its own calls to the vendor — one key for the suite, rotated in one place, usage counted per app. External (OpenID Connect) apps never get it.groups = "a;b", hubRole) only when granted. A per-person group lookup (groupsOf) is answered only for people inside the app's own population.checkAccess, the directory the app receives, avatars, notifications, and which menu entries a person sees. Source scope and attribute filters remain as advanced knobs underneath.connectorDirectory) on their own timer — the SDK example pulls every 15 minutes — and on unknown sign-ins. With the push lane the hub additionally pushes it into hub_upsert every 15 minutes, for directories up to 5 000 people; larger ones are pull-only. The push loop iterates apps with an explicit lane record — an app connected before lanes existed is pushed only after you saved its lanes once. Push failures are not journaled yet.hub_deactivate, one call per app, 30 s timeout) — best effort, no retry, not journaled. An app that was unreachable in that second learns of it on its next checkAccess or directory pull, which is why the SDK guide asks for both. Offboarding is one guided flow: lock out → unlink keys → hand over their work (apps that hold owned objects implement hub_ownedObjects / hub_reassign).
hub_notify (needs the notify lane; admins may call it for tests). The hub stores it in the person's inbox (the bell on the menu) and — if Slack delivery is switched on, a bot is configured and the person has not opted out — sends a Slack DM. Payload policy: title + deep link only — title ≤ 200 characters, https URL ≤ 500 — content stays behind the app's own sign-in.dedupeKey within 48 h is silently dropped (the call still returns ok).mountTopbar in the SDK's hub-client.js) reading the same inbox. Apps get a suite token when they redeem a sign-in ticket — read-only, 12 hours, good for exactly six calls (unread count, list, mark read, your apps, your name, your picture) and nothing else — and their topbar talks to the hub with it directly; no app ever proxies or sees another app's notifications. Freshness: the unread count every 30 seconds, the full list when the bell opens, a refresh whenever the tab comes back and after every action. An ended token shows reconnect, never a stale zero.
kebab-mcp server exchanges it for a personal token (30 days, at most ten per person, revocable in the menu and by staff under Settings → AI; the hub stores only a hash of it). With that token the hub mints app tickets exactly as the menu does, so the assistant signs into desk, assets, forms, trust, watch … as that person, with that person's rights — the same lease, the same lock-out, the same access rules, and an app that is off the menu is off for the assistant too. When a person's address changes, their assistants follow; when an address is re-issued to someone new, the previous holder's assistants are disconnected. Nothing is granted that the person could not do by hand..did with its documentation) and call it; a few ready-made tools cover the common questions — my tickets, file a request, who has this device, who is this colleague (exactly the people the person's own apps show in their pickers), how are our domains. Every session an assistant opens is journaled (kind assistant) with the assistant's name; the chat client asks before it acts. Switching the lane off disconnects every assistant for good. Setup for people and the contract for developers: docs/agent/mcp.md.
check-sdk fails when an app ships a different copy of the SDK's client or design tokens.
sub, email, email_verified, name, given_name, family_name, preferred_username; job profile → title, department, division, organization, city, countryCode, managerName, employeeNumber; groups → groups; hub role → hub_role), the access policy decides who may sign in, and a lock-out refuses the code, the token and userinfo within seconds. Where it shows up: in the Apps list like every other app (type "other software"; Edit → Technical holds client id, redirect URIs, secret) and on the menu as "signed in" — opening the entry opens the app, whose login sends the person through the hub without a second prompt (consent once).response_type=code), PKCE S256 supported and required for public clients. The app sends the browser to <issuer>/oidc/authorize (the backend forwards it to the sign-in view on the frontend); the person signs in with their passkey (or existing session), sees one consent card — which app, as whom, which lanes — and is sent back with a 32-byte single-use code bound to client, redirect URI, nonce, PKCE challenge and person, valid 60 s. The app's backend posts it to <issuer>/oidc/token (client_secret_basic, client_secret_post or none + code_verifier) and receives an id_token (15 min; iss, aud, iat, exp, auth_time, nonce, amr:["passkey"] + claims) and an opaque access token (1 h) for /oidc/userinfo. Discovery at <issuer>/.well-known/openid-configuration, keys at /oidc/jwks. All HTTP endpoints run as update calls (consensus, no certification gap); the token endpoint allows 600 successful exchanges per client per 5 minutes (what bounds signing work — secrets and codes are 256-bit, guessing is not the threat). Codes and access tokens are stored under their SHA-256, so a state dump holds no live credential; a code that arrives with a verifier although it was issued without a challenge is refused (PKCE-downgrade protection); the token endpoint refuses repeated parameters and mixed client authentication; the sign-in page refuses to work inside another site's frame (frame-ancestors 'none' + a runtime check), honours prompt=consent/login by always asking, and rejects prompt=none. Failed exchanges (wrong secret, unknown code, PKCE failure, foreign code) are journaled, throttled per client.sub is an opaque random id allocated once per e-mail address — because the e-mail is the hub's identity. Consequence, stated plainly: an address re-issued to a new hire inherits the old subject at every app unless the old person was purged from the hub first (purging retires the subject); an e-mail change gives a new subject. amr says passkey for passkey sessions and fed for company-SSO sessions.end_session_endpoint or back-channel logout — an app's own session lives as long as the app decides, the same limit Okta has —, max_age is ignored and prompt=login cannot force a fresh passkey (it asks for consent instead), no picture claim, no PS256/HS256, the ES256 key cannot be rotated yet, no dynamic client registration ever, no SAML ever. SCIM out (the hub provisioning people into other apps) is the planned second step.
vaultAuth(principal) — only the configured vault may ask, only owners pass.kitchen/art/<id>.png): it shows on the kitchen card and becomes the app's menu picture after install. Limits: the vault cannot protect itself (it holds nothing worth restoring); snapshots stay inside the platform (no download yet, see roadmap); locks self-expire after 40 minutes; journal keeps 1 000 actions, shows the newest 200; a failed scheduled run is visible on Home and under Schedules but is not yet pushed as a notification.
recipes/index.json, produced by kitchen/tools/pack-recipes.py from the repo). On Install the kitchen creates two canisters (controllers: yours, the kitchen, the vault), installs the backend (directly up to 1.9 MB, through the management canister's chunk store above), installs the asset canister and copies the frontend files in — patching placeholders such as the backend id and the hub URL — calls the app's setHub, registers it in the hub (kitchenConnect: exactly the lanes the app's manifest needs, everyone may use it, a bound menu entry), sets the console labels plus PUBLIC_CANISTER_ID:* and registers both canisters in the vault. A failed install leaves a "failed" record with "Remove leftovers" (stops and deletes the half-installed canisters). On Update it takes a vault snapshot (mandatory when a vault is configured), upgrades the backend keeping memory (wasm_memory_persistence = keep) and refreshes the frontend files, dropping stale encodings and keys. Up to date is decided by the running module hash versus the recipe's sha256, never by version strings. The hub itself can be updated the same way once the kitchen is a co-controller of the hub's two canisters (a one-time add-controller, Setup (IT) prints it). Apps deployed by hand are adopted: the kitchen checks it controls both canisters and that the backend's hub_ping equals the recipe id (services: the running module hash equals the recipe's).vault.setHub / setKitchen, hub.bootstrapWire), registers the hub in the vault with a default schedule (one backup a day at 03:00 UTC, keep 3 — the vault accepts this from the kitchen only where no schedule exists yet; owners change it under Backups → Schedules) and labels everything for the Console — thirteen visible steps. The hub is born protected: a 256-bit claim code is planted as its KEBAB_CLAIM_CODE environment variable at creation, before any code runs, manual Hub deployments also require a code or controller authorization; only the person who cooked sees the code (once, via a certified call) and the hub's claim form takes it from the URL fragment and scrubs it. Cooking requires the operator’s KEBAB_SETUP_CODE; there is no open first-hour window. After a reload the same login can recover its accepted job and claim link. Install = one icp deploy (or one uploaded bundle), everything else in the browser.kitchenAuth) for every action; the kitchen's platform controllers bypass that. The kitchen is a controller of everything it installs — the second most privileged canister after the vault — and journals every job. One job at a time; progress is polled step by step from the UI, each step with its exact error text if it fails..icp bundle upload is experimental in icp-cli. Recipes travel with the kitchen deploy; there is no online catalogue yet. The kitchen cannot update itself. Verified on a cloud engine on 2026-09-03: canister creation and installation from a canister work without cycles.
| Material | Where | Exposure |
|---|---|---|
| Invite codes | 256-bit from IC randomness, in the invite link | single use, 7-day expiry, revocable; re-minting replaces the old code |
| SCIM bearer token | 512-bit from IC randomness, held by your HR / sign-in system | shown once at generation; rotatable and revocable under Sources; wrong token → 401; the only authentication on that public HTTP path (no rate limit) |
| SSO session tokens | 512-bit from IC randomness, stored in canister state | bearer token in the person's browser; 8 h; ends on logout; refused after lock-out; no admin-side revocation list |
| App tickets | minted per click, bound to person + menu entry | 90 seconds, single use, redeemable only by the bound app (unbound entries: any connected app); transient |
| OIDC provider keys sign-in for other apps | RSA-2048 (resumable in-canister prime search from IC randomness) + P-256, canister state | never leave the canister; apps fetch public JWKs from /oidc/jwks; owner-rotatable, previous RSA key served for 24 h |
| OIDC client secrets | 256-bit from IC randomness, shown once | hub stores the SHA-256 fingerprint only; rotatable per client; public clients have none (PKCE) |
| OIDC codes & access tokens | 256-bit, transient maps with TTL sweeps | code 60 s single-use, bound to client + redirect URI + PKCE + person; access token 1 h; both refused after lock-out; ≤ 1 000 codes in flight |
| ES256 signing keys Okta pull | generated in-canister from IC randomness | never leave the canister; Okta gets the public JWK only; rotatable |
| Event-hook secrets Okta pull | 256-bit, in the hook URL path | known to your Okta admins only; rotatable; wrong secret → 403; no HMAC verification of Okta's header |
| Okta API client secret Okta pull, fallback lane | canister state, write-only | only if you choose client_secret_basic instead of private_key_jwt; masked in the UI |
| Google client secret optional | canister state, write-only | masked in the UI except its last four characters; Okta SSO uses PKCE without any secret |
| AI API key optional | canister state, write-only | does leave the hub: apps with the ai lane fetch it via hub_aiCredentials and send it to the AI vendor from your engine; the browser never sees it; owners set and rotate it under Settings → AI |
| Slack bot tokens & signing secrets optional | canister state | do leave the hub: apps you assign to a bot fetch them via hub_slackCredentials so that one rotation here reaches every app |
| Also in canister state | the people directory and org profiles, group memberships, notifications (90 days), avatars, OAuth client ids, the vault id, the journal | |
is_replicated=false), because auth codes and DM sends are single-use and would otherwise fire once per replica. Consequence: a malicious node on the engine could in principle forge an outcall response (a forged Okta pull could flip statuses, subject to the lock-out-wins rule). This is the platform trust base of everything on the engine, not specific to this app..well-known/ii-alternative-origins. Internet Identity is a dependency for passkey sign-in (a public system canister, not a vendor server).tools/setup-code.py. Without a configured code only the actual canister controller can initialize the backend. A public URL does not confer ownership.vaultAuth, kitchenAuth) plus their own platform controllers, journal every action, and hold no business data — but they are the two canisters to guard most.ssoExchange: input size caps, https-only redirect URI, global rate limit (300 / 5 min).password, schemas, id, meta, active, userName, groups, x509Certificates are never stored; everything else in the body is kept) — forwarding to apps stays whitelist-only.vaultAuth, only owners pass it, every vault action journaled; restore takes a safety snapshot by default.| What | Value | Note |
|---|---|---|
| Hub journal | trimmed to the newest 500 once it passes 800; UI shows 250 | older history is gone; no export yet |
| Vault journal | 1 000 kept, 200 shown | |
| Notifications | 120 per app per 5 min · 200 per person · 90 days · 48 h dedupe | title ≤ 200 chars, https URL ≤ 500 |
| SSO exchanges | 300 per 5 min, hub-wide | transient window |
| Directory push | ≤ 5 000 people, every 15 min | larger directories: pull only |
| Groups / bulk edits | ≤ 500 member changes per call · 60-char names | |
| Avatars | ≤ 400 KB each · at most 5 000 pictures hub-wide | count cap, no byte-total cap |
| Snapshots | ≤ 10 per canister (platform); keep 1–10 | count towards the canister's storage |
| Public HTTP paths (SCIM, hooks) | no rate limit | authentication by bearer / URL secret only |
moc --stable-compatible guards every deploy. Transient state is lost: open app tickets, sync guards, rate-limit windows, the Okta bearer cache. Timers re-arm automatically. Vault locks are persistent and self-expire after 40 minutes. Take a snapshot before every live upgrade — that is what the vault is for.
picture claim, ES256 key not rotatable, no SCIM out (provisioning people into those apps) yet, never SAML.userName eq filtering, no externalId echo. A userName change is accepted as a rename of the same person (0.17); an address another active person already holds is refused..icp bundle as the documented install path (today: one icp deploy), an online recipe catalogue with hash verification, update notifications on Home, kitchen self-update.main.mo into modules before the public launch; journal split + export; SCIM pagination and filters; notification retries; vault download and failure notifications; one-person view.
hub_deactivate the moment someone is locked out; apps additionally gate every session with checkAccess. The full playbook (manifest, tickets, directory cache, notifications, ownership hand-over) is onboard-app.md above./sdk/ — so a developer, or a coding agent, fetches the current contract from the hub it is about to talk to, never from a stale copy. | File | What it is | |
|---|---|---|
| kebab-hub.mo | the Motoko module mo:kebab-hub: typed hub interface, ticket sign-in, sessions, directory cache, contract gate, manifest type | Raw |
| hub-client.js | the browser client: ticket pick-up from the URL, jump back to the menu, session store, initials | Raw |
| onboard-app.md | the prompt-ready onboarding guide — paste it into any coding agent, it wires the app and runs the acceptance checklist | Raw |
| mcp.md | AI assistants on the hub: how a person connects one, the assistant plane of the hub, what makes an app usable from the chat, threat model | Raw |
| hub.did | the hub's live Candid interface, exactly what this backend exposes | Raw |
| README.md | SDK readme: backend in five steps, frontend in three lines, the rules that keep the suite coherent | Raw |
Fetch <this hub's URL>/sdk/onboard-app.md and /sdk/kebab-hub.mo, then wire my app to the hub at <backend canister id>.sdk/) and is released with it. Two checks run before every hub deploy (hub/test/run-smoke.sh): sdk/tools/check-sdk.py compares every hub method the SDK promises with the hub's live Candid — name, arity, query vs. update — and refuses a deploy whose served /sdk/ copies differ from the sources; hub/tools/sync-sdk.py refreshes those copies and stamps /sdk/VERSION. Contract changes are additive: methods are never renamed or removed, new ones are optional for apps.hub_ping() returns the recipe id, setHub(Text) is controller-gated, the backend takes no init argument, dist/ carries the placeholders __BACKEND_CANISTER_ID__ / __HUB_URL__. Then one entry in kitchen/tools/pack-recipes.py — onboard-app.md § 10 has the table.onboard-app.md in the table. Deploy discipline for every layer: moc --stable-compatible against the committed .most, a vault snapshot, then icp deploy. Stable variables are append-only; never --mode reinstall a live canister.
The pantry. Snapshots of the hub and of every connected app — taken now or on a schedule, restored in two clicks when an update goes wrong. Owners only: the vault holds the keys to everything it protects.
| Service | Protected | State | Snapshots | Schedule |
|---|
| Service | Plan | Keep | Last run | Result |
|---|
| When | Who | What |
|---|
-f skips the confirmation prompt, which otherwise swallows the run); the vault becomes a co-controller — you keep yours. Then Re-check all on the Services tab.icp canister status.Where new layers go onto the skewer. Pick a recipe, press Install — the kitchen creates the app's two services, installs the code, wires it to the hub, puts it on the menu and hands it to the vault. Updates take a snapshot first. Owners only.
| When | What | State | Steps |
|---|
| When | Who | What |
|---|
-f skips the prompt.Your company's name and logo, who runs this hub, and — for the IT-minded — Slack delivery and company single sign-on.
| Person | Assistant | Connected | Last used | Uses | Until |
|---|
| App | Lane AI | Calls reported | Key fetched |
|---|
hub_notify (same caller gate as redeemTicket), the hub stores it in each user's portal inbox and — when enabled here — DMs them on Slack. Payload policy: title + link only, content stays behind the tool's SSO. One bot per Slack WORKSPACE: delivery finds the recipient automatically in the right workspace.| Bot | Workspace | Token | Signing secret | Status |
|---|
chat:write + users:read.email (add intake scopes if tools will reuse this bot for Events intake) → Install → paste the xoxb token here. The token is verified against auth.test on save. Users opt out of DMs in their portal bell — the inbox always works.hub_slackCredentials. Tools stop owning Slack config: rotate a token here and every assigned tool picks it up on its next hub pull.| When | To | App | Title | Slack |
|---|
| ID | Name | Type | Client ID | Status |
|---|
| Person | Role | Status |
|---|