← Telemachus

Design — decisions

Kickoff Decisions

The decisions needed to start building, compiled from the Decisions to confirm sections of the five design docs. Each has a recommendation (bold) and an ID so choices can be recorded tersely.

Legend: 🔑 = genuinely gates kickoff (decide now). Everything else has a safe default that can be revisited when we build that subsystem.

Fast path: accept all recommendations, or override by ID (e.g. "defaults except RBAC‑2 and SCHED‑5"). Choices get recorded in the Decision log at the bottom.


A. Tenancy (cross-cutting)

ID Decision Options — rec Why it matters
🔑 TEN Isolation boundary Team = boundary, single implicit org · vs multi-org now Changes every isolation check + whether org_id exists in schema. Additive later is cheap; retrofitting multi-org isn't.
🔑 TEN‑2 Multi-org tenancy (slice 45) Org above team, behind a flag · vs instance-per-tenant only ⚠️ Supersedes TEN. "Additive later is cheap" was the bet TEN made — TEN‑2 is that addition, cashed in.

B. RBAC & Teams

ID Decision Options — rec Why / when
🔑 RBAC‑1 Built-in roles owner / admin / member / viewer (+guest later) Seeds the roles table + every permission check.
RBAC‑2 Custom per-team roles in v1 Yes (cheap given the tables) · vs built-ins only Low impact; tables support either.
RBAC‑3 Cross-team sharing Within-team shares only in v1 · cross-team later Scopes the resource_grants check.
🔑 RBAC‑4 API token scope model issuer-perms ∩ explicit scopes · vs fixed catalog Defines token issuance + enforcement; hard to change later.
🔑 RBAC‑5 Operator vs owner Distinct instance operator · vs first-team-owner is operator Shapes first-run bootstrap.

C. Persistence (gates the first build)

ID Decision Options — rec Why / when
🔑 DB‑1 Migration mechanism Thin runner in db-kit: ordered Racket steps, per-dialect SQL, schema_migrations table · vs raw SQL files · vs external lib First slice creates tables; this keeps sqlite→Postgres a swap.
🔑 DB‑2 Primary key strategy UUID (text) · vs integer autoincrement UUIDs survive federation/merge and avoid sqlite/pg sequence divergence; touches every table.

D. Quotas (can ride defaults — built after RBAC)

ID Decision Options — rec
QUOTA‑1 v1 metered dimensions AI-first: tokens / requests / concurrency / rate (defer storage, tasks)
QUOTA‑2 On exceed Budgets → hard reject; rate/concurrency → enqueue-and-delay
QUOTA‑3 Windows Daily + monthly budgets, 60s rolling rate; UTC (team override later)
QUOTA‑4 Team vs user min(team, user) caps, meter both
QUOTA‑5 Token estimate Heuristic (chars/4) in v1, tokenizer later
QUOTA‑6 Default limits Conservative default policy + small/med/large presets

E. AI Workload Scheduler (can ride defaults — built after quotas)

ID Decision Options — rec
SCHED‑1 Executor kinds v1 local-model only, contract ready for remote-model
SCHED‑2 Job requirements {model, est_tokens}, add compute class when CPU/HPC land
SCHED‑3 Remote transport Defer — interface only (RI is local; no wire yet)
🔑 SCHED‑4 Job durability Persisted jobs table from the start · vs in-memory (mild gate: schema)
SCHED‑5 Fairness Priority + FIFO in v1 (policy seam ready for weighted-fair) · vs weighted-fair now
SCHED‑6 Governor scopes global + per-executor + per-team v1; per-user later
SCHED‑7 Preemption Cancelable always; preemptible later
ID Decision Options — rec Why / when
🔑 LOC‑1 Catalog format ICU-MessageFormat in JSON · Fluent · gettext PO Defines the localizer + CLI + every catalog file. ICU = power + ubiquity; Fluent = strongest for complex l10n; PO = deepest tooling.
LOC‑2 v1 surfaces enforced UI + API + tool descriptions first; emails/docs/plugins phased Scopes the bare-literal scanner.
LOC‑3 Message ids Named namespaced keys (documents.editor.save) · vs source-hash Catalog maintainability.
LOC‑4 Release gate Advisory in v1 (only en 100% is hard-required) · vs coverage-blocking now How strict CI is at launch.
LOC‑5 AI drafting Opt-in per locale, via a new translation model role (fallback utility) · vs on-by-default Auto-draft behavior + which model.
LOC‑6 Doc generation Separate follow-up design doc · vs fold in now Keeps localization doc focused.
LOC‑7 Default locale owner Instance operator (instance:manage) + an off switch for negotiation · vs per-team · vs browser-only Decides what an anonymous visitor is answered in. Browser-only was the old behaviour and made a Japanese instance impossible.

What actually gates kickoff

Only the 🔑 rows: TEN, RBAC‑1/4/5, DB‑1/2, plus LOC‑1/2/3 if we run the localization slice early. The rest can ride defaults and be confirmed when we reach that subsystem.

Proposed first two build slices (once 🔑 are set)

  1. RBAC + persistencedb-kit migration runner + users / teams / memberships / roles / role_permissions / api_tokens / resource_grants / audit_log tables + AuthzService, wired into the engine's #:exec permission check.
  2. en externalization + telemachus-localize CLI — mostly independent; delivers the "flag unlocalized strings in CI" win early and proves the dogfood loop.

Decision log

Status: LOCKED 2026-08-13. → default = the recommendation above was accepted.

ID Recommendation (short) Chosen
TEN team-boundary, single org ⚠️ superseded by TEN‑2 (was: one legal entity, multitenancy a non-goal) 1
TEN‑2 org above team, behind TELEMACHUS_MULTITENANT ✅ built (slice 45) — several companies on one instance 2
TEN‑2a org admin manages but does not read team data ✅ built — least privilege; a company admin who needs data joins the team, audibly
TEN‑2b username instance-global (email); teams.slug per-org ✅ built — keeps /api/login unambiguous with no org selector
TEN‑2c a user belongs to exactly one org ✅ built — enforced at the add-member! seam
TEN‑2d per-org branding / hostname routing ✅ built 15 Sep 2026 (slice 68) — orgs.domain (superadmin-assigned, unique) picks the company from the Host header; branding:<org-id> under instance_settings; unset = the instance's, whole
TEN‑2e per-org model endpoints (BYO inference) ✅ built 15 Sep 2026 — executors.org_id (PULL‑6): POST /api/org/executors binds one to the caller's company, offered only to its teams
TEN‑2f provisioning is an API operation; explicit slug = natural key (409 on re-run), derived slug suffixes ✅ built — a pipeline must converge; a silent duplicate company is worse than a refused call
TEN‑2g no DELETE /api/orgs — suspend is the terminal API state, erasure is a SQL maintenance procedure ✅ decided — the cascade spans teams, users, tokens, blobs and audit
TEN‑2h cross-team reading is the org_reader role holding org:read-data (team-visible only; org:* does not imply it) ✅ decided 15 Sep 2026 — the downstream's "org scoped data routes" without reversing TEN‑2a: reading a company's data is a deliberate, auditable assignment
RBAC‑1 owner/admin/member/viewer ✅ default
RBAC‑2 allow custom per-team roles ✅ default
RBAC‑3 within-team shares only (v1) ✅ default
RBAC‑4 token = issuer-perms ∩ scopes ✅ default (first option)
RBAC‑5 distinct instance operator ⚙️ amended — first team owner bootstraps as operator, but operator is a distinct super-admin tier 3
DB‑1 migrations runner in db-kit ✅ default
DB‑2 UUID (text) PKs ✅ default
QUOTA‑1 AI dims first ✅ default
QUOTA‑2 reject budgets / delay rate ✅ default
QUOTA‑3 daily+monthly+60s, UTC ✅ default
QUOTA‑4 min(team,user), meter both ✅ default
QUOTA‑5 chars/4 estimate ✅ default (assumed — not explicitly called)
QUOTA‑6 default policy + presets ✅ default
SCHED‑1 local-model only ✅ default
SCHED‑2 {model, est_tokens} ✅ default
SCHED‑3 defer remote transport ✅ default
SCHED‑4 persist jobs table ✅ default
SCHED‑5 priority+FIFO (v1) ✅ default
SCHED‑6 global+executor+team scopes ✅ default
SCHED‑7 cancelable, not preemptible ✅ default
LOC‑1 ICU-JSON catalogs ✅ default
LOC‑2 UI+API+tools first ✅ default
LOC‑3 named namespaced ids ✅ default
LOC‑4 advisory coverage (v1) ✅ default
LOC‑5 opt-in AI draft, translation role ✅ default
LOC‑6 separate doc-gen design ✅ default
LOC‑7 instance operator sets the default locale, and may turn per-request negotiation off ✅ built — Admin › Localization; the sign-in screen belongs to no team and cannot be configured by the visitor
ONB‑1 instance-per-tenant (hosted) ✅ built (slice 19)
ONB‑2 provider auth via provision token, no operator user ✅ built
ONB‑3 both /api/provision + boot-env seeding ✅ built
ONB‑4 magic-link activation ✅ built
ONB‑5 2FA optional at activation ✅ default
ONB‑6 72h activation TTL, resendable ✅ built
ONB‑7 suspend = 402 + read-only, data retained ✅ built
ONB‑9 the form is the config: required is enforced from it, an unconfigured field is never demanded, and email is the one structural field ✅ built — both halves were bugs; validation vocabulary is required/digits/minlength/maxlength and deliberately not a regex on a public endpoint
ONB‑8 funnel copy localizes via an i18n overlay on the experience document; translation is presentation only ✅ built — an overlay matches fields by key and takes only label/options, so the submitted body and the anti-abuse config are identical in every language by construction
ONB‑8 30-day retention then deprovision + export ⏳ control-plane
WF‑1 workflow spec is the contract; define-workflow compiles to it ✅ decided 4
WF‑2 binding sublanguage frozen (references + fixed predicates, no eval) ✅ decided
WF‑3 borrow CNCF Serverless Workflow vocabulary, claim no conformance ✅ decided
WF‑4 durability unit = one scheduler job per step ✅ decided
WF‑5 human approve gate in v1 ✅ decided
WF‑6 visual interface read-only at v1 ✅ decided
WF‑7 workflows:{read,write,run}; org admins get read only (TEN‑2a) ✅ decided
WF‑8 plugin steps run in process, as tools do ✅ decided — revisit as hardening
WF‑9 the spec is a public contract (documented, versioned, publishable) ✅ decided
WF‑10 unknown fields in a submitted spec are rejected ✅ decided

See saas-onboarding.md for the full flow. ONB‑2 refines the doc's "operator service token" to the simpler provision token so the seeded instance holds exactly one user (the owner); provider actions (provision/suspend/resume) authenticate with that per-instance secret.

Documentation generation (DOCGEN) — decided 11 Sep 2026: recommendations adopted, see documentation-generation.md

# Decision Recommendation · alternatives Why it matters
DOCGEN‑1 Where generated docs live Committed under docs/reference/, drift-gated in CI · vs built at release · vs on demand The PR that changes a tool shows the doc change.
DOCGEN‑2 Making routes declarable A declarative route table route dispatches over · vs annotating the cond · vs regexing it Only a table gives permission, public-ness and description a home CI can assert.
DOCGEN‑3 Permission descriptions Beside the declaration; a missing one is a build error · vs a parallel table A parallel table drifts.
DOCGEN‑4 What gets localized Prose only, doc. namespace, a JSON surface for extract · vs whole pages as documents Structure is identical in every language.
DOCGEN‑5 Rendering Plain Markdown, no site generator · vs an HTML site now No new dependency; readable on GitHub today.
DOCGEN‑6 Determinism Byte-identical output from identical sources The drift gate is meaningless otherwise.

Knowledge graph (KG) — decided 11 Sep 2026: recommendations adopted, see knowledge-graph.md

# Decision Recommendation · alternatives Why it matters
KG‑1 Entity types Open vocabulary + shipped starter set; plugins may extend · vs closed schema · vs fully open Closed refuses real things; unbounded is unqueryable.
KG‑2 Scope Team-scoped, org gate at step 0 · vs instance-wide A company's knowledge is not the product's.
KG‑3 Fact visibility Visible if ANY source mention is readable; snippets filtered per row · vs all sources must be readable All-sources hides a public fact because a private note repeats it.
KG‑4 Dedup (type, name_norm); no cross-type merging in v1 · vs embedding-based entity resolution Cross-type merging is where knowledge graphs die.
KG‑5 Query language None; hops ≤ 2 from a named entity · vs Cypher/Gremlin/SPARQL No dependency, no parser, no injection surface, no demand.
KG‑6 Visualization Lists with citations; hand-drawn SVG neighbourhood later if used · vs a graph-viz library now A dependency and a week of tuning before anyone asks a question.
KG‑7 Extraction model utility role, opt-in per team (LOC‑5 precedent) · vs always the chat model Extraction is bulk and cheap-model-shaped. ✅ built 15 Sep 2026 (slice 67): domain/ai/roles.rkt, PUT /api/model-roles; the pipeline and the Manager's drafts share the role.

Document sharing (DSH) — decided 12 Sep 2026, see document-sharing.md

# Decision Recommendation · alternatives Why it matters
DSH‑1 What a person picks when sharing Three capabilities (view / edit / manage) that grant permission sets · vs permission strings · vs a boolean Explainable in one sentence; the grant row stays exact.
DSH‑2 Who may re-share Only manage · vs any viewer Keeps the set of people who can widen access small and auditable.
DSH‑3 Expiring grants v1, optional expires_at, expired rows kept · vs later · vs never Links already expire; a grant should not outlive a contract.
DSH‑4 Groups Deferred; user + team principals · vs build groups now A later principal type changes nothing here.
DSH‑5 Derived documents Inherit visibility + grants at creation, then independent · vs live-linked · vs private-by-default The only rule explainable in a sentence.
DSH‑6 Cross-team sharing Inside an org only; the org gate already guarantees it Cross-team without a tenancy hole.

Document workflows (DWF) — decided 12 Sep 2026, see document-workflows.md

# Decision Recommendation · alternatives Why it matters
DWF‑1 Where triggers fire Inside repo-put!, after the version commits · vs per-endpoint hooks One seam covers console, shim and S3; three hooks drift.
DWF‑2 Whose principal a triggered run uses The uploader's · vs the trigger creator's · vs a service account A run reads only what its uploader could; outputs are owned by someone real.
DWF‑3 Re-firing Once per version; derived documents do not re-trigger unless opted in · vs fire on every write Otherwise a pipeline runs itself forever.
DWF‑4 Where outputs live Repository documents with a provenance row · vs blobs in run state Shareable, versioned, searchable; provenance is a click.
DWF‑5 Extraction output Validated against a caller-supplied JSON schema; refused on mismatch · vs accept and flag A wrong extraction that looks finished is worse than a failed step.
DWF‑6 Form rendering Markdown/HTML v1, DOCX via template; PDF via format: "pdf" (pandoc → tectonic, slice 68) · vs a PDF renderer in-process The renderer is the two tools the e-book already pinned; looked up at call time, so a box without them refuses that one call by name.
DWF‑7 Manual vs automatic Same run path; "Run workflow…" ships first · vs triggers only Test by hand, automate the same code.
DWF‑8 The first-user path A shipped doc-pipeline plugin, configured per trigger · vs bespoke per customer One workflow, many configurations.

Pull-model executors (PULL) — decided 15 Sep 2026, see pull-executors.md

# Decision Recommendation · alternatives Why it matters
PULL‑1 Where pull work lives The existing jobs table, claimed over HTTP · vs a separate queue One queue: one cap, one quota, one cancel, one audit.
PULL‑2 Worker identity An API token scoped jobs:execute, bound to an executor row · vs a new credential type · vs mTLS Scopes, expiry and revocation already exist.
PULL‑3 Lost workers Leases + heartbeats; expiry re-queues with a bounded attempt count · vs run-once-and-fail A vanished host must neither lose nor double-run a job.
PULL‑4 Placement Capability match (kinds, models) at claim · vs named routing only SCHED‑2 gets a consumer.
PULL‑5 Synchronous model calls Enqueue infer.chat and wait, bounded by the caller's lease · vs make tools asynchronous Every existing tool keeps working.
PULL‑6 Per-org executors (TEN‑2e) executors.org_id; null = instance-wide · vs instance-wide only The org gate, again.
PULL‑7 Transport HTTPS long-poll, JSON · vs WebSocket · vs a bus Works through NAT and a tailnet; nothing to deploy beside the server.
PULL‑8 Token formats Opaque now; PASETO only if a host must verify without the database · vs signed now The server checks the database on every call anyway.