Skip to content

Latest commit

 

History

132 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

app-studio provider

Important

Read-only mirror — do not push or open PRs here. The standalone faroshq/provider-app-studio repository is automatically synced from the faros monorepo faroshq/faros (path providers/app-studio/) via splitsh-lite. Every sync force-updates the mirror, so any direct change here is overwritten. File issues and PRs against faroshq/faros instead. See docs/provider-publishing.md for how the mirror is published.

App Studio is a faros provider that gives each tenant a persistent AI project workspace: named Projects with durable "memory" (goals / requirements / constraints) and a chat surface backed by the tenant's own LLM credentials, with optional MCP tool use against their workspace. Projects are stored as projects.ai.faros.sh resources in the tenant's own kcp workspace; chat transcripts persist in the provider's message store (Postgres in production and local dev, with explicit in-memory mode available only for throwaway UI work).

The provider acts as the calling user: the hub's backend proxy forwards /services/providers/app-studio/* with the verified X-Faros-Tenant / X-Faros-User headers and the caller's bearer token, and the provider builds a per-request, token-scoped client (see tenant/). There is no provider service-account escalation.

What's here

Surface Where
Provider binary main.go — loads the provider kubeconfig, opens the message store, mounts /api + the embedded portal, heartbeats the hub
REST / LLM / message API api/ — Project CRUD, memory, LLM settings, streaming chat (/api/projects/*)
API types apis/ai/v1alpha1/ — the Project, Session, and Studio CRD types (deepcopy generated)
Typed client client/ — trimmed dynamic client for the Project resource
Tenant client tenant/ — token-forwarding ClientFactory (host+TLS from the provider kubeconfig, caller token per request)
Message store store/ — Postgres + in-memory + envelope-encryption implementations
Development runtime api/development_* + api/dataplane_client.go — template-selected development instances, component-aware sync, restart/log/status calls, and edge-checked preview authorization
Portal portal/ — the Vue micro-frontend (<faros-provider-app-studio>), embedded via assets.go
Registration manifest.yaml — CatalogEntry + APIExport (ai.faros.sh) + Code and Infrastructure dependencies + Project/Session/Studio schemas + tenant-scoped Infrastructure, Code Repository, ServiceAccount, Secret, and RBAC claims
Deploy deploy/chart/ — Helm chart (Deployment, Service, CatalogEntry)
CI (mirror) .github/workflows/{image,chart}.yaml — publish the image + chart to GHCR (run only in the mirror)

Skills

The project portal includes a first-class Skills workbench. It presents the installed catalog as a searchable grid, marks enabled skills, and opens a focused detail view with author-visible instructions, supporting-resource metadata, and an Enable or Disable action. The portal intentionally does not offer skill creation, import, editing, export, or deletion. Bundled skill content is read-only; project packages live under .agents/skills, with activation metadata in .agents/skills/.faros-catalog.json.

Every package must contain a SKILL.md whose YAML frontmatter includes the required name and description fields. Skill bodies and supporting resources are untrusted guidance: they cannot grant tools, permissions, models, approval bypasses, or override system/tool policy. App Studio reads bundled skills, authenticated provider-inline packages, and project packages; there is no remote skill registry. Provider packages are read-only system skills qualified as providers/<provider>/<packageName>.

Provider package distribution and provider/action enablement are separate. A validated CatalogEntry.spec.assistantSkills entry is distributed through the authenticated hub /api/providers catalog. It follows the system-skill default of enabled, and each project may disable or re-enable it. The package's version and canonical sha256: digest are retained in the same catalog snapshot used for metadata discovery, progressive load_skill/read_skill_resource, activation, and turn receipts. Provider readiness is not skill authority: a transient heartbeat/readiness change does not revoke declared guidance. A missing bearer or transient provider-catalog failure leaves bundled and project skills available and emits only a bounded sanitized warning where applicable. Provider Actions and grants remain authoritative and fail closed; skill text cannot widen them.

For each Default, Plan, or Review turn, catalog discovery exposes metadata for enabled skills and the model selectively invokes the assistant's native durable read tools: load_skill loads one qualified skill and read_skill_resource reads a bounded package-relative resource only after that skill is loaded. Those invocations appear in the same action pane as other tool calls, using lifecycle labels such as Loading skill and Loaded skill while showing only the qualified skill ID.

Skill selections are pinned to a catalog digest with bounded digest/content receipts. Skill lifecycle, selection, load, resource, and drift metrics use fixed outcome labels and do not include skill IDs, package paths, tenant IDs, request bodies, or resource paths.

The HTTP surface is:

GET    /api/projects/{project}/assistant/skills                         catalog metadata
GET    /api/projects/{project}/assistant/skills/detail?id={qualifiedID} author-visible detail
POST   /api/projects/{project}/assistant/skills/project                 create
POST   /api/projects/{project}/assistant/skills/project/import          import
GET    /api/projects/{project}/assistant/skills/project/{packageName}   inspect
PUT    /api/projects/{project}/assistant/skills/project/{packageName}   edit
DELETE /api/projects/{project}/assistant/skills/project/{packageName}   delete
GET    /api/projects/{project}/assistant/skills/project/{packageName}/export export
POST   /api/projects/{project}/assistant/skills/activation              enable/disable

The project lifecycle endpoints remain available for programmatic management, but they are not currently exposed as portal authoring controls. Turn and review requests continue to accept an optional skills field for API clients; the portal no longer presents a per-turn selector or sends that field.

Configuration

Environment variables consumed by the binary:

Var Purpose
PORT Listen port (default 8081)
FAROS_HUB_URL Hub base URL for tenant GraphQL, caller-scoped catalog lookup, and Provider Actions forwarding
FAROS_HUB_TOKEN Bearer token for the heartbeat
FAROS_PROVIDER_NAME CatalogEntry name (default app-studio)
FAROS_PROVIDER_KUBECONFIG Provider kubeconfig (kcp front-proxy host + TLS only)
FAROS_ACTIONS_EXTERNAL_URL Optional absolute HTTPS hub origin, reachable and certificate-valid from sandbox pods, injected into action-enabled development runtimes for workload-token exchange and the declared server-side Actions SDK gateway calls; no local default
FAROS_ACTIONS_CA_BUNDLE_FILE Optional PEM file containing the public CA for that origin; passed only to action-enabled development runtimes, never used to disable TLS verification
FAROS_ACTIONS_CA_BUNDLE Optional direct PEM equivalent for local launches; when both CA settings are present they must match
APP_STUDIO_DATABASE_URL Postgres DSN for the message store
APP_STUDIO_IN_MEMORY_MESSAGE_STORE true → non-durable in-memory store (dev)
APP_STUDIO_CONTROLLER_MODE Controller lifecycle policy: required requires the multicluster controller to become ready; rest-only intentionally disables it for local REST/portal development. Helm deployments set required.
APP_STUDIO_REST_ONLY Legacy local-development compatibility flag. Used only when APP_STUDIO_CONTROLLER_MODE is unset; true selects rest-only.
APP_STUDIO_MESSAGE_ENCRYPTION_KEYS Comma-separated key-id:base64-aes-key entries for message content and metadata encryption at rest
APP_STUDIO_MESSAGE_RETENTION Retention window (time.ParseDuration, e.g. 720h)
APP_STUDIO_WORKSPACE_ROOT Filesystem root for App Studio project workspaces and local file tools
APP_STUDIO_ASSISTANT_MAX_ITERATIONS Optional positive emergency model-call ceiling. The default is continuation-driven/unlimited, matching Codex; exhaustion fails with budget_limited.
APP_STUDIO_ASSISTANT_ROLLOUT_BUDGET_TOKENS Optional positive weighted-token budget for the Project conversation; disabled by default. Usage and reminders survive compaction and carry across runs. Exhaustion produces failed with budget_limited.
APP_STUDIO_ASSISTANT_MODEL_CONTEXT_TOKENS Active model context window used for token-pressure compaction (default 128000 when provider model metadata is unavailable).
APP_STUDIO_MCP_INSECURE_SKIP_TLS_VERIFY true → skip TLS verify on MCP calls (dev)
APP_STUDIO_PREVIEW_INSECURE_SKIP_TLS_VERIFY true → skip TLS verification only for preview readiness probes (local dev with a self-signed Gateway)
APP_STUDIO_PREVIEW_CONSOLE_ENABLED Automatically shares bounded browser-console evidence while the embedded preview is open; set false for a deployment-wide kill switch.
APP_STUDIO_PREVIEW_CONSOLE_SIGNING_KEY PEM-encoded P-256 private key used to sign short-lived ES256 iframe capabilities
APP_STUDIO_PREVIEW_CONSOLE_SIGNING_KEY_ID Stable key ID matching the public JWK independently deployed to the preview bridge

Health and readiness

GET /healthz is process liveness: it remains successful while a required controller is starting or retrying. GET /readyz is the provider readiness contract used by the CatalogEntry and Kubernetes readiness probe. In required mode it returns success only while the multicluster controller is running; startup, setup failure, retry, and unexpected controller exit remain not ready. In intentional rest-only mode it reports ready with that mode in the response. This separation keeps the process alive for recovery without advertising a provider whose reconciliation plane is unavailable.

Local message history

make run-provider-app-studio starts/reuses a local Postgres container by default and passes APP_STUDIO_DATABASE_URL to the provider:

make app-studio-db-up
make run-provider-app-studio

Preview inspection (inspect_development_preview) drives the workspace's shared headless browser — the infrastructure provider's Playwright MCP browser template, provisioned once per workspace by the Studio reconciler — over the infrastructure data plane. There is no app-studio browser worker to run; the tool is exposed to the model only when that shared browser is Ready. The model can supply only a path within the server-resolved current preview plus bounded semantic assertions. It cannot select an origin, click, type, or execute arbitrary JavaScript.

The database container is named faros-app-studio-postgres, listens on 127.0.0.1:55432, and stores data under .kcp/app-studio-postgres/. Both Tiltfiles expose it as the app-studio-db resource, so hard-refreshing the UI or rebuilding the provider no longer drops prior conversation history.

Both Tilt stacks also expose an app-studio-preview-console-key resource. It atomically generates and reuses a P-256 key under .kcp/app-studio-preview-console/. The App Studio process receives the private key and derived key ID; Infrastructure receives only the matching public JWKS and propagates it into development-preview init containers. The directory is gitignored and uses mode 0700; the private key uses mode 0600. Delete that directory only when intentionally rotating the local key, then restart the App Studio and Infrastructure Tilt resources so both sides receive the new pair. Local Make/Tilt targets intentionally ignore one-sided signing-key or JWKS environment overrides; use the Helm values or launch the binaries directly when testing a custom key pair.

To use your own database, set APP_STUDIO_DATABASE_URL in the environment or in providers/app-studio/.env (copy from .env.example). To intentionally use the old throwaway behavior, set APP_STUDIO_IN_MEMORY_MESSAGE_STORE=true.

Action-enabled development sandboxes also require FAROS_ACTIONS_EXTERNAL_URL in that environment file (or the launcher environment). make run-provider-app-studio forwards the explicitly configured value; it does not substitute the provider's internal FAROS_HUB_URL, localhost, or an insecure HTTP URL. The origin must be reachable from the sandbox pod and trusted by its system CA; configure deployment CA material separately when a private certificate authority is used. Set FAROS_ACTIONS_CA_BUNDLE_FILE (or the direct FAROS_ACTIONS_CA_BUNDLE value) for that case. The bundle is copied into a public ConfigMap-backed development mount and added to Go/Node trust roots; an unset bundle leaves the image's system trust unchanged. Helm deployments can use hub.actionsCABundleConfigMap to mount the same public PEM file into App Studio.

Resilient assistant conversations

Once a Project and its first user message exist, App Studio owns assistant work on the provider lifecycle rather than on an HTTP request. Its public contract is Thread → Turn → Item: clients create or select an assistant thread, POST .../assistant/threads/{thread}/turns, materialize the transcript with GET .../threads/{thread}/items, and follow typed events from GET .../threads/{thread}/events. Event sequence numbers and Last-Event-ID make reconnection incremental. Closing an SSE connection only removes that subscriber; it never cancels the Eino worker. The former /messages, latest-run, resume, stop, and snapshot-stream routes are no longer public.

A message submitted while the current run is working is durable steering for that same run, not a replacement run. The request names the expected run and is accepted only for the actor who started it. The supervisor persists an idempotent receipt, the user item, a new assistant segment, and the advanced run revision before Eino can observe the input. Eino drains steering between model calls; input that races with a final response is carried into the new segment. Admission and the terminal boundary share one lock, so late input is either queued or rejected for the next run, never acknowledged and lost. The active collaboration mode remains sticky.

New assistant runs use one sticky collaboration mode: Default, Plan, or Review. Plan is read-only. Review is an explicitly started, independently durable read-only turn over the current_workspace target; clients start one with POST .../assistant/threads/{thread}/reviews and may provide bounded review instructions. It reports evidence-backed findings and is never an automatic completion gate. Default follows the user's request directly and exposes the current evidence tools plus these source-mutation tools: create_file, replace_file, edit_file, delete_file, and move_file. read_file returns bounded structured data; only a complete read carries the opaque version needed for a mutation, while partial reads are inspection-only. create_file is always create-only: it never replaces an existing file and has no phase-dependent or initial-build variant. replace_file atomically replaces a whole file and requires the exact expectedVersion from a complete same-turn read. edit_file performs exact oldString/newString replacement (with an explicit replaceAll option) and, like delete_file and move_file, requires that complete same-turn read plus its expectedVersion; move destinations must be unused. Paths are normalized and authorized by the server, and stale, ambiguous, partial, or otherwise invalid mutations fail without changing the file. There is no patch grammar and no backwards-compatibility alias. Mutation failures use bounded structured typed metadata (code, operation, path, and guidance); an optional server-issued recoveryOf only correlates a retry in the activity feed, is presentation-only, and never grants authority or substitutes for a fresh read. Retrying and recovered feed entries retain the durable evidence of the original failure. The semantic action router, WorkItem promotion flow, phase-driven inner loop, and model-facing workspace hydration tool have been removed. The portal's explicit Implement plan action starts a fresh Default turn rather than silently changing the mode of a running turn.

The Thread/Turn/Item cutover intentionally starts with no canonical threads. Pre-cutover assistant history is not projected into the new public transcript. Legacy run/message rows remain an internal Eino persistence bridge during the cutover and are not exposed to clients.

Every model response batch is admitted before dispatch. Tool-call IDs are deterministic, malformed calls and conflicting IDs fail closed, and the model's call order and cardinality are preserved. Eino retains native concurrent execution and ordered rejoin, while a run-scoped reader/writer gate permits only explicitly parallel-safe reads to overlap; effects, unknown tools, and MCP tools are exclusive. An append-only AssistantRunEvent ledger records each admitted call and exact model-visible result together with its typed semantic disposition. Model-call audit entries also bind the visible tool contracts to a stable schema digest. The ledger provides idempotency within the active run and between concurrent workers; it is not a provider-restart continuation mechanism.

Transient setup failures and incomplete model streams retry from the current accepted turn history. Partial responses are discarded before tools can be dispatched or prose published. The configured retry count is bounded at the execution boundary with a hard maximum of 100, independently of HTTP settings validation; exhaustion returns the original classified stream failure.

The encrypted, append-only thread event stream records user and assistant items, tool calls, plans, steering, approval/input requests, and lifecycle transitions. Thread and turn rows are materialized projections; a turn's terminal projection and terminal event commit atomically. New turns reconstruct model context from the latest persisted compaction plus subsequent conversation evidence instead of dropping tool results. Reasoning, secrets, and transient preview-console payloads are not stored there.

Source mutations produce bounded server-generated diffs and structured operation/path metadata for the audit and action projections. The repository commit bridge carries the complete atomic upsert/delete bundle to provider-code. If a mutation fails after an I/O failure, the actual remaining paths are reported as a partial failure and retained in the durable dirty-path set; stale reads for those paths are invalidated before another mutation. Dirty paths are workspace information, not a hidden verification or commit obligation. Repository commits use the complete server-owned durable dirty bundle, including paths from earlier turns; the model supplies commit prose rather than authoritative file scope. Approval is bound to the bundle's current path membership and content digest, and only successfully committed paths are removed from the dirty set. The model is instructed never to commit unless the user explicitly requests repository persistence.

After a source mutation, runtime verification requires positive completion of workspace synchronization for that exact mutation revision before it can report ready. Operational readiness covers synchronization, process/log health, and preview reachability only. It never proves rendered content, interactions, data flow, application behavior, or acceptance criteria. Verification remains an optional model-selected tool; middleware does not force it or rewrite the final assistant response.

Mutation syncs are serialized in submission order per Project UID, and their revision, status, failure, and one bounded retry are checkpointed across permission or follow-up interrupts. Runtime verification and commit both hash the complete dirty bundle. Verification remains optional, but when a run claims both verified and committed state they must refer to the same digest; membership or content changes invalidate approval and any stale verification binding. While a run owns the project, server-side reservations reject external workspace hydration, template switching, manual sync, and deletion; matching disabled portal controls are only the UX layer over that server boundary.

This remains a single-replica execution design: work cannot continue across a provider restart. Recovery marks an orphaned active turn interrupted, while durable permission and input checkpoints remain resumable. Interrupt first persists the internal stopping transition, then asks Eino to cancel gracefully. Clients recover from the canonical item projection and resume the typed event stream after their last sequence; they do not depend on token replay.

Every effect is re-admitted at the Stop-serialized supervisor boundary against the run's durable actor digest. The model-visible project/repository snapshot and executable tool adapters share one per-sample request snapshot, so a tool cannot execute against an older request view than the one shown to the model.

The public turn lifecycle is in_progress, completed, failed, and interrupted. Approval and structured-input waits are typed items/events within an in-progress turn, not extra public lifecycle states. Model, provider, and budget failures are failed with structured error data; explicit interruption and provider process loss are interrupted. The portal renders terminal errors separately from real assistant prose and re-enables input for every terminal state.

Approval defaults to on_request: routine workspace work proceeds, while consequential external effects and repository commits ask. always_ask asks before every state-changing/external action, and never denies actions that need authority. Bounded compiler, test, and lint commands run automatically in the synchronized development runtime under on_request; they never write back to App Studio source. A plan communicates intended work and progress; it never grants permission. Default collaboration mode has no structured follow-up tool, while Plan mode may request structured input and remains read-only.

Lifecycle logs contain only organization, workspace, project, run, revision, and status fields. They intentionally omit prompt text, assistant content, tool arguments, and credentials.

Useful checks from this module:

go test ./...
go test -race ./api ./store
cd portal \
  && npm run test:workbench \
  && npm run test:preview-state \
  && npm run test:preview-actions \
  && npm run test:create-readiness \
  && npm run test:llm-settings \
  && npm run test:assistant-actions \
  && npm run test:assistant-plan \
  && npm run test:assistant-plan-popover \
  && npm run test:conversation-resilience \
  && npm run typecheck \
  && npm run build

Local project files

App Studio keeps project files in its own workspace root so the assistant can list, read, search, and safely mutate text files before asking provider-code to commit selected changed files to git. Set APP_STUDIO_WORKSPACE_ROOT to choose the directory; the binary defaults to a temp directory, while the Helm chart mounts a persistent volume at /var/lib/faros-app-studio/workspaces.

The assistant-facing workspace tools are App Studio local tools. Provider-code remains the git-source boundary: commit_project_files reads changed workspace files, represents missing dirty paths as deletions, and delegates the atomic upsert/delete commit to the Code provider's code__commit_files tool. A workspace move is persisted as an upsert of the destination and deletion of the source in the same repository commit.

Development runtime

App Studio owns the project-facing development API and workspace. A project selects an infrastructure Template; its development contract declares the component workspace paths and the instance resource to provision. App Studio creates or deletes that tenant-scoped instance, then routes file sync and runtime operations through the infrastructure provider's published data-plane subresources as the requesting user. App Studio never holds a credential for the infrastructure provider's runtime cluster. See docs/app-studio-sandbox-runtime.md for the current boundary and docs/app-studio-runtime-decoupling.md for the retained design proposal.

POST .../authorize-development-preview reads the selected instance's status.url and probes the public edge. It returns ready: true and the URL only after DNS/TLS/routing is serving; the portal retries readiness while the edge is provisioning. The preview URL is the Template's normal public route, not an App Studio-signed preview token, and browser traffic goes directly to that route. APP_STUDIO_PREVIEW_INSECURE_SKIP_TLS_VERIFY is only a local-dev override for the readiness probe.

Generated-app integrations

Project environments may contain a non-owning providerReference binding:

name: sales
provider: databricks
kind: providerReference
resourceRef:
  apiVersion: databricks.faros.sh/v1alpha1
  kind: Table
  resource: tables
  name: order-history
allowedActions:
- name: query_table
  version: v1
  schemaDigest: sha256:<catalog-digest>

App Studio only GETs the referenced object while reconciling and never creates, updates, owns, or deletes it. Integrations are managed through /api/projects/{project}/integrations (GET/POST), removed with DELETE on the alias, and invoked with POST on {alias}/invoke. On create or reactivation, App Studio resolves the caller-scoped /api/providers catalog and records a server-owned schemaDigest, grantedBy, and grantedAt for every exact action/resource grant. Revocation preserves that grant audit and records revokedBy/revokedAt; reactivation requires fresh catalog verification and consent when declared.

Invocation re-verifies the persisted grant digest against the live catalog (409 on drift), then forwards {"input": ...} to the provider's data-plane action route through the hub backend proxy — /services/providers/{provider}/actions/clusters/{cluster}/{resource}/{name}/{action}/{version}. The route is composed from the hub base plus the grant's bound resource; App Studio never learns a provider URL or embeds provider transport logic. Caller credentials, provider backend URLs, resource overrides, and raw SQL are rejected.

Generated server applications install the public @crwilhit/faros-actions-node@0.1.0 artifact under the stable consumer name with this exact dependency alias in the server component's package.json:

{
  "dependencies": {
    "@faros/actions-node": "npm:@crwilhit/faros-actions-node@0.1.0"
  }
}

Application code keeps the canonical import import { createActionsClient } from '@faros/actions-node'; and can call client.integration(alias).invoke(...) or invokeEnvelope(...). The SDK is server-only, requires an absolute HTTPS base URL (except an explicit loopback test override), reads the short-lived workload token from FAROS_ACTIONS_TOKEN_FILE on every request or from a refreshable credential provider, and retries once with forceRefresh after a 401. The bootstrap token used by the workload exchange is never the app token; no development token fallback exists. Development sandboxes install this declared dependency through the component toolchain; faros-dev-agent does not project an SDK or mount /node_modules.

Running it yourself

This provider can run in your own cluster instead of on the platform. faros creates a workspace for it in your organization, mints a credential scoped to that workspace alone, and generates the exact helm commands — under Providers → Self-Hosting in the portal.

Nothing to fill in: the infrastructure and code identity hashes it needs are resolved for you. It does expect the infrastructure and code providers to be available — self-host those too if you want the whole chain in your cluster.

Once installed, the provider registers itself and your workspaces enable it exactly like the platform copy. See docs/byo-providers.md for how the flow works, and deploy/chart/README.md for every chart value.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages