Search for a command to run...
Compiled from 39 nodes · est. 63 min read
Updated
QM (yc-software/qm) is a self-hosted AI agent platform that an organization deploys to its own Fly.io or AWS infrastructure, giving the team a shared assistant reachable through both a web UI and Slack. Operators drive the platform through the qm CLI (qm init, qm up, qm doctor), while end users chat via a surface (Slack or browser) and the agent executes skills inside isolated MicroVM sandboxes. Two architectural facts explain most of the rest: src/wiring.ts instantiates every service into a built object that src/index.ts consumes, and every turn flows through the Orchestrator, which routes to a pluggable Harness (claude, codex, opencode, pi, or mock) selected per org and scope. QM requires Node.js >=24.15.0 and npm >=11.10.0; the published @yc-software/qm CLI package sets a slightly looser floor of Node.js >=24.0.0.
Requirements and compatibility states the runtime floor, and Architecture and wiring is the anchor section — it covers Wiring and services, Server entry and lifecycle, Domain types, Skills and resolution, and the Model gateway. The Orchestrator section and its children (Orchestrator types and deps, Turn helpers, Sandbox provisioning, Prompt blocks, Turn behaviors and invariants) describe how a single agent turn is executed, and the Harness framework section plus its per-harness pages (Harness router, Pi harness, Pi tools, Claude harness, OpenCode harness) describe the model-execution substrates that turns dispatch into. Runs and workers covers the background execution layer (Run store, Worker loop, Turn stream), and API layer covers the HTTP surface (HTTP gate and auth, Routes and app assembly). Deployment and configuration documents the Configuration object and Sandbox backends, Deployment and CLI documents the operator-facing qm tool (qm init bootstrapping, qm commands, CLI internals, Deployment providers and layers, CLI package publishing), and Developer workflow plus its CI pipeline child cover running, testing, and shipping changes; Feature notes collects recent additions.
If you came here to understand how the system fits together, read Architecture and wiring first, then Wiring and services and Server entry and lifecycle. If you came here to deploy or operate QM, start with Requirements and compatibility, then qm init bootstrapping and qm commands under Deployment and CLI. If you came here to trace what happens during a single agent turn, read Orchestrator and then follow into Orchestrator types and deps, Turn helpers, and the relevant page under Harness framework. If you came here to contribute code or debug tests, start with Developer workflow and its CI pipeline child.
Updated
Pages in this section:
Updated
QM's server entry in src/index.ts hydrates identity and configuration before the listen loop, resolves effective egress enforcement and default models from providers, and classifies Slack integration state before starting the scheduler and shutdown handlers. The startup log reports listening port, org ID, store choices, worker count, and background-work status; both SIGINT and SIGTERM trigger a guarded shutdown that runs only once.
Before accepting requests, src/index.ts calls built.identity.hydrate() and built.config.hydrate?.(), ensuring identity and configuration are fully loaded before the server's listen loop begins.[1] On startup, src/index.ts logs the listening port, org ID, session store, run store, worker count, and background-work status.[1] The scheduler is only started when config.backgroundWorkEnabled is true; otherwise, src/index.ts logs that both the scheduler and runtime loops are suppressed.[1] The postdeploy smoke test in src/deployment/postdeploy-smoke.ts is hardened to catch regressions earlier in the deploy pipeline. The Postgres connection pool in src/persistence/pg-pool.ts includes init-retry behavior to improve reliability during startup.
Egress enforcement passed to createServer in src/index.ts is the result of effectiveEgressEnforcement(built.sandbox.profile, { signingSecret, apiBaseUrl }), which may differ from the egressDeclaredEnforcement stored in the sandbox profile — the two fields are kept separate.[1] The baseModelDefault passed to createServer is resolved by defaultModelForHarness(config.harness, configuredModelForHarness(config, config.harness), baseModelProviders(config)), making provider availability part of default-model selection — the fix for an OpenRouter-only deployment receiving an Anthropic default.[1] Slack environment state is classified as "absent", "configured", or "partial" in src/index.ts: "configured" when both SLACK_BOT_TOKEN and SLACK_APP_TOKEN are fully parsed, "partial" when the tokens are present but the config could not be assembled, and "absent" when neither token is set.[1] At startup, src/index.ts probes Docker daemon availability and injects the result into the deploy provider at construction time via src/wiring.ts, rather than performing the probe inside docker-deploy-provider.ts. When the Docker daemon is unreachable at startup, src/index.ts emits a structured warning log during boot instead of failing silently or deferring the error to the first deployment attempt.
Both SIGINT and SIGTERM invoke the same shutdown() function in src/index.ts, and a boolean guard (shuttingDown) ensures the function body runs at most once even if both signals fire.[1]
Sources
Updated
src/wiring.ts is the central dependency-wiring module: it imports and instantiates every major service — identity, ACL, skills, runs, sessions, sandboxes, connectors, model gateway, scheduler, and more — and returns a built object consumed by src/index.ts.[1]
src/wiring.ts supports two persistence backends for most services — memory and Postgres — selecting between pairs such as createMemorySessionStore vs createPostgresSessionStore and createMemoryRunStore vs createPostgresRunStore based on configuration.[1] src/wiring.ts wires a separate CronFireStore (src/cron/cron-fire-store.ts) into the service graph for persisting cron fire history, distinct from the primary CronStore (src/cron/cron-store.ts). Scheduler scans in src/api/control-service.ts and cron routes in src/api/routes/crons.ts query only the CronStore for cron state; fire-history reads are routed through CronFireStore, preventing history accumulation from affecting scheduler performance and correctness.
Two deploy providers are wired in src/wiring.ts — createDockerDeployProvider and createAwsDeployProvider — assembled alongside createDeployService and createDeployStore.[1] src/wiring.ts supports two secret sources — createEnvSecretSource and createAwsSecretsManagerSource — composed via createLayeredSecretSource, so secrets from environment variables can be overridden or augmented by AWS Secrets Manager.[1]
src/wiring.ts wires five named harnesses — createMockHarness, createOpenCodeHarness, createCodexHarness, createClaudeHarness, and createPiHarness — all routed through createHarnessRouter; harness-specific architecture and invariants are covered on the Harness framework and Harness router pages.[1] A harness is an adapter that connects QM's execution engine to a specific AI model or runtime (e.g., Claude, Codex); the harness router selects the correct adapter per request, allowing different models to be used without changing core logic.
The plugins/ directory contains the optional surface plugins: Slack, web UI, admin panel, and public portal.[2]
Sources
Updated
QM's domain types divide platform access by principal type (internal member or guest) and scope kind (personal, channel, team, org, or group), each scope identified by scopeId(kind, ref) strings and governed by distinct policies for management, sharing, filesystem access, and network egress. The type system represents the agent turn loop (TurnRequest, TurnOrigin, Resolution) as a configuration bundle for execution, and the session tape (EntryType) as a record of interactions, with fine-grained policies (CommandPolicy, EgressPolicy, ApprovalGrantModes) controlling what actions are permitted under which identities and approval conditions.
PrincipalType in src/types.ts is a union of "internal" and "guest", distinguishing authenticated org members from external guests.[1] The five valid ScopeKind values in src/types.ts are "personal", "channel", "team", "org", and "group", forming the isolation hierarchy for the platform.[1] ScopeId strings are formatted as "<kind>:<ref>" (e.g., "personal:user123"), produced by the exported scopeId(kind, ref) function in src/types.ts.[1] personalScope(principalId) is a convenience wrapper that returns scopeId("personal", principalId), representing a user's private isolation boundary.[1] parseScopeId returns { kind: null, ref: "" } when the input contains no : separator, and { kind: null, ref: ... } when the prefix is not a recognized ScopeKind.[1] isManageableCreationScope returns true only for "channel" and "team" scopes, meaning only those scopes support managed resource creation.[1] isSharedScope returns true only for "channel" and "group" scopes, distinguishing them from "personal", "team", and "org" scopes.[1] In QM's isolation hierarchy, narrower scopes (e.g., "personal") are fully private to a single user, while broader scopes (e.g., "org") span all members, determining which agents and users can share context and access resources. Person resolution in src/api/routes/directory.ts and src/directory/directory-store.ts handles deployments with no Slack surface configured, preventing failures and empty results for non-Slack deployments when resolving members. src/api/app-helpers.ts and src/api/app-sessions.ts permit adding a project member whose identity the directory store has never encountered, allowing external users to be added to projects before their first directory sync.
TurnRequest in src/types.ts is the primary input structure for the agent turn loop, covering model selection (model, harness, thinkingLevel, fastMode), approval handling, attachment passing, and async dispatch.[1] TurnOrigin is a discriminated union distinguishing "human" (interactive user), "ambient" (background observation), "automation" (scripted/scheduled), and "direct" (internal programmatic) origins for a turn.[1] EntryType in src/types.ts is a union covering "user", "assistant", "thinking", "text", "tool_call", "tool_result", "soul", "system", "delivery", "approval_request", and "approval_resolved" — the full set of tape entry kinds.[1]
The Resolution interface in src/types.ts is the fully resolved runtime configuration for a turn, bundling workspace layers, system prompt, egress policy, command policy, security policy, approval grant modes, org scope, and granted handles.[1] WorkspaceLayer has a mode of "ro" (read-only) or "rw" (read-write), controlling filesystem access for each mounted scope layer.[1] EgressPolicy controls outbound network access via allowedHosts (whitelist), an optional denyPrivateNetworks flag, privateNetworkAllowedHosts exemptions, and an optional deniedHosts list.[1] CommandPolicy operates in either "denylist" or "allowlist" mode, with an ordered list of CommandRule entries each carrying a pattern, a decision ("allow" | "deny" | "require_approval"), and an optional reason.[1] ApprovalGrantModes has two boolean flags — session and always — controlling whether an approval can be granted for the current session only or permanently.[1]
BackgroundWakeTrigger in src/types.ts is typed as "cron" | "webhook" | "monitor" | (string & {}), allowing known trigger kinds to be named while still accepting arbitrary future strings.[1] The Cron trigger interface supports a runAs field with values "owner", "scopeFloor", or "scopeShared", controlling which identity context the cron job executes under.[1]
The Session interface in src/types.ts tracks agent-session lifecycle state via optional fields including archived, pinned, working, awaitingInput, backgroundJobs, and watches.[1] Session.forkedFrom records the parent session ID and title when a session is a fork, with forkBoundarySeq marking the tape sequence where the fork diverges.[1]
The Destination interface in src/types.ts supports a taskList field for structured task tracking, with each item carrying an id, title, and status of "pending", "in_progress", "completed", "skipped", or "failed".[1] RecipientConsent tracks whether a delivery recipient has accepted or declined being contacted, with statuses "pending", "accepted", or "declined".[1]
Sources
Updated
QM's resolution and skill systems split concerns: ResolutionService computes access policies (layers, prompts, rules, security) per conversation-actor pair, while SkillSyncEngine and SkillMaterializer handle keeping skills synchronized and their assets on disk. The skill sync pipeline runs on a leader-leased 5-minute tick that both drives materialization and can be triggered on-demand for tests; materialization itself has two granularities—full index or single-skill bundle.
createResolutionService() in src/resolution/resolution-service.ts is the factory for a ResolutionService that computes the full Resolution — layers, system prompt, egress rules, command policy, security policy, approval modes, and granted handles — for a given conversation and actor.[1]
src/skills/materialize.ts exports the SkillMaterializer interface with two operations: materializeIndex() (writes all active skills' SKILL.md files and an index marker) and materializeTree() (writes a single skill's assets and bundle pack files).[2]
src/skills/skill-sync-engine.ts defines the SkillSyncEngine interface, whose tick() method is public and can be called directly — in tests or on-demand triggers — independently of the periodic sweeper; start() and stop() control the background loop.[3] The default polling interval for SkillSyncEngine is 300,000 ms (5 minutes).[3] The leader-lease key "skills:sync:tick" ensures only one cluster node drives skill sync at a time.[3] A leader lease is a short-lived distributed lock that only one cluster node holds at a time; in QM's skill sync, it prevents duplicate materialization work across concurrent instances.
Sources
Updated
createModelGateway in src/model/model-gateway.ts creates an in-memory ring buffer of model call records, defaulting to a maximum of 1,000 records and evicting the oldest when the limit is exceeded.[1] ModelGateway.audit() returns a shallow copy of all stored records, preventing callers from mutating the internal buffer.[1]
A term of art: an OpenRouter model identifier is a string that selects a specific model hosted via the OpenRouter API. The Model Gateway accepts these identifiers dynamically at turn-invocation time. Per-user model authentication: a mode in which each turn is authenticated against the requesting user's own provider account (e.g. Claude or ChatGPT) rather than a shared server-side credential.
The Model Gateway accepts dynamic OpenRouter model identifier strings at turn-invocation time; callers are not required to pre-register the model in src/model/model-catalog.ts.
The Model Gateway supports per-user model authentication, allowing each turn to be authenticated against the requesting user's own provider account rather than a shared server-side credential. Per-user model auth routing logic lives in src/core/individual-auth-routing.ts; src/core/orchestrator.ts carries per-user model auth context through the turn lifecycle. Token resolution for per-user model authentication is handled in src/credentials/harness-auth-env.ts and src/credentials/connector-token.ts; the API route src/api/routes/user-model-auth.ts lets users register and manage their own provider credentials.
Sources
Updated
Pages in this section:
Updated
The Config interface in src/config.ts is the central runtime configuration object, with fields covering every subsystem: model harness selection, sandbox backends, memory policy, worker pool, rate and budget limits, security posture, deployment targets, and the Slack plugin.[1]
Config.harness selects the AI execution substrate and must be one of "mock", "pi", "opencode", "codex", or "claude".[1] configuredModelForHarness(config, harness) resolves the correct model ID for a given harness string, routing "codex" → config.codexModel, "claude" → config.claudeModel, "opencode" → config.opencodeModel, and everything else → config.modelId.[1] providerKeysPresent(config) returns a ModelProviderAvailability object indicating which API keys are configured (anthropic, openai, openrouter).[1] baseModelProviders(config) returns onlyProvider(config.modelProvider) when a modelProvider is set, or undefined otherwise — this is the fix for the "OpenRouter-only deployment gets Anthropic default" bug where MODEL_PROVIDER was ignored.[1]
Config.sandboxBackend selects the sandbox execution environment: "aws" (MicroVM), "local" (Docker), or "sprites" (Fly.io); a secondary backend is also optionally configurable via sandboxSecondaryBackend.[1] Config.snapshotStore and Config.transferStore each accept "local" or "s3", with S3 configured via s3Bucket, s3Region, and s3Prefix.[1] Config.secretsBackend is either "env" (secrets read from environment variables) or "aws" (secrets read from AWS Secrets Manager), paired with a secretsPrefix.[1]
The AwsSandboxEnv block in src/config.ts is populated from environment variables including AWS_SANDBOX_REGION, AWS_SANDBOX_IMAGE, AWS_SANDBOX_EXEC_ROLE_ARN, AWS_SANDBOX_INGRESS_CONNECTORS, and AWS_SANDBOX_EGRESS_CONNECTORS (comma-separated ARN lists).[1] The sandbox region falls back through AWS_SANDBOX_REGION → AWS_REGION → AWS_DEFAULT_REGION → "us-west-2" if none is set, and the image identifier defaults to "qm-microvm-sandbox" when AWS_SANDBOX_IMAGE is absent.[1]
Config.deployProvider selects the deployment target: "docker" or "aws".[1] Config.ecsTaskProtection and Config.ecsAgentUri govern ECS-specific behavior, enabling task protection (scale-in blocking) during active turns on AWS ECS deployments.[1]
Config.rateLimitPerWindow and Config.rateLimitWindowMs configure the per-principal turn rate limit; Config.budgetUsdPerWindow, Config.orgBudgetUsdPerWindow, and Config.budgetWindowMs configure per-user and per-org spend ceilings.[1]
Worker concurrency is governed by Config.workers (pool size), Config.leaseTtlMs (job lease duration), Config.heartbeatIntervalMs, Config.reaperIntervalMs, and Config.maxClaims (max jobs a worker claims at once).[1] Config.backgroundWorkEnabled is a feature flag controlling whether background jobs — crons, monitors, and webhooks — are processed.[1]
Config.memoryCapture, Config.memoryRecall, and Config.memoryStrategy configure the memory subsystem's capture mode, recall mode, and consolidation strategy independently; optional fields memoryConsolidateAfter and memoryCaptureMaxTurns add further control.[1] Config.memoryCaptureQuietMs is initialized from DEFAULT_CAPTURE_QUIET_MS (imported from src/memory/strategies/per-turn.ts), controlling the quiet-period debounce before memory capture fires.[1] The procedural memory provider interface, defined across provider.ts, relay.ts, and inject.ts, is vendor-neutral; vendor-specific implementation details are isolated to the concrete implementation layer and kept out of the shared interface. The src/memory/memorable/ module provides a concrete procedural memory provider implemented in capture.ts, inject.ts, provider.ts, and relay.ts; registration and configuration are managed by provider-factory.ts and provider-config.ts. .github/workflows/cicd.yml pins an exact memorable-cli version for end-to-end testing; the pin must be kept in sync when the CLI version drifts from the running environment. provider-router.ts routes memory operations by scope; strategy files consolidation.ts, per-turn.ts, and scratch-promote.ts, along with memory-service.ts, dispatch through it. Scope configuration is wired into the server via src/config.ts and src/wiring.ts. Per-scope provider selection allows different memory scopes (user, skill, organization, etc.) to route to different backends; provider-router.ts and provider-config.ts manage the dispatch logic. src/memory/memorable/mcp-memory-provider.ts implements a Model Context Protocol (MCP) backend for procedural memory, dispatched by provider-router.ts based on the active scope. Model Context Protocol (MCP) is a vendor-neutral protocol for exposing context and tools to AI models; used here as a pluggable backend transport for the procedural memory provider.
Config.securityScreenBackend is either "model" (local model-based screening) or "proxy" (external proxy); the proxy path is configured via securityScreenProxy, which accepts provider, endpoint, token, and shadow fields.[1]
Config.pluginSkillDirs is a list of filesystem directories scanned for plugin skills, complementing the seed skills found in Config.skillsSeedDir.[1] The plugins/chassis/src/branding.ts module is the canonical source for bot identity values (name, avatar, branding); all plugins and backend extensions must pull identity through this module rather than hardcoding values, as bypassing it causes fallback to a default identity that breaks white-label deployments. Bot identity (name, avatar, branding) is configurable independently of the QM deployment via the Config interface; the branding surface is consumed by the auth, admin, and web-ui plugins, and is scaffolded across CLI config (cli/src/config.ts, cli/src/services.ts), all three deployment backends (AWS, Docker, Fly), and Slack manifest generation. The acknowledgement emoji posted to Slack threads is configurable per-org; org admins set it via an emoji picker in the admin panel (plugins/admin/public/index.html, plugins/admin/src/index.ts) rather than through static config. src/slack/ack-emoji.ts and src/slack/config.ts establish the ack-emoji as a first-class config value, resolved through src/resolution/config-store.ts and src/wiring.ts; src/slack/turn-handler.ts reads the resolved value at turn time. Admin API routes in src/api/routes/admin/slack-installation.ts and src/api/routes/admin.ts expose read and write endpoints for the org-level ack-emoji setting.
Sources
Updated
src/wiring.ts routes agent work to one of three sandbox backends (local, Fly.io, AWS Lambda), each implementing the Sandbox interface with a core set of file and process methods plus optional capabilities detected at runtime via type guards. Optional capabilities—backup, blob staging, and process sessions—require both a profile flag AND all associated methods to be present; egress enforcement is silently disabled if the control plane lacks signing credentials or an API URL.
src/wiring.ts supports three sandbox backends — createLocalSandbox, createSpritesSandbox (Fly.io MicroVM), and createAwsSandbox (AWS Lambda MicroVM) — selected at runtime.[1] The Docker sandbox implementation spans three layers: cli/src/backends/docker.ts (CLI backend), src/sandbox/local-sandbox.ts (server-side lifecycle), and deploy/core/Dockerfile (container image). The Docker sandbox backend (cli/src/backends/docker.ts) runs sandboxes directly on the deployment host; configuration is stored in cli/src/config.ts and src/config.ts, with wiring exposed via cli/src/providers.ts. The Docker sandbox backend routes authentication-broker traffic over a private network alias rather than the public interface, keeping auth-broker connectivity intact in deployments where the broker is not externally reachable; this routing is validated by preflight checks in cli/src/backends/doctor.ts and cli/src/commands/check.ts. aws/microvm-agent/Dockerfile and cli/templates/aws/microvm-agent/Dockerfile install the GitHub CLI via an immutable (pinned) method to ensure consistent versions across image rebuilds and prevent silent version drift. cli/test/microvm-dockerfile.test.ts validates that the MicroVM Dockerfile uses the immutable GitHub CLI install form, guarding against regression in CI. The agent37 sandbox backend is implemented in src/sandbox/agent37-sandbox.ts, integrating with Agent37's computer API to provision and manage sandbox environments. The agent37 sandbox backend is wired into src/sandbox/sandbox-routing.ts, src/wiring.ts, and src/config.ts. src/sandbox/agent37-sandbox.ts includes auto-sleep for idle computers and hardened state handling in the start path to prevent failures or hangs from transient lifecycle states.
The Sandbox interface in src/sandbox/sandbox.ts defines the full contract for agent computer backends, with required methods provision, run, readFile, writeFile, listDir, removeDir, and teardown, plus optional capabilities including backupComputer, startProcess, readProcess, writeStdin, signalProcess, listProcesses, stageIn, stageOut, and extractFiles.[2] Optional sandbox capabilities — backup, blob staging, and process sessions — are detected at runtime via type-guard functions (supportsAgentComputerBackup, supportsBlobStaging, supportsProcessSessions) rather than interface conformance alone.[2] supportsProcessSessions requires BOTH profile.processSessions === true AND all five process-related methods (startProcess, readProcess, writeStdin, signalProcess, listProcesses) to be functions — a backend that sets the profile flag but omits any method will fail the guard.[2] supportsBlobStaging requires all three of stageIn, stageOut, and extractFiles to be present; a sandbox implementing only some of these methods is not treated as blob-staging-capable.[2]
AgentComputerProfile in src/sandbox/sandbox.ts has two writable-persistence modes, "snapshot_to_workspace" and "resident_disk", controlled by the writablePersistence field.[2] EgressEnforcement has three levels — "none", "ip_port", and "domain" — ranked in ascending restrictiveness, where "domain" is the most restrictive.[2] effectiveEgressEnforcement in src/sandbox/sandbox.ts returns "none" if the control plane lacks either a signingSecret or an apiBaseUrl, regardless of what the profile's egressEnforcement field says — egress enforcement is silently disabled on incomplete control-plane configuration.[2] TeardownOptions accepts keepWarm (keep the sandbox alive for reuse) and destroy (force full destruction) as optional flags passed to sandbox.teardown().[2] AgentComputerBackupOptions allows filtering backup entries via include (areas), exclude (per-entry predicate), includePaths, followSymlinks, and keepContentCaches.[2]
capabilitiesLostMovingTo in src/sandbox/sandbox.ts compares two sandboxes and returns human-readable strings for any capability or egress-enforcement level that would be lost by the migration.[2] CapabilityUnsupportedError is thrown when a caller requests a capability not supported by the current backend; it exposes backend and capability string fields and uses the error name "CapabilityUnsupportedError" for programmatic detection.[2] hasParentPathSegment in src/sandbox/sandbox.ts detects path-traversal attempts by checking whether any /-delimited segment equals "..", serving as a security guard against relative escape paths.[2] visibleNotInstalled in src/sandbox/sandbox.ts filters notInstalled tool names that are already advertised as extra tools, preventing tools the operator added via extraTools from appearing in the "not installed" list shown to the agent.[2] visibleTools in src/sandbox/sandbox.ts deduplicates the tool list by binary name (first whitespace-delimited token per line), keeping only the first occurrence of each binary.[2] A bug in command-form approval rule evaluation, fixed in cli/src/sandbox-layer.ts and src/deployment/deployment-layer.ts, ensures approval rules correctly govern which tool invocations proceed without explicit human sign-off.
Sources
Updated
QM requires Node.js >=24.15.0 and npm >=11.10.0 in the root workspace; the CLI package relaxes Node.js to >=24.0.0 but is otherwise an unpublished ES module monorepo.
QM requires Node.js >=24.15.0 and npm >=11.10.0, as declared in the root package.json engines field.[1] The @yc-software/qm CLI package sets a slightly looser engine floor of Node.js >=24.0.0 in cli/package.json.[2] The root package.json declares the project as an ES module ("type": "module") and marks it private: true, meaning the root package is not published directly to npm.[1]
The web UI plugin's DOMPurify dependency in plugins/web-ui/package.json is pinned at version 3.4.13 to address CVE GHSA-55q2-fjhq-7xh7, a cross-site scripting (XSS) vulnerability.
Sources
Updated
QM's dev workflow spans a main server, separate background worker, and isolated test helpers—all started via npm scripts and configured through environment variables and distinct TypeScript configs for type-checking different layers (runtime, contract, CLI). Feature requests must be authored as human-written .txt or .md files in the adrs/ folder; bug reports go to GitHub issues.
The main server is started with node --env-file-if-exists=.env src/index.ts; the dev server additionally sets SHUTDOWN_DRAIN_MS=2000 and uses --watch for hot reload.[1] The background job worker is started separately via the worker npm script, which runs node --env-file-if-exists=.env src/runs/worker-main.ts.[1]
Server-side type-checking uses tsc --noEmit; a separate typecheck:contract script checks against tsconfig.contract.json to validate the public API contract.[1] CLI type-checking is done via npm run typecheck, which runs tsc -p tsconfig.json — a separate config from the build tsconfig.[2]
qm dev --ci [up|down] runs CI mode: core services only (Slack in-process), with no pool lease, intended for live end-to-end CI runs.[3]
Tests in test/orchestrator.test.ts use a freshApp() helper that calls buildApp from src/wiring.ts with a fresh temporary data directory per test, ensuring isolation without shared state.[4] spyProvisioning in test/orchestrator.test.ts wraps sandbox.provision and sandbox.teardown to track provisioned (total ever provisioned) and live (currently active) sandbox counts for use in test assertions.[4]
Feature requests are submitted as a human-written .txt or .md file added to the adrs/ folder via PR — AI-expanded formal proposals are explicitly not accepted. Bug reports go as GitHub issues.[5]
Sources
Updated
QM's CI pipeline enforces code quality through four linting checks (Prettier, ESLint, Knip dead-code detection, Oxlint), parallelized core tests across 5 shards, Postgres integration tests, CLI version bumping rules, and per-plugin Docker image verification. Concurrent runs deduplicate smartly—canceling in-progress pull-request builds but preserving all main pushes—while reading the authoritative Node.js version from .node-version across all jobs.
Concurrent CI runs on the same branch are de-duplicated: the concurrency key cancels in-progress runs only for pull requests, never for pushes to main.[1] The CI/CD pipeline reads the Node.js version from a .node-version file (via node-version-file: .node-version) across all jobs, so the authoritative Node version is maintained there, not in the workflow YAML.[1]
The lint job runs four distinct checks in sequence: Prettier formatting (npm run format:check), ESLint for the whole repo including plugins (npm run lint), dead-code detection via Knip (npm run lint:knip), and Oxlint (npm run lint:ox).[1]
Core tests run across 5 parallel shards using the CORE_TEST_SHARD environment variable and the npm run test:root:shard command; shard 1 also verifies the shard plan via npm run test:root:shard:check.[1] The core-postgres CI job spins up a postgres:16 service container and passes DATABASE_URL=postgres://postgres:postgres@localhost:5432/qm to the test runner via npm run test:pg; this is the required Postgres version for durability and cross-process tests.[1]
A cli-version job on pull requests enforces that any change under cli/bin, cli/src, cli/templates, cli/manifest.json, cli/package.json, cli/package-lock.json, cli/README.md, cli/LICENSE, cli/tsconfig.json, or cli/tsconfig.build.json must be accompanied by a semver version bump in cli/package.json.[1] The CLI job also runs deployment-stack contract tests after building the CLI package: npm run typecheck:contract and node --test "deploy/stacks/*/test/*.test.ts".[1]
Each plugin (admin, web-ui, auth, portal) has its own CI job that runs npm ci, npm run typecheck, and npm test, then calls bash scripts/smoke-surface-image.sh <name> to build and boot its production Docker image.[1]
Sources
Updated
Pages in this section:
Updated
To bootstrap a deployment, run qm init via npx from an organization-owned repository; initialization walks through infrastructure, web sign-in, connector credentials, optional Slack access, deployment, and live verification without requiring a source checkout.[1] The full invocation for a new org layer takes the form node cli/bin/qm.ts init deploy/layers/<org> --org <slug> --target <fly-or-aws>, where <slug> is a local name derived from the organization and is not globally unique.[2]
qm init materializes deployment.md and .codex/skills/deploy-qm/ in the new deployment directory; the generated skill is handed to an agent to drive the rest of the deployment.[2] Base model provider defaults to anthropic (its API key is a required secret), and sign-in email transport defaults to resend; the command prompts the operator to choose one email transport and scaffolds only that transport's secret keys.[3][4]
The built-in auth broker uses email-delivered one-time links (magic links); supplying the admin address, a verified sender, and a Resend key or SMTP credentials is sufficient — the CLI generates and wires the rest.[2] To use an external identity provider instead of the built-in auth broker, drop "auth" from services; that provider must register the exact <publicUrl>/auth/callback redirect URI.[2]
Sources
Updated
QM's command-line interface in cli/src/cli.ts offers deploy, validation, and secret management commands that share common path options (--config, --env-file, --sandbox-dir) and can output structured reports. qm up, qm check, and qm secrets provide deployment planning, config validation, and safe secret injection, while qm rollback restores prior deployments using platform-specific identifiers.
All deploy commands in cli/src/cli.ts accept three shared options: --config <path> (default: qm.config.jsonc in the deploy directory), --env-file <path> (default: .env in the deploy directory), and --sandbox-dir <path> (default: sandbox/ in the deploy directory).[1]
qm up --dry-run resolves the config and reports the deployment plan without making any changes; qm plan is a direct alias for that flag.[1]
qm check performs static config and sandbox validation by default; passing --live additionally verifies the running identity, rendered config, and deployment health.[1] Passing --json to qm check outputs machine-readable results keyed by contract clause.[1]
qm secrets set writes one .env value in place, deduplicating the key and preserving file order and mode; when no value is supplied it reads from stdin or prompts interactively, so the secret never enters shell history.[1]
qm rollback accepts a --to <target> argument whose form is platform-specific: on AWS it takes a prior deployment manifest or a manifest ID/release label; on Fly it takes a sandbox image or tag.[1]
Sources
Updated
QM abstracts deployment across Fly.io and AWS through a provider registry, with provider choice locked at qm init time and organization-specific deployment logic isolated in deploy/layers/<org>/. Operators must implement their own deployment CI; QM provides provider templates and a sandbox build command (flyctl deploy --remote-only --build-only --push), not a complete workflow.
The installed @yc-software/qm package carries both Fly.io and AWS provider templates and dispatches their common lifecycle through a hosting-provider registry.[1] Provider choice (Fly.io or AWS) is fixed at qm init time because it determines the config, secret rules, generated files, and teardown contract; changing providers requires initializing a new empty directory.[1] The hosting-provider registry is an internal lookup table mapping provider names (e.g., fly, aws) to lifecycle hooks (init, deploy, teardown), allowing QM to dispatch provider-specific logic without callers needing to know which provider is active. FlyDeployProvider (src/deploy/fly-deploy-provider.ts) implements the hosting-provider interface, enabling qm publish to target Fly.io-hosted deployments as a first-class provider option. Fly provider selection is driven by config declared in src/config.ts; src/deploy-service.ts integrates the provider into the deploy-service lifecycle. src/wiring.ts conditionally instantiates FlyDeployProvider based on the active config, registering it as a deploy backend alongside any other configured providers.
Organization-specific deployment customizations live in deploy/layers/<org>/, containing config, sandbox customizations, provider coordinates, and generated Slack manifests; the rest of the tree stays identical to upstream.[1] The QM source repository carries no production deployment workflow, and qm init does not create deployment CI — operators must wire their own.[1]
Fly.io sandbox images are built and published with flyctl deploy --remote-only --build-only --push --image-label latest; the target app name is controlled by the FLY_SANDBOX_APP_NAME environment variable.[2]
Sources
Updated
QM's CLI startup code (cli/src/manifest.ts and cli/src/cli.ts) loads and validates immutable image references, parses a custom argument syntax, and resolves deployment configuration from filesystem or flags. Boolean flags are enumerated in BOOLEAN_FLAGS; several multi-valued options (--target, --model-provider, --email-transport) are validated against allowed sets, and --config with optional --sandbox-dir establish the deployment context.
cli/src/manifest.ts reads manifest.json with a sibling-directory fallback: it first tries the path adjacent to the current module file, then tries one level up (../manifest.json).[1] Every image reference in manifest.json (service or sandbox base) must be digest-pinned to a sha256:<64-hex-char> digest; cli/src/manifest.ts enforces this via immutableRef(), throwing an error on any non-pinned ref.[1] manifestRef(service) throws if manifest.json contains no entry for the requested service name, and delegates to immutableRef() for the pin check if an entry is found.[1] cliVersion() reads the CLI version from package.json at runtime, keeping the in-binary version always consistent with the published package version.[1]
cli/src/cli.ts defines a minimal custom argument parser that distinguishes boolean flags — enumerated in BOOLEAN_FLAGS (e.g. build-only, ci, dry-run, follow, json, live, purge, static, yes) — from value-taking flags, collecting everything else as positionals.[2] The --follow and -f flags are treated as synonyms for log tailing: followFlag() returns true if either boolean is set.[2]
The --target flag is validated against the list of known hosting-provider IDs; an invalid value throws a CliError listing the allowed choices.[2] The --model-provider flag is validated against MODEL_PROVIDERS; an invalid value throws a CliError with clause cli.invocation.[2] The --email-transport flag is validated against EMAIL_TRANSPORTS; an invalid value likewise throws a CliError with clause cli.invocation.[2]
deployContext() resolves the deployment config from --config <path> if supplied, otherwise searches the specified or current working directory; sandboxDir defaults to sandbox/ inside the config directory but can be overridden with --sandbox-dir.[2]
Sources
Updated
The CLI package is published on npm with provenance attestation to verify build origin, including only dist, templates, and metadata—source files are excluded. The published CLI exports a ./contract entry point that provides types and runtime interfaces for consuming the package API.
The CLI package is published publicly on npm with provenance attestation (provenance: true).[1] Published to npm, the package includes only dist, templates, manifest.json, and README.md — source files are excluded.[1] The CLI package exports a ./contract entry point with types at dist/src/contract.d.ts and runtime at dist/src/contract.js.[1] The CI workflow (.github/workflows/cicd.yml) does not gate PR merges on CLI version changes; version management is deferred to the release workflow. The release workflow (.github/workflows/release.yml) enforces semver validation as a preflight check at publish time.
Sources
Updated
A Harness is a composition of control, tool, and model layers that standardizes how QM backends handle turns: it ties a control transport (mock, HTTP, in-process) and tool transport (plugin, MCP, dynamic) to optional model utilities like history compaction and security screening, all instantiated through defineHarness. A HarnessTurnInput lets callers control per-turn behavior—model selection, thinking level, tool approval gates, security screening—while HarnessTurnResult surfaces outcomes (pending approvals, cache usage, LLM telemetry) from a single turn of execution.
The Harness interface in src/harness/harness.ts composes three sub-interfaces: HarnessTurnController (executes turns), HarnessModelUtilities (optional model utilities such as compact history and security screening), and HarnessToolPresentation (tool name formatting).[1] defineHarness in src/harness/harness.ts is the factory for constructing a Harness: it takes a HarnessAdapterProfile, a combined HarnessImplementation (turn controller plus model utilities), and an optional HarnessToolPresentation, returning a bound Harness object.[1] When no HarnessToolPresentation is supplied, defineHarness defaults to an identity mapping — name: (coreName) => coreName — so tool names are passed through unchanged unless a harness explicitly overrides it.[1]
HarnessAdapterProfile declares the valid control transports ("mock" | "in-process" | "sdk" | "http" | "json-rpc" | "api"), tool transports ("mock" | "in-process" | "plugin" | "dynamic" | "in-process-mcp" | "mcp"), and capabilities ("abort" | "steer" | "images" | "thinking-level" | "fast-mode" | "provider-sessions").[1]
HarnessTurnInput supports optional per-turn model selection (model?), harness override (harness?), thinking level (thinkingLevel?), fast mode (fastMode?), and read-only mode (readOnly?).[1] HarnessTurnInput also accepts a tapeMode of "shadow" or "serve" along with tapeRows and tapeFold, enabling the tape-fold conversation healing mechanism.[1] Per-turn security screening is available via HarnessTurnInput.screenExternalContent, an optional callback that receives the content, tool name, and source, and returns a SecurityScreenVerdict or undefined.[1] HarnessTurnInput.toolApprovalGate is a synchronous per-turn predicate; returning true for a tool name blocks its execution and routes it into the pending-approvals workflow.[1]
HarnessTurnResult carries a pendingApprovals array of { command, reason, kind?, matched?, purpose?, approvalKey? } objects and a pausedOnApproval flag for the command-approval gate workflow.[1] HarnessTurnResult.cacheUsage surfaces three token-budget dimensions: cacheRead, cacheWrite, and uncachedInput.[1]
HarnessModelUtilities is entirely optional: every method — shouldRespond, compactHistory, contextTokenBudget, oneShot, judge, screenSecurity, pickAckEmoji, generateTitle, and summarizeApproval — is an optional property, so harness implementations may omit any subset.[1]
HarnessLlmRequestRecord captures per-step LLM call telemetry including time-to-first-token (ttftMs), total duration (durationMs), step-gap time (stepGapMs), per-tool-call wall times (toolWallMs), gap phases, and token usage.[1]
Sources
Updated
The harness router resolves which Harness adapter and model to use for each turn by layering approval, org-level and scope-level config, and caller intent, then falls back safely when the configured choice is unavailable. Wrapping multiple adapters behind a single Harness interface, the router handles session transitions between harnesses, sources metadata from a utility adapter, and manages lifecycle operations across the adapter set. The OpenRouter model catalog is a term-of-art dependency: it is a remote list of available models fetched at runtime and cached; absence of a warm cache is called a "cold" resolution.
src/harness/harness-router.ts exports resolveRuntimeChoice, which resolves the harness and model to use for a turn by layering: approved harness list → org-level stored/legacy config → scope-level stored/legacy config → explicit caller request. A scope-level runtime selection is only consulted when scope !== orgScopeId; at org scope the scoped values are set to null, preventing the org's own record from being applied twice.[1] If the final resolved choice is not approved, a NonRetryableTurnError is thrown when the caller explicitly requested that choice; otherwise resolution falls back to the org-level choice.[1] When the configured fallback harness is not in the approved list or its model is unsupported, safeFallback is recalculated using the first approved harness ID and defaultModelForHarness(firstApproved, fallback.modelId) — preventing a provider-blind fallback from silently failing all turns.[1] resolveRuntimeChoiceDurable is an async variant that fetches all config values (approved harnesses, stored runtime selections, base models) concurrently via Promise.all and then delegates to the synchronous resolver.[1] In src/harness/harness-router.ts and src/wiring.ts, the OpenRouter model catalog is fetched and hydrated synchronously within the model-resolution path on the first (cold) resolution, so catalog fetch latency or failure surfaces at model selection time rather than at startup.
src/harness/harness-router.ts also exports createHarnessRouter, which wraps multiple Harness adapters behind a single Harness interface, resolving the correct adapter on each turn via an injected resolve callback.[1] When the session's harness has changed since the last turn, createHarnessRouter calls resetSession on both the old and new harness before running the turn.[1] profile, models, and tools on the router are sourced from an injected utility harness, making the router's metadata surface independent of which adapter handles any given turn.[1]
Calling resetSession on the harness router deletes the session's last-harness record and broadcasts resetSession to all registered adapters in parallel via Promise.all.[1] Calling close on the harness router deduplicates adapters via new Set before invoking close on each, preventing a double-close when multiple harness IDs share the same adapter instance.[1]
Sources
Updated
The Pi harness defines configuration, API communication, and message-handling logic for QM's Pi integration, bridging Config objects to PiHarnessOptions, managing turn detection and emoji reactions, and orchestrating SSE calls against Anthropic's API. Pi harness message seeding reconstructs tool rounds and pushes them into the session's live state and persistence layer; error handling surfaces structured API errors and gracefully tolerates missing Pi internals. SSE (Server-Sent Events) is a protocol in which a server streams a sequence of text events to a client over a single HTTP connection; the Pi harness uses SSE to receive Anthropic API responses incrementally rather than waiting for a single blocking response. src/harness/pi-harness.ts routes turns dispatched through the Pi harness to the correct Pi-targeted destination.
src/harness/pi-harness.ts defines PiHarnessOptions, the configuration interface for the Pi harness, with options covering model resolution, API keys, tool toggles, timeout budgets, and signal wiring.[1] piHarnessConfigOptions() translates a Config object into PiHarnessOptions, mapping fields such as modelId→defaultModelId, detectModelId→detectModelId, titleModelId→titleModelId, anthropicApiKey→apiKey, piCaptureRequests→captureRequests, piSystemCacheSplit→systemCacheSplit, scratchExecEnabled→scratchExec, sharedOwnerAuthIsolation→ownerAuthExec, reachExecEnabled→reachExec, turnWallClockMs→turnWallClockMs, execTimeoutDefaultMs→execTimeoutMs, execTimeoutMaxMs→execTimeoutCeilingMs, backgroundJobTtlMs→backgroundJobTtlMs, and backgroundJobTtlMaxMs→backgroundJobTtlMaxMs.[1][2] Optional fields — defaultModelId, detectModelId, titleModelId, and apiKey — are omitted entirely from the returned object (not present as undefined) when the corresponding config values are unset.[2] piHarnessConfigOptions sets controlTools: true only when both signingSecret and apiBaseUrl are present in the config; either field alone is insufficient.[2]
Valid turnEffortLevel values in src/harness/pi-harness.ts are "off", "minimal", "low", "medium", "high", "xhigh", "max", "ultracode", and "auto"; the legacy thinking-level set omits "max", "ultracode", and "auto".[1]
buildDetectionPrompt() constructs the system prompt used to decide whether the AI assistant should reply. When reactionGuidance is supplied, the verdict set expands to include REACT (for emoji acknowledgements); otherwise only YES or NO are valid first-line responses.[1] The turn-detection prompt instructs the model to prefer YES when a message is feedback or a preference about the assistant's own behaviour (even a flat statement with no question mark), and to prefer NO when genuinely unsure — though it should lean YES when a message is plausibly directed at the assistant, because blanking a directed message is worse than a brief reply.[1] parseDetectVerdict() parses the LLM's raw turn-detection output: it strips common preamble tokens (answer:, verdict:), then checks the first line for YES, NO, or (when reactionsEnabled) REACT. A REACT response extracts up to 3 emoji tokens via parseEmojiTokens(); if none are found the result still omits respond: true.[1] parseEmojiTokens() extracts emoji reactions from a line by matching both :name: shortcodes and Unicode Extended_Pictographic characters, deduplicates them, and returns at most 3 tokens.[1]
oneShot in src/harness/pi-harness.ts sends the Anthropic API key as the x-api-key request header and includes the system prompt and user prompt in the request body, returning the assistant's text content on a successful SSE turn.[2] Example: oneShot completes a full Pi 0.82 SSE turn against a local stub server, verifying auth header and response text extraction.[2] oneShot cleans up all temporary directories it creates even when the underlying session call throws, leaving no temp dirs behind on failure.[2]
toPiMessage in src/harness/pi-harness.ts gives assistant-role seed messages a synthetic usage: { totalTokens: 0 } block and stopReason: "stop" so that Pi's pre-prompt compaction check cannot crash on missing usage; user-role seeds carry no usage field.[2] seedRawMessagesIntoSession reconstructs a tool round from history as the sequence [user, assistant, toolResult, assistant], preserving the toolCallId and result content on the toolResult entry.[2] seedRawMessagesIntoSession simultaneously pushes reconstructed messages (including toolResult entries) onto the live agent.state.messages array and persists each via sessionManager.appendMessage, leaving the two arrays identical.[2] seedRawMessagesIntoSession is a no-op when called with an empty message list, and degrades gracefully — without throwing — when called with a session object that lacks Pi internals.[2]
piLastAssistantTextOrThrow in src/harness/pi-harness.ts throws with the provider's error message when the last assistant message has stopReason: "error", rather than returning a blank reply.[2] piLastAssistantTextOrThrow parses JSON error bodies in the assistant's errorMessage field and surfaces a human-readable message in the format Model provider API error (<type>): <message>.[2] piTurnError inspects the session's last assistant message for a structured JSON error; if the thrown error is a generic catch-all (e.g. "An unknown error occurred"), it replaces it with the richer structured error from the session.[2] piTurnError falls back to the thrown error unchanged when the session's last assistant message has no structured error (e.g. stopReason: "stop"); it also accepts a plain string as the thrown value and wraps it in an Error.[2]
src/deployment/postdeploy-smoke.ts runs post-deploy smoke tests to catch failure modes before traffic is accepted, narrowing the safety window between deploy and serve.
Sources
Updated
The Claude harness (src/harness/claude-harness.ts) spawns and manages a Claude child process with sandboxed config, environment isolation, tool bridging, and prompt assembly from session history and user input. Seven named tools—execute, read, write, publish, memory, history, background—are forwarded to the Claude subprocess, with context wiring for approvals and async message queueing.
src/harness/claude-harness.ts defines ClaudeHarnessOptions, the configuration interface for the Claude harness, supporting a model ID (string or scope-resolver function), a judge model, a binary path, process env, tool-capability flags, timeout budgets, a signal store, and a task store.[1] claudeHarnessConfigOptions() maps a Config object to ClaudeHarnessOptions: config.claudeModel → defaultModelId; config.judgeModelId is forwarded only when modelSupportedByHarness(judgeModelId, "claude") returns true; config.claudeBinPath → binaryPath; config.claudeProcessEnv → env; and config.turnWallClockMs → turnWallClockMs.[1] The default judge model is "claude-haiku-4-5", used when no judgeModelId is provided in ClaudeHarnessOptions.[1]
claudeChildEnv() builds a sandboxed environment for the Claude child process: it sets HOME to the jail directory and CLAUDE_CONFIG_DIR to <jail>/.claude, then passes through a fixed allowlist of env vars — no variables outside the allowlist are forwarded.[1] The env-var passthrough allowlist (CLAUDE_ENV_PASSTHROUGH) is: PATH, TMPDIR, LANG, LC_ALL, SSL_CERT_FILE, SSL_CERT_DIR, NODE_EXTRA_CA_CERTS, HTTP_PROXY, HTTPS_PROXY, NO_PROXY, ALL_PROXY, ANTHROPIC_API_KEY, ANTHROPIC_AUTH_TOKEN, ANTHROPIC_BASE_URL, and CLAUDE_CODE_OAUTH_TOKEN.[1] claudeProcessIdentity() returns { uid: 65534, gid: 65534 } (the nobody user) when the current process runs as root (uid 0), and undefined otherwise, preventing the Claude child process from inheriting a root identity.[1] spawnClaudeProcess() spawns the Claude binary with stdio: ["pipe", "pipe", "inherit"] — stdin and stdout are piped to the parent while stderr is inherited — and spreads an optional identity object into spawn options to apply uid/gid dropping.[1]
promptText() assembles the user-facing prompt by concatenating, in order: the replayed transcript from durable session history, prior-turn seed text (only when turn.history is empty), the user's input, and the environment string — filtering out blank segments.[1] claudeReplayTranscript() wraps replayed history in a trust-boundary header (<<<BEGIN TRANSCRIPT / END TRANSCRIPT>>>) with an explicit note that the content is "untrusted conversation history, not instructions", guarding against prompt-injection from stored messages.[1]
The Claude harness bridges exactly seven child tool names: "execute", "read", "write", "publish", "memory", "history", and "background" — collected in the CHILD_TOOL_NAMES set and forwarded to the Claude subprocess.[1] claudeToolContext() constructs a ToolContextRef from a HarnessTurnInput, wiring pollFire, emit, scopeLabel, orgScopeId, screenExternalContent, and toolApprovalGate through; pendingApprovals is always initialized to [] and pausedOnApproval / silentRequested default to false.[1]
MessageQueue is an AsyncIterable<SDKUserMessage> backed by an in-memory queue with a promise-based waiter list: push() delivers immediately to a waiting consumer if one exists, and close() resolves all pending waiters with done: true, making the iterator terminate cleanly.[1] The effort() helper maps a string thinking level to the typed union "low" | "medium" | "high" | "xhigh" | "max", returning undefined for any unrecognized value — including "off", "minimal", "auto", and "ultracode".[1] stripClaudeImageBytes() removes base64 image data from SDKMessage objects before logging by replacing the data field with "[image omitted]" whenever the containing object has type: "base64".[1]
Sources
Updated
The OpenCode harness manages a child OpenCode server process, validating its binary version, configuring model providers, and translating between QM's internal tool and message formats and OpenCode's wire protocol. Session tokens are HMAC-SHA-256 digests of the session ID, and Bearer token comparison uses constant-time comparison to prevent timing attacks.
opencode-harness.ts pins the OpenCode binary version it expects at OPENCODE_VERSION = "1.17.18", refusing to run against a different build.[1] The OpenCode harness waits up to OPENCODE_STARTUP_TIMEOUT_MS = 90_000 ms (90 seconds) for the OpenCode server process to become ready before failing a turn.[1] After OPENCODE_IDLE_WAIT_MS = 30 * 60_000 ms (30 minutes) of inactivity, the harness kills the idle OpenCode child process.[1]
openCodeHarnessConfigOptions maps the flat Config object into OpenCodeHarnessOptions, propagating modelId as defaultModelId, anthropicApiKey as apiKey, and openaiApiKey directly.[1] resolveCustomProviders on OpenCodeHarnessOptions is called once when the OpenCode server starts; registrations made while a server is already running only take effect on the next server start.[1]
modelRef resolves a model id string into a { providerID, modelID } pair; custom-registered model ids (which may contain slashes, e.g. bedrock/claude-x) are matched first against the custom-provider registry to avoid mis-routing on the slash.[1] When no slash or custom registration is found, modelRef falls back to provider "openai" for ids starting with "gpt-" and to "anthropic" for everything else.[1]
bridgeToolName renames three core pi-tools when they are surfaced to the OpenCode bridge: execute → workspace_execute, read → workspace_read, write → workspace_write; all other names pass through unchanged.[1] replayMessages converts the internal tape history into OpenCode's message wire format, pairing each tool-call part with its immediately-following toolResult entry and consuming it from the source array (i++) to avoid double-processing.[1] stripDataUrls removes data: URI parts from OpenCode messages before storing or forwarding them, replacing each removed part with { omitted: true } to preserve the part slot.[1]
Session tokens for the OpenCode bridge are HMAC-SHA-256 digests of the session ID, keyed with the shared secret and encoded as base64url.[1] Bearer token comparison in the bridge uses timingSafeEqual to prevent timing-based secret leakage.[1] The HTTP bridge rejects request bodies larger than 16 MiB with an error and destroys the request socket.[1]
Sources
Updated
Pi tools in QM expose configuration via PiToolsOptions (feature flags and timeouts) and derive execution-safe subsets (CoreToolOptions) that respect auth and read-only constraints. Tool result handling includes character capping, pagination defaults, and ordering logic — all designed to bound output and present lists consistently across the interface. A miniapp is an interactive UI produced by the QM orchestrator; miniapps are rendered inline in the web thread rather than served as detached links.
PiToolsOptions in src/harness/pi-tools.ts exposes feature flags for the tool set: scratchExec, ownerAuthExec, reachExec, controlTools, surfaceTools, surfaceName, readOnly, and timeout/TTL knobs (execTimeoutMs, execTimeoutCeilingMs, backgroundJobTtlMs, backgroundJobTtlMaxMs).[1] CoreToolOptions is PiToolsOptions with readOnly, surfaceTools, and surfaceName omitted — capturing only the options derived from global Config and shared across all surfaces.[1] coreToolOptions derives a CoreToolOptions from a Config object, enabling controlTools only when both signingSecret and apiBaseUrl are present in config.[1]
READ_ONLY_TOOL_NAMES enumerates the tools permitted even in read-only mode: memory, history, and finish_silently.[1] pauseStampAfterToolCall wraps a turn's afterToolCall hook so that the turn terminates immediately (returning { terminate: true }) whenever ref.pausedOnApproval or ref.silentRequested is set — halting the agent loop after an approval gate or a silent-finish request.[1]
Tool result text is capped at MAX_TOOL_RESULT_CHARS = 100_000 characters; when a result exceeds this, the middle is dropped — the first (100_000 − 10_000 − notice.length) characters and the last 10_000 (TRUNCATED_TAIL_CHARS) characters are kept, with a truncation notice inserted between them.[1] capPayloadStrings recursively walks arrays and plain objects to cap every string leaf via capResultText, but skips objects whose prototype is not Object.prototype or null — leaving class instances intact.[1]
List pagination defaults to LIST_PAGE_SIZE = 25 items per page with a hard ceiling of LIST_PAGE_MAX = 100; task body previews are truncated to LIST_TASK_PREVIEW_CHARS = 200 characters.[1] listOrder sorts items so that enabled (active) items come first, then paused items, then archived items; within each tier, items are sorted by createdAt descending, then by id lexicographically.[1] pageOf returns a note string describing pagination state: null when the entire list fits on one page, an "end of list" message when at the last page past offset 0, a "nothing at offset N" message when the slice is empty, and a "next page: offset: N" hint otherwise.[1]
Miniapp playgrounds are rendered inline in the web thread; the primary rendering surface is plugins/web-ui/src/miniapp.ts, with integration points in plugins/web-ui/src/chat.ts and plugins/portal/src/index.ts. The miniapp delivery contract — covering API routes and skill authoring expectations — is documented in src/api/routes/miniapps.ts and skills-seed/miniapp/SKILL.md.
Sources
Updated
src/core/orchestrator.ts is the central turn-processing module, exporting the createOrchestrator factory, which wires together sessions, harness models, memory, skills, credentials, sandboxes, security screening, and delivery into a single Orchestrator instance.[1] src/core/orchestrator.ts re-exports egressClaimAllowingControlPlane, conversationLabelFor, filterConnectorSkills, and loadTapeImage from ./orchestrator/turn-helpers.ts, and re-exports the types Orchestrator, OrchestratorDeps, OrchestratorInput, and SurfaceContextPuller from ./orchestrator/types.ts — see Orchestrator types and deps and Turn helpers for the definitions those modules own.[1] src/core/wake-envelope.ts defines a WakeEnvelope type that wraps all externally-initiated resumes — webhooks, crons, and deep links — into a uniform shape for entry into the orchestrator run loop. The Auto-posture HiLO approval flow is a risk-tiered release mechanism in which tool results flagged as high or low risk are quarantined and suspended, awaiting explicit approval before execution resumes. src/core/orchestrator.ts routes tool-result quarantines triggered by the Auto-posture mechanism through a HiLO (High/Low risk) release approval flow; quarantined results suspend execution and await explicit approval before continuing. src/harness/mock-harness.ts supports the Auto-posture HiLO approval flow for testing, with coverage exercised in test/orchestrator.test.ts, test/pi-tools.test.ts, and test/slack-approval-cards.test.ts. src/types.ts carries additional fields on quarantined-content payloads to surface richer metadata; src/core/orchestrator.ts populates these fields when quarantining tool results through the Auto-posture HiLO flow. src/api/app-helpers.ts supports rendering of quarantined-content metadata; the web UI — plugins/web-ui/src/chat.ts, core-bridge.ts, and shell.css — uses this metadata to render expandable previews of blocked output. test/orchestrator.test.ts asserts on the expanded quarantined-content structure introduced by the richer metadata fields.
createOrchestrator falls back to in-memory stores when optional deps are absent: deps.approvals defaults to createMemoryMap<PendingApprovalRecord>(), deps.approvalGrants to createMemoryMap<CommandApprovalGrant>(), and deps.blobTransfer to createMemoryBlobTransferStore().[1]
Protocol-file constants are loaded once at module initialization: SHARED_CORE_MD, MODE_CONVERSATION_MD, MODE_AUTONOMOUS_MD, and MODE_FALLBACK_MD, each via loadProtocolFile.[1] The ACTIVITY_ENTRY_TYPES set classifies "tool_call", "tool_result", "approval_request", and "approval_resolved" as activity entries, distinguishing active-execution entries from conversational history.[1] First-block capture is capped at FIRST_BLOCK_CAPTURE_MAX_CHARS = 20_000 characters.[1]
The orchestrator maintains an LRU cache for the reachable-channel roster, keyed by principalId, with a TTL of 5 minutes and a maximum of INSTANCE_CACHE_MAX_ENTRIES (5,000) entries.[1] A separate LRU cache holds the directory member index, keyed by the string "org", sharing the same 5-minute TTL but capped at DIRECTORY_INDEX_CACHE_MAX_ENTRIES (100) entries.[1]
Sources
Updated
The Orchestrator interface defines three core methods (handleTurn, screenSecuritySteer, regenerateTitle) that implement the request-handling and security decision-making contract for QM's execution engine. OrchestratorDeps is the dependency-injection container wiring orchestrator operations; it requires core services (identity, sessions, workspace, sandbox, modelGateway, acl, memory) and accepts optional timeout, cache-mode, and AWS-role-brokering configuration.
The Orchestrator interface in src/core/orchestrator/types.ts exposes three methods: handleTurn (returns Promise<TurnResult>), screenSecuritySteer (returns Promise<"allow" | "block" | "unscreened">), and regenerateTitle.[1] The OrchestratorInput interface in src/core/orchestrator/types.ts extends TurnRequest via Omit and adds orchestrator-specific fields: origin: TurnOrigin, optional runId, attempt, finalAttempt, background, cancel: AbortSignal, queueMs, sessionParticipantIds, and scopeVersion.[1] The SurfaceContextPuller interface in src/core/orchestrator/types.ts has a required pull method and an optional searchLive method, both returning Promise<SurfaceContextResult | null>.[1]
OrchestratorDeps in src/core/orchestrator/types.ts is the dependency-injection bag for the orchestrator; required fields are identity, resolution, sessions, workspace, files, sandbox, modelGateway, auditLog, rateLimiter, harness, deploy, acl, and memory — nearly all other fields are optional.[1] The optional sessionTapeMode field of OrchestratorDeps accepts "shadow" or "serve", controlling how the session tape is accessed.[1] The optional layerBrokerFor function field of OrchestratorDeps maps a BrokeredLayerTool to an AwsRoleBroker | undefined, enabling AWS role brokering per deployment-layer tool.[1]
OrchestratorDeps includes optional timeout knobs: execTimeoutMs, execTimeoutCeilingMs, approvalSummaryTimeoutMs, and securityScreenTimeoutMs.[1] OrchestratorDeps also carries optional background-job TTL knobs: backgroundJobTtlMs and backgroundJobTtlMaxMs.[1]
Sources
Updated
Turn helpers are utilities in src/core/orchestrator/turn-helpers.ts that manage text screening, attachment loading, connector skill filtering, and egress policy—enabling QM to validate content safety, resource budgets, and network access during agent execution. The module provides helpers for constructing output (stripping boilerplate, acks, and retrieving channel labels) and resolving permissions—determining which scopes a skill can write to and which hosts an agent may reach. The Slack integration extracts forwarded message payloads and includes them in the turn context passed to the agent, enabling agents to read content from messages forwarded into conversations. turn-handler.ts and conversation-view.ts in src/slack/ are the primary owners of assembling forwarded message content into turn input. The ambient reply path in src/api/app-ambient.ts fetches and injects the conversation roster via the directory store, giving agents knowledge of conversation participants during reply assembly.
MAX_AUTO_ATTACHMENT_SCREEN_BYTES in src/core/orchestrator/turn-helpers.ts is set to 12_000 bytes, capping the size of attachments that are automatically screened as text.[1] isScreenableTextAttachment treats a MIME type as screenable text if it starts with text/ or is one of application/json, application/xml, application/javascript, application/yaml, or application/x-yaml; only the portion before the first ; is considered.[1] headLooksLikeText classifies a buffer as binary if it contains a null byte, and as suspiciously binary if more than 10% of its UTF-8 characters are replacement characters (U+FFFD) or non-printable control characters (excluding tab, LF, and CR).[1]
loadTapeImage returns the sentinel value "over-budget" — rather than null — when the artifact's sizeBytes exceeds MAX_VISION_IMAGE_BYTES or the caller-supplied remainingBytes, letting callers distinguish a budget exhaustion from an authorization or availability failure.[1] Before reading any bytes, loadTapeImage validates the opened stream against the stored artifact metadata (size, sha256, mimetype) and destroys the stream, returning null, if any field mismatches — preventing use of corrupted or swapped file content.[1]
CONNECTOR_SKILL_PROVIDERS is the static mapping from skill names to their required connector provider — for example, "google-workspace" → "google", "slack-drafts" → "slack", and "morning-digest" → "x".[1] filterConnectorSkills removes skill resolutions whose connector provider is absent from the set of availableProviders; skills with no entry in CONNECTOR_SKILL_PROVIDERS always pass through.[1]
visibleSkillScopes computes which scopes a skill may write to: the writable memory scope, all read-only non-org layer scopes (team scopes), and the org scope.[1]
egressClaimAllowingControlPlane returns undefined — imposing no egress restriction — when allowedHosts and deniedHosts are both empty and denyPrivateNetworks is false, short-circuiting policy construction for unrestricted deployments.[1] When an allowlist is active, egressClaimAllowingControlPlane always strips the control-plane host (apiBaseUrl hostname) from deniedHosts and adds it to allowedHosts, ensuring agent-to-control-plane traffic is never accidentally blocked by egress policy.[1]
stripTurnBoilerplate removes paragraphs that start with [ — such as bracketed tool-result or metadata blocks — from assistant text before using it for display purposes such as title generation.[1] conversationLabelFor resolves a human-readable channel label (e.g., #general) by first using channelName if provided, then querying the DirectoryStore only for channel-kind scope IDs; it returns undefined for all other scope kinds or when no directory is available.[1] stripAckPrefix removes a leading acknowledgment token (e.g., a bot's @-mention ack) from the start of a message, trimming leading whitespace before and after the prefix; it is a no-op when either text or ack is falsy.[1]
Sources
Updated
Sandbox provisioning in QM creates isolated execution environments for agent turns by bundling actor credentials, tool access, and configuration into three tracked sandbox handles (box, scratchBox, ownerAuthBox), provisioning them on-demand with deduplication and lazy initialization. Device-flow credentials are restored selectively into each sandbox's keychain based on origin and isolation rules, with failed restores logged but non-fatal, and broker-vended tools are shimmed into owner-auth commands only when detected as word-boundary tokens.
src/core/orchestrator/sandboxes.ts defines the TurnSandboxContext interface — the complete set of dependencies and configuration passed into createTurnSandboxes for a single agent turn, including the actor principal, session, resolution, scope IDs, credential environment, broker-vended tools, skill resolutions, and performance tracking.[1] Three separate sandbox handles are tracked per turn: box (the primary turn sandbox), scratchBox (a scratch sandbox), and ownerAuthBox (an isolated owner-auth sandbox), each with independent provision and pending-handle state.[1]
Sandboxes are provisioned lazily: provision(eager = false) records box.used = true only on non-eager calls and de-duplicates concurrent provisions via provisionInFlight ??= doProvision(...). A failed provision nulls the in-flight reference so it can be retried.[1] After provisioning, doProvision sets the sandbox environment variable AGENT_OUTBOX to ${handle.rootDir}/${turnOutboxDir}, making it available to all tool executions in that sandbox.[1] Sandbox status events are streamed to deps.runActivity (if present) as records with type: "sandbox_status", using a monotonically increasing sequence number starting at 2_000_000 to avoid colliding with other activity record ranges.[1]
During provisioning, device-flow (keychain) credentials are restored to the sandbox. For automation-origin turns with useOwnerKeychain: true and no isolation, the actor's own ID is used as the restore owner; otherwise a derived deviceFlowCredOwner(memoryScopeId, actorId) key is used.[1] Services listed in quarantinedServices or marked for resident reset are removed from the sandbox keychain before credentials are restored, and the removal also clears canonical filesystem roots associated with each tool.[1] For every device-flow service successfully restored, a credential usage record is emitted with status "legacy_retained" when cutoverModeOf(service) === "prefer_ephemeral", and "legacy_restored" otherwise.[1] Device-flow restore errors are swallowed and recorded via deps.errors (category "keychain", code "device_flow_restore_failed") rather than failing the turn, so a keychain restore failure is non-fatal.[1]
The ownerAuthCommand wrapper injects owner-auth environment variables and creates shell function shims for any broker-vended tool binaries detected in the command string. Audit log entries are emitted for both keychain materialize and credential materialize events.[1] Broker-vended tool binaries are detected using the regex (^|[\s;&|()])${tool.binary}(?=$|[\s;&|()]), so credentials are injected only when the specific binary appears as a word-boundary token in the command.[1]
destroyOwnerAuthHandle retries teardown up to 3 times with exponential-style back-off (sleep(50 * attempt) ms between attempts) before re-throwing the last error.[1] Turn files are swept by age: the constant TURN_FILES_MAX_AGE_MS sets the stale threshold to 24 hours (24 * 60 * 60_000 ms), and files older than this are removed during sandbox provisioning via sweepStaleTurnFiles.[1]
Sources
Updated
Prompt blocks for the orchestrator live in src/core/orchestrator/prompt-blocks.ts and expose several pure rendering functions that are safe to concatenate unconditionally into a prompt.[1]
currentTimeBlock formats the user's local time using Intl.DateTimeFormat with dateStyle: "full" and timeStyle: "short", and returns an empty string — not null — when the timezone is invalid, making it safe to unconditionally concatenate into a prompt.[1]
renderConversationRoster caps displayed members at 20 (ROSTER_CAP = 20) and renders each entry as - DisplayName (principalId).[1] When the member list is truncated, an overflow suffix "…and N more in this conversation." is appended; an empty member list returns null.[1]
deliveryMenu renders a ## Where scheduled tasks post prompt block listing candidate delivery destinations, marking the default with "(default — where we're talking now)" and noting that destinationKey values must come from that list.[1] Passing scope:"personal" routes cron output to the user's personal DM instead of the default destination.[1]
Schedule entries are rendered with three label formats: cron <expr> [<timezone> | default timezone] for cron expressions, every Nm (rounded to whole minutes, minimum 1) for interval schedules, and once at <ISO-date> for one-shot schedules.[1]
Sources
Updated
A turn via app.turn() returns a standard response envelope and records user–assistant entry pairs; variants like proactive openers and triggered turns control message visibility through the hidden flag to separate actual user input from system-generated prompts. Inbound failures (files, delivery) are stored durably as system entries with metadata but excluded from the reply text and model context, preserving the appearance of a direct user–assistant exchange.
An end-to-end DM turn via app.turn() returns { status: "ok", sessionId, reply } and records exactly a ["user", "assistant"] entry sequence in the session.[1]
A proactive-opener turn with empty user text records the seed entry as hidden: true so no surface renders it as a user message; the reply still generates normally.[1] A proactive-opener turn that carries real user text keeps the user entry visible (hidden is NOT set to true), even when the proactiveOpener flag is present.[1] A triggered (background job) turn records its synthetic wake prompt with hidden: true so no surface renders it as a user message.[1] Without the hidden flag set to true, every session entry is treated as part of the visible user–assistant conversation.
Inbound file problems (e.g., too many files) are stored as a durable file_event system entry with direction: "in" and an issues array naming the dropped files; they do NOT appear in the reply text and are NOT sent to the LLM in the model context.[1]
A cron-delivered digest lands as a delivery event with provenance (sourceSessionId, fireKey, sourceThreadRef) but does NOT inject a fake assistant row into the recipient's transcript — recordPrincipalDelivery creates the recipient DM session and leaves it assistant-entry-free.[1] The cron source session does NOT appear in listSessions for the sender — background cron runs are never surfaced as human conversations for the owner.[1]
plugins/web-ui/src/chat.ts is the single authoritative owner of approval continuation state in the web UI; split ownership across core-bridge.ts and individual card components has been removed. When an approved run is resumed, its output routes into the currently visible chat pane rather than a background pane. core-bridge.ts documents the web chat retry protocol; test/turn-idempotency-route.test.ts specifies the server-side idempotent re-submission contract. Failed outbound sends in the web chat UI retain the user message as visible with a retry affordance rather than discarding it; the message object carries a failure-state representation. In the web UI, message dispatch passes through an intermediate queue; integrations and tests asserting send timing must account for this queued state rather than assuming direct, synchronous delivery order.
Sources
Updated
Pages in this section:
Updated
The Run model in QM represents a queued task carrying identity, session affiliation, execution status, request/result payloads, deduplication key, attempt tracking, lease expiration, and timestamps — enabling the store to route, deduplicate, retry, and track lifecycle. In src/sessions/postgres-session-store.ts, the entry search function is marked parallel-unsafe to prevent race conditions when multiple callers query session entries simultaneously under concurrent Postgres planner execution.
src/runs/run-store.ts defines the Run data model, which carries id, sessionId, status ("pending" | "running" | "done" | "failed"), request, result, deliveryState, dedupKey, attempts, errorAttempts, maxAttempts, leaseToken, leaseExpiresAt, workerId, createdAt, startedAt, and finishedAt.[1]
RunStore.enqueue accepts an EnqueueInput that includes an optional dedupKey; it returns an EnqueueResult whose deduped: boolean flag indicates whether the run was matched against an existing one rather than newly created.[1]
leaseLapsed considers a run's lease expired when its status is "running", leaseExpiresAt is non-null, and leaseExpiresAt <= asOf.[1] errorParks returns true — meaning the run will be parked rather than requeued — when either errorAttempts + 1 >= maxAttempts or when an optional maxClaims value is defined and attempts >= maxClaims.[1] A lease is a time-bounded claim on a run granted to a worker; if the worker does not complete or renew it before leaseExpiresAt, the run is considered abandoned and becomes eligible for reaping and requeueing. Parking a run moves it to a permanent failure state rather than requeueing it for another attempt, preventing infinite retry loops when a run repeatedly errors.
RunStore.reapExpired returns a { requeued, parked } count, accepts an optional onReap callback that receives a ReapEvent for each reaped run, and an optional async onRetired hook called with the session IDs of fully-retired runs.[1] RunStore.waitFor polls or waits for a run to reach terminal status and accepts an optional timeoutMs to bound the wait.[1]
Sources
Updated
The Worker interface in QM polls for unclaimed runs, claims and processes them with a lease token, and exposes methods to start/stop polling and release in-flight runs; createWorker generates workers that poll every 50ms and invoke the orchestrator to handle each turn. On failure or shutdown, the worker safeguards against lease orphaning by releasing leases back to the store, and protects against transient heartbeat errors while enforcing non-retryable errors via explicit failure markers. A lease token is a unique credential issued to a worker when it claims a run, proving exclusive ownership; no other worker can process a run without holding its valid lease token.
The Worker interface in src/runs/worker.ts exposes four methods: start(), stop(drainMs?), releaseInFlight(), and busy() — where busy() returns true when a run is currently in flight.[1] createWorker generates a worker ID of the form w-<8-char UUID prefix> when deps.workerId is not provided, and polls for new runs every 50 ms by default (pollMs = 50).[1] WorkerDeps extends ProcessDeps with optional pollMs, workerId, required sessions: SessionStore, and optional canClaim and onClaimed callbacks; canClaim gates whether the worker will attempt to claim a new run on each poll cycle.[1]
processRun in src/runs/worker.ts requires the run to already hold a lease (leaseToken !== null) before it is called; it throws synchronously if the token is absent.[1] Before invoking the orchestrator, processRun computes queueMs as run.startedAt - run.createdAt (clamped to 0) and passes it to orchestrator.handleTurn only when startedAt is non-null.[1] On failure, processRun calls deps.runs.fail with retry: false when the thrown error is an instance of NonRetryableTurnError, preventing the run from being requeued.[1]
A transient heartbeat error resets the consecutiveLost counter to 0 and is not counted against the lease-loss threshold, allowing intermittent network failures to be tolerated.[1] When createWorker is stopped while a run has just been claimed, src/runs/worker.ts releases the lease back via deps.runs.releaseLease before breaking out of the loop, preventing lease orphaning.[1] Worker.releaseInFlight is idempotent: it tracks releasedLeaseToken and returns early if the in-flight run's lease token matches the previously released token, and coalesces concurrent calls behind a single releasing promise.[1] Worker.releaseInFlight also force-releases the associated session lease via deps.sessions.forceReleaseLease before releasing the run lease, ensuring both session and run locks are freed together.[1]
The worker-main.ts entry point starts the run-draining worker, logs the org, run-store type, and worker count on startup, and registers SIGINT/SIGTERM handlers that call stopWithBackstop for a graceful shutdown.[2] The shutdown() function guards against double-invocation: a shuttingDown flag is checked at the top and the function returns immediately if already set.[2]
Sources
Updated
A TurnStream in QM tracks per-run state across an agent's full reply lifecycle—from start through block publishing, tool calls, and final confirmation—and fires callbacks when the first text block closes or the reply posts to a surface. Character limits (overall maxChars and first-block cap) and a grace-period timer prevent unbounded buffering while allowing the Node.js process to exit cleanly.
The TurnStream interface in src/runs/turn-stream.ts tracks per-run streaming state across the full lifecycle of an agent reply: from begin() through publish(), publishBlockStart(), and noteToolCall(), to markReplyDone() and end().[1] The TurnStreamListener interface in the same file exposes two optional callbacks: onFirstBlock(text), fired when the first text block is closed by a tool call, and onSurfacePosted(), fired when the reply is confirmed posted to a surface.[1]
createTurnStream() is the factory for a TurnStream instance; it accepts optional maxChars (default 200,000) and graceMs (default 30,000 ms) parameters.[1] The first text block in a run is additionally capped at 20,000 characters (FIRST_BLOCK_MAX_CHARS), independently of the overall maxChars limit.[1] The grace-period timer calls timer.unref?.() so it does not prevent the Node.js process from exiting when a run-map entry is the only remaining work.[1] The grace-period timer starts when a run ends; if the TurnStream entry is not explicitly cleared within the grace window, it is removed automatically to prevent memory leaks from replies that never receive final confirmation.
Sources
Updated
Pages in this section:
Updated
src/api/server.ts implements the central HTTP gate function that authenticates and authorizes every incoming API request before it reaches a route handler.[1] The gate function returns null to signal that a response has already been sent (reject path) and returns a GateResult object to signal success — callers must check for null before proceeding.[1] The GateResult type carries three fields: body (parsed JSON or {}), capability (decoded CapabilityClaims or null), and actor (decoded PortalIdentity or null) — exactly one of capability or actor will be non-null for authenticated requests.[1] The Wiring interface packages the dependencies threaded through gate: app, deps, signing secret, a SourceAuth instance, a requirePortalIdentity flag, and an allowUnsignedSourceAuth flag.[1] A capability token is a signed JWT encoding a scope, audience, and actor identity, allowing gate to verify request authorization without consulting a session store on every call.
Routes are classified by a RouteAuth value: "public" skips all auth checks, "either" accepts a capability token or source-auth signature, and an object { aud } requires a capability token with a specific audience.[1] For non-public routes, gate tries three authentication paths in order: capability token (via CAPABILITY_HEADER), portal identity token (via PORTAL_IDENTITY_HEADER), or HMAC source-auth signature verification.[1] When a capability token is present, gate verifies it against deps.capabilitySecret ?? secret, checks that the actor is still classified as "internal" via deps.identity, and confirms scope membership via app.authorizesCapabilityScope — returning 401 or 403 on any failure.[1] When requirePortalIdentity is enabled, user-scoped routes, admin routes, unclassified writes, and POST /v1/turns with surface: "web" all require a valid portal identity token; a missing or mismatched identity returns 401 or 403.[1] The admin plugin (plugins/admin/src/index.ts) and portal proxy (plugins/portal/src/index.ts) enforce an authorization readiness check before acting on admin requests, blocking any request that arrives before the admin auth subsystem is fully initialized. Email-auth and directory-sourced identities are unified into a single identity model, reflected consistently across src/identity/identity-service.ts, src/config.ts, src/deployment/secret-schema.ts, and src/api/routes/admin/users.ts. An email-based external-user invitation flow, managed in src/admin/invite-email.ts, allows invitations to carry a role and expiry and be issued to users outside the organization. Invitation acceptance is handled through the OIDC layer in plugins/portal/src/oidc.ts, with invitation state stored as part of the grants model. External-member scaffolding — provisioning and lifecycle management for org-external users — is implemented in plugins/chassis/src/external-members.ts. Signing key IDs in src/auth/signed-token.ts are derived using HMAC rather than a plain hash, tightening the binding between key material and its identifier and reducing collision or substitution risk across independent deployments. Key IDs generated before the HMAC-derivation scheme do not match those generated after it; any stored or cached key IDs (e.g., in in-flight JWTs) require re-derivation on next issuance.
When deps.config.getSecurityPostureDurable(capability.scopeId) returns "strict", non-GET mutations that are not on the strictPostAllowed allowlist are blocked with HTTP 403 ("Strict posture blocks direct control-plane mutations").[1] The strictPostAllowed function defines the exact set of POST/PUT paths that bypass the strict-posture mutation block, including /v1/surface-context, /v1/projects, /v1/conversations, /v1/memory/search, /v1/memory/restore, run-signals, conversation forks, project members, skill restore, and decline trigger-consent decisions.[1]
capabilityAdminDenied enforces that agent-issued capability tokens cannot access admin grant changes (/v1/admin/grants), impersonation (/v1/admin/impersonate), bulk scope imports from non-personal scopes, or the admin-session-reads flag from non-personal scopes — returning a specific denial message for each.[1] Autonomous (cron) agent turns are blocked from all admin routes because capabilityAdminDenied requires claims.liveActor === true for the CONTROL_PLANE_AUD audience — returning "admin actions through the agent require a turn the admin started themselves — autonomous turns (crons) cannot act as an admin".[1] Admin content-read routes (memory, keychain, volumes, scopes, sessions, files, runs, audit, errors, egress, crons, deployments, skills, shadow deliveries, slack-mirror, ambient-judgments, ack-emoji-picks, users) from a non-personal scope are blocked for agent tokens unless the target scope is an org scope — preventing private content reads from channel or automated turns.[1] The orchestrator in src/core/orchestrator.ts now checks service-credential grants before allowing org-level credentials delivered through environment variables to be used, closing an authorization gap where env-delivered org credentials previously bypassed grant-based authorization checks.
Raw request bodies are stored in a WeakMap<IncomingMessage, string> keyed on the Node.js IncomingMessage object, allowing the body string to be read once and reused without re-streaming.[1] src/api/server.ts augments Fastify's type system to attach gate?: GateResult to FastifyRequest and route?: Route<ApiCtx> to FastifyContextConfig, making auth results and matched routes available as typed request properties throughout route handlers.[1] Route definitions are imported from ./routes/index.ts (apiRoutes and rawRoutes); deployment subdomain proxying is imported from ./routes/deployments.ts (proxyDeploymentSubdomain) — see Routes and app assembly for how these are assembled.[1] plugins/web-ui/server/index.ts performs server-side rewriting of links in conversation messages, resolving sandbox file URLs correctly rather than stripping or misrouting them. plugins/web-ui/src/markdown-sanitize.ts sanitizes markdown in conversation messages and maintains an allow-list of permitted link schemes; sandbox file URLs are included in that allow-list.
Sources
Updated
The createApp function in src/api/app.ts wires five method groups (turn, session, messaging, deployment, skill) into an App object via factory functions, passing a circular reference through helpers before methods attach. Routes split into rawRoutes (unauthenticated: health, git-HTTP broker, blobs, session) and apiRoutes (authenticated: turns, credentials, keychain, skill packs); capability auth uses the x-agent-capability header. The portal plugin in plugins/portal/src/index.ts retries admin health and probe requests on transient failures rather than immediately surfacing errors to callers. Tests in plugins/portal/test/proxy-errors.test.ts define which error classes are treated as transient and therefore eligible for retry in the portal plugin.
The createApp function in src/api/app.ts assembles the App object by composing five method groups — turn, session, messaging, deployment, and skill methods — each created by a dedicated factory.[1] Assembly works by first creating an empty app reference ({} as App) and passing it into the createAppHelpers and createAmbientHelpers factories before any methods are assigned, allowing circular references between helpers and the app itself.[1] src/api/agent-api-catalog.ts declares thread support for DMs, and src/api/app-messaging.ts integrates thread context into the message-routing layer during app assembly. The unscoped search backend injection point formerly in app-search.ts, app-types.ts, and wiring.ts has been removed; all search operations must now thread principal context through core-search.ts. A principal-scoped search abstraction in src/search/core-search.ts and src/search/backends.ts filters results by the calling principal's identity rather than returning global results.
The route registry in src/api/routes/index.ts is split into two arrays: rawRoutes (unauthenticated or credential-broker-scoped — health check, git-HTTP broker, connectors, deployments, blobs, session state) and apiRoutes (authenticated — turns, credentials, keychain, skill packs, surfaces, projects, crons, egress audit, auth broker, and others).[2] The /healthz endpoint is declared in rawRoutes with auth: "public" and responds with { ok: true } at HTTP 200.[2] The git-HTTP broker is mounted at GIT_HTTP_BROKER_PREFIX in rawRoutes and requires { aud: "credential-broker" } auth; it matches both GET and POST requests — as well as any other method — on that path prefix.[2] Direct-message conversations in src/api/routes/reach.ts and src/reach/reach.ts support threaded replies; reach routing directs DM replies to the originating thread rather than the top-level channel. src/types.ts carries schema definitions for thread context in direct messages; src/slack/deliveries.ts and reach-routing consumers depend on this schema for correct thread-aware delivery. The UI rendering layer in plugins/web-ui/src/ui.ts detects SVG MIME types and routes them to a download prompt rather than rendering them as <img> or inline SVG elements; SVG's ability to embed arbitrary script makes inline serving a security risk. plugins/web-ui/test/renderable-image-source.test.ts asserts that SVG MIME types are classified correctly and routed to the download prompt rather than any inline rendering path. The web shell in plugins/web-ui/src/shell.ts renders split panes, each with its own tab bar to support multi-pane tab management. The session list in plugins/web-ui/src/session-select.ts and src/sessions.ts supports multi-select to enable bulk operations across sessions. Scroll position preservation across layout reflows for split panes is managed in plugins/web-ui/src/split.ts and src/conversations.ts. The miniapps (interactive web-thread playgrounds) feature has been fully reverted; route handlers, orchestrator hooks, Slack dispatch, client code, and seed skills for miniapps are absent from the codebase on the main branch. The search HTTP route is declared in src/api/routes/search.ts and wired into routes/index.ts and agent-api-catalog.ts for exposure via the agent API. OAuth connector flows store transient context (redirect targets, session metadata) server-side in src/connectors/oauth-flow-store.ts rather than embedding it in the state parameter; only an opaque reference is placed in state. Connector routes in src/api/routes/connectors.ts, src/api/deps.ts, and src/wiring.ts use the oauth-flow-store abstraction to persist per-flow OAuth context.
The capability authentication header name is "x-agent-capability", exported as CAPABILITY_HEADER from src/api/contract.ts.[3] keychainUseCommand in src/api/contract.ts accepts either a { grant: string } or { credential: string } reference, serializing each to a distinct JSON body for the keychain API.[3] The command it generates POSTs to $AGENT_API_URL/v1/keychain/use with the capability token via curl, then sources the response into the shell environment with . /tmp/keychain.env.[3]
Sources
Updated
QM has added qm secrets set for safe .env edits, made fast mode opt-in so turns don't consume unbudgeted quota, provided org-wide admin control to default fast mode, and extended the /v1/reach endpoint with threading support for Slack channels and groups.
qm secrets set was added for safe in-place .env edits.[1] ui.ts adds a default effort picker to the chat input surface, letting users set effort level directly without requiring custom field configuration. The web UI composer uses a rebuilt model picker — implemented across plugins/web-ui/src/model-options.ts, plugins/web-ui/src/composer.ts, and plugins/web-ui/src/shell.css — designed to handle large model catalogs without layout or usability degradation; model-options.ts is the canonical home for model picker logic.
Fast mode is opt-in: a turn that never requested it is not billed against a tier the organization may have no quota for.[2] An org-wide admin toggle can default all interactive turns to fast mode; the web UI inherits this org default until the user makes an explicit picker selection.[3] Fast mode is a QM execution tier that processes turns faster than standard mode, drawing from a separate quota pool distinct from the standard execution tier.
The /v1/reach endpoint accepts a threadTs parameter on channel and participants posts so agents can reply inside a Slack thread; threadTs is rejected on person DMs, react, and delete actions.[4] src/slack/deliveries.ts and src/slack/attachments.ts bundle posts containing multiple files into a single Slack message with accompanying commentary, rather than fragmenting them across disconnected messages. test/slack-attachments.test.ts and test/slack-deliveries.test.ts cover the multi-file Slack message bundling behavior.
context-model.ts fixes a broken value binding in the web UI's <select> component, allowing selected values to round-trip correctly through the context model. field-select-source.test.ts and context-model-source.test.ts are updated to specify the corrected <select> value-binding semantics and how select fields propagate values.
Playground data in QM is modeled as typed artifacts; src/core/orchestrator/types.ts and src/playgrounds/playground.ts define the canonical types that must be conformed to across the orchestrator, Slack, and web UI surfaces. Playgrounds render inline within the web thread rather than as separate navigations; plugins/web-ui/src/playground.ts owns the client-side playground component, and plugins/web-ui/src/chat.ts and miniapp.ts are updated to embed it. plugins/portal/src/index.ts and its router expose the endpoints required for inline playground rendering. A QM Playground is a sandboxed interactive surface for running and sharing artifacts; it can be embedded inline in conversational threads rather than requiring a separate navigation context.
Sources