Repository navigation
feat: managed policy for organization-wide T3 Code configuration - #27197
Conversation
A machine-wide policy file (/etc/t3code/policy.json, /Library/Application Support/T3Code/policy.json, C:\ProgramData\T3Code\policy.json) sets defaults and locks for server settings. Defaults fill keys the user's file leaves out; locks are laid over every settings read, including each project's overrides, and writes that try to change them are refused. Policy values are never persisted, so removing the file restores the user's own settings. A file this build cannot fully enforce stops the server. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ged policy `show` prints the policy that applies on this machine, or says there is none. `check <file>` validates a draft before an administrator deploys it, with the same decoding and enforcement checks the server runs at startup. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On web and desktop, a settings row whose key the environment's policy locks shows its control inert with "Managed by <organization>" and no reset. On mobile, server settings pages say which organization manages some of them; a locked write is refused with the server's message. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Administrators can set `limits.maxRuntimeMode`. Until now only MCP callers had a mode limit; the user's own commands were unlimited. The orchestrator reads the ceiling once from the policy summary and checks each planned command's events inside the thread's command lock. It refuses a thread created or switched above the ceiling, and refuses a turn started or steered on a thread already above it. The refusal names the organization and the ceiling. It records no receipt, so the same command succeeds once the mode is lowered. Renames, stops and lowering the mode still work on a thread above the ceiling. A caller's own limit is narrowed to the ceiling, and queued runs on a thread above the ceiling are held. A policy whose own defaultRuntimeMode is above the ceiling fails to load. A user's stored default, or a project default, above the ceiling reads back as the ceiling, so new threads keep working. The stored value is left unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A policy file can now say which clients a machine serves (`limits.clients`: desktop, local-browser, remote) and whether T3 Connect is allowed (`limits.connect`). - Startup checks the auth policy the server would run under against `clients` before binding, so a non-loopback host or Tailscale Serve is refused with an error naming the organization, never a silent fallback. `unsafe-no-auth` is refused under any policy file. - Without `remote`, EnvironmentAuth refuses to create pairing links (the startup link for this machine's browser still works) and revokes link-paired sessions and unused links on startup. Connect is treated as remote access, so it is off too. - With Connect off, CloudLink unlinks on startup and refuses link, relay-config, preference, health, and mint requests; the endpoint runtime never starts a connector; the relay client refuses to install; `t3 connect`, `link`, `login`, and `publish` print the policy message. Status, unlink, and logout stay available. - Clients see the effective limits in `ManagedPolicySummary.limits`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e import A managed policy can now set `limits.webhookTasks`, `limits.telemetry`, and `limits.browserCookieImport`. Leaving one out means no restriction; only `false` restricts. Clients see the flags in `ManagedPolicySummary.limits`. - Webhook tasks: the scheduled-task service refuses to save a webhook task, so WS RPC and the MCP tools behave the same. Existing webhook tasks are kept, carry `blockedByPolicy` in the list and MCP read models, and queued deliveries are skipped. The hook route answers 403 before any lookup, run, or delivery log. - Telemetry: product analytics is off under policy whatever T3CODE_TELEMETRY_ENABLED says. Policy cannot turn it on. - Cookie import: the desktop main process reads the policy file on each import and refuses when the policy forbids it. It also refuses when the file cannot be read or decoded. Refusals all use one message shape: "Example Corp's policy does not allow webhook tasks." Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ances A managed policy's `limits.providers` now lets an administrator allow only some provider drivers and pin the environment, binary path, and launch arguments of every instance of a driver (for example Claude through Bedrock). Disallowed drivers' instances stay in settings and show as unavailable with the policy's reason; creating or enabling one is refused, and built-in text generation, review-guide, and branch-naming selections no longer fall back onto them. Pinned fields are laid over instances at read time and never written to the settings file; locked ones refuse edits like locked settings do. Unknown drivers or fields a driver lacks fail the policy at load. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A managed policy can now set limits.updates: mode (auto, manual, off), an https feedUrl, and allowNightly. Clients see it in ManagedPolicySummary.limits.updates. The desktop main process reads the policy file itself. off stops all checks, manual keeps checks while downloads and installs stay user-initiated, feedUrl points electron-updater at a generic feed, and allowNightly: false refuses the nightly channel and holds an install already on nightly until its user switches to stable. A policy file the desktop cannot read or decode turns updates off and logs why. On the server, ServerSelfUpdate and `t3 update` refuse under off and refuse non-stable targets without nightly, with one message: "<organization> manages T3 Code updates." Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…very surface Clients and the server now phrase a policy refusal the same way: the runtime-mode ceiling, a switched-off capability, a disallowed provider, and managed updates each have one helper in the contract. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A hook reads an environment's policy summary, a wrapper shows a control inert with the policy's reason, and settings rows accept a managed reason so any row can be locked the way locked settings already are. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…m off When an environment's policy refuses remote clients, pairing links and the network and Tailscale toggles show inert with the policy's reason; they can still be turned off. When it refuses T3 Connect, linking, agent activity, webhook holding, guest sharing, and the onboarding publish step are unavailable, while link status and unlinking stay. Server updates an administrator manages show the reason instead of an update button, and update-all leaves those environments out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The composer's permissions picker, reached from the chip, the effort menu, the overflow menu, and mod+shift+a, keeps modes above the administrator's ceiling listed but disabled with the policy's reason. A thread or draft already above the ceiling cannot send; a notice above the composer explains why and switches it to the broadest allowed mode in one click. The default permissions setting disables the same modes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The add-provider dialog offers only drivers the policy allows, ACP Registry included. An instance of a disallowed driver shows its enable switch inert with the policy's reason, and the model picker shows why it is unavailable rather than calling it disabled in settings. Instance fields the policy pins (binary path, launch arguments, environment variables) are read-only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When the policy refuses webhook tasks, the schedule dialog disables the webhook trigger with the reason, and an existing webhook task shows the server's explanation instead of its URL, token, and secret controls. Pause, delete, and switching to a timer stay. Scheduled tasks save in a mode at or below the administrator's ceiling, since they have no mode picker. When the policy refuses cookie import, the import menu says so, and a refusal from the desktop shows the policy's message rather than a generic read failure. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When an administrator manages updates, the sidebar pill, update toasts, the About version row and channel picker, and the update-everything flow give the policy's message instead of offering an update or claiming this build cannot update. Nightly is unavailable when the policy disallows it. Usage data notices say when the policy turned analytics off, and About names the organization that manages the environment with its support link. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
When an environment's managed policy caps runtime modes, the thread settings sheet, the new-task sheet, and the scheduled-task mode picker show the modes above the ceiling disabled with the policy's reason. A new task's stale draft mode above the ceiling falls back to the default, and new scheduled tasks start at the ceiling instead of full access. A thread above the ceiling shows the reason above the composer with a button that switches it to the ceiling. Queued messages above the ceiling are handed back to the composer with the server's wording instead of failing settings sync, which retried forever. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…llows Every model picker drops drivers the environment's policy does not allow. A thread or draft already on one shows its model as unavailable, and sending explains that the organization's policy does not allow the provider instead of waiting for the server to refuse. Provider sign-in lists them no more. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… them Server settings pages disable each row or choice a managed policy locks on any selected environment, and name the organization in the row or section footer. The default permissions choices also disable modes above a managed ceiling. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… tasks New tasks no longer offer the webhook trigger when the environment's policy turns webhook tasks off. An existing webhook task keeps the option so it can switch to a timer, but shows the policy's reason in place of its URL and token rotation, and the task list shows the server's blocked reason. Pause, delete, and switching to a timer still work. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An environment with a managed policy says "Managed by <organization>" under its version and links to the organization's support page when the policy has one. When the policy turns updates off, the page shows the policy's reason in place of the update check and install actions, which the server refuses. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…rns off Connect Turning Live Activities or ongoing activity on or off links each environment to T3 Connect. An environment whose managed policy turns Connect off refuses that, which failed the whole change for every environment. Those environments are now skipped, like ones this session cannot configure. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The agent sign-in page loads without the server's config, so the approval details now carry the managed runtime-mode ceiling. Access levels above it are disabled and say which organization limits them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…e administrator's A user could beat `limits.maxRuntimeMode` without touching the thread's mode: `--dangerously-skip-permissions` in Claude's launch arguments, a wrapper script as any CLI's binary, a config directory with allow rules, an ACP registry command that starts in its own skip-approval mode, or `T3CODE_CODEX_LAUNCH_ARGS` in the server's environment. With a ceiling set, each driver's launch fields (binaryPath, launchArgs, homePath, shadowHomePath, ACP commandPath/commandArgs, OpenCode serverUrl, whichever its schema has) and its whole environment take only what `limits.providers.instances` pins, or the built-in default. Edits to them are refused as managed settings; stored values stay in the file and return once the policy is gone. Drivers launch without `T3CODE_CODEX_LAUNCH_ARGS`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… picks stay under it A run resumed after a restart reached the provider without the orchestrator's ceiling check, so a thread whose mode predates the policy kept running above it. RuntimePolicyV2 now refuses those, and the turn fails with the same "limits threads to" message the orchestrator uses. The runtime policy also carries the ceiling to adapters. An ACP session mode a user picks through `_t3/session-mode` or a mode config option is applied only when the flavor can place it under the ceiling; Antigravity maps its `default`/`auto_edit`/`yolo` modes, and other agents' modes are skipped. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…mode ceiling Imported threads were created in full access, above any ceiling, and the importer writes events directly, so the orchestrator never refused them. Under a ceiling they now take the project's default runtime mode, which the policy already keeps under it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Such a task saved fine and then failed every run as dispatch_failed. Saving an enabled task above the ceiling now fails with the policy's message, so every surface (WS, MCP) explains it up front. A disabled task can still be saved, so one stored above the ceiling can be turned off. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A project override with enabled: true on an instance of a driver the policy leaves out saved, while the same change on the instance itself was refused. Both are now refused the same way. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…lease (#27322) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
| Effect.catchTags({ | ||
| // Only a CLI that is not there; one that will not run might still front a daemon. | ||
| TailscaleCommandSpawnError: (error) => | ||
| isMissingCli(error.cause) ? Effect.succeed([]) : Effect.fail(error), |
There was a problem hiding this comment.
🟠 High policy/managedLimits.ts:106
When tailscale is missing or its path produces ENOTDIR, closeTailscaleServeTo treats the lookup as an empty mapping list and lets the server bind while a persistent Tailscale Serve mapping may still expose it to remote clients. Fail closed on these spawn errors instead of treating them as proof that no forwarding exists.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/server/src/policy/managedLimits.ts around line 106:
When `tailscale` is missing or its path produces `ENOTDIR`, `closeTailscaleServeTo` treats the lookup as an empty mapping list and lets the server bind while a persistent Tailscale Serve mapping may still expose it to remote clients. Fail closed on these spawn errors instead of treating them as proof that no forwarding exists.
| const runtimeModeCeilingDenial = managedRuntimeModeDenial(managedPolicy, compatibleRuntimeMode); | ||
| const runtimeModeCeilingFallback = | ||
| runtimeModeCeilingDenial === undefined | ||
| ? undefined | ||
| : clampManagedRuntimeMode(managedPolicy, compatibleRuntimeMode, compatibleRuntimeModes); |
There was a problem hiding this comment.
🟠 High chat/ChatComposer.tsx:922
A persisted auto draft on Pi remains sendable under an auto-accept-edits ceiling, but the server rejects its submissions because useComposerSend dispatches the original auto mode. compatibleRuntimeMode has already normalized that mode to approval-required, so the ceiling check misses the violation; check and clamp using runtimeMode instead.
- const runtimeModeCeilingDenial = managedRuntimeModeDenial(managedPolicy, compatibleRuntimeMode);
+ const runtimeModeCeilingDenial = managedRuntimeModeDenial(managedPolicy, runtimeMode);
const runtimeModeCeilingFallback =
runtimeModeCeilingDenial === undefined
? undefined
- : clampManagedRuntimeMode(managedPolicy, compatibleRuntimeMode, compatibleRuntimeModes);
+ : clampManagedRuntimeMode(managedPolicy, runtimeMode, compatibleRuntimeModes);🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/components/chat/ChatComposer.tsx around lines 922-926:
A persisted `auto` draft on Pi remains sendable under an `auto-accept-edits` ceiling, but the server rejects its submissions because `useComposerSend` dispatches the original `auto` mode. `compatibleRuntimeMode` has already normalized that mode to `approval-required`, so the ceiling check misses the violation; check and clamp using `runtimeMode` instead.
| ]); | ||
|
|
||
| /** Legacy settings the server derives from another on every write; manage that one instead. */ | ||
| const DERIVED_FROM: ReadonlyMap<string, string> = new Map([ |
There was a problem hiding this comment.
🟠 High policy/managedSettings.ts:30
A lock on automaticGitFetchInterval (or providerHealthRefreshInterval / backgroundActivityProfile) is accepted but does not enforce the effective setting: with backgroundActivity.profile: "performance", resolveServerBackgroundActivitySettings ignores the legacy interval, so VcsStatusBroadcaster continues fetching every 15 seconds despite a lock of 60000. Reject these legacy paths in favor of the canonical settings, or enforce their effective equivalents.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/server/src/policy/managedSettings.ts around line 30:
A lock on `automaticGitFetchInterval` (or `providerHealthRefreshInterval` / `backgroundActivityProfile`) is accepted but does not enforce the effective setting: with `backgroundActivity.profile: "performance"`, `resolveServerBackgroundActivitySettings` ignores the legacy interval, so `VcsStatusBroadcaster` continues fetching every 15 seconds despite a lock of `60000`. Reject these legacy paths in favor of the canonical settings, or enforce their effective equivalents.
… reads, and rollout checks (#28000) Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Adamulek123 <adam.bogucki2018@gmail.com> Co-authored-by: maria-rcks <maria@kuuro.net>
| for (const [driver, known] of KNOWN_DRIVERS) { | ||
| const pin = pins.get(driver); | ||
| const bypasses = pin === undefined ? [] : Object.keys(PIN_BYPASS_FIELDS.get(driver) ?? {}); | ||
| if (maxRuntimeMode !== undefined) { |
There was a problem hiding this comment.
🟠 High policy/managedProviders.ts:319
With limits.maxRuntimeMode set, applyLimits deletes administrator-locked launch settings unless they are duplicated in limits.providers.instances, so effective settings no longer honor accepted settings locks; for example, a locked Claude binaryPath and gateway environment are replaced with no binary path and an empty environment. This branch builds its config rule from the provider pin (which can be absent) and sets environment: "all", overwriting values already applied by managed.applyLocks; preserve those locked values or reject the conflicting policy combination.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/server/src/policy/managedProviders.ts around line 319:
With `limits.maxRuntimeMode` set, `applyLimits` deletes administrator-locked launch settings unless they are duplicated in `limits.providers.instances`, so effective settings no longer honor accepted settings locks; for example, a locked Claude `binaryPath` and gateway environment are replaced with no binary path and an empty environment. This branch builds its config rule from the provider pin (which can be absent) and sets `environment: "all"`, overwriting values already applied by `managed.applyLocks`; preserve those locked values or reject the conflicting policy combination.
# Conflicts: # apps/desktop/src/backend/DesktopServerExposure.ts # apps/mobile/src/features/settings/SettingsScheduledTasksRouteScreen.tsx # apps/mobile/src/features/threads/ThreadDetailScreen.tsx # apps/server/src/cli/connect.ts # apps/server/src/cli/update.test.ts # apps/server/src/cloud/ManagedEndpointRuntime.ts # apps/server/src/orchestration-v2/Orchestrator.ts # apps/server/src/orchestration-v2/ProviderTurnStartService.ts # apps/server/src/orchestration-v2/RuntimePolicy.test.ts # apps/server/src/project/AgentSessionImporter.test.ts # apps/server/src/provider/ProviderInstanceRegistry.ts # apps/server/src/provider/ProviderInstanceRegistryHydration.ts # apps/server/src/provider/agentContextBudget.json # apps/server/src/scheduledTasks/ScheduledTaskService.test.ts # apps/server/src/scheduledTasks/ScheduledTaskService.ts # apps/server/src/serverSettings.ts # apps/server/src/telemetry/AnalyticsService.ts # apps/web/src/components/auth/ConnectAgentSurface.tsx # apps/web/src/components/chat/ComposerFooterControls.tsx # apps/web/src/components/settings/AddProviderInstanceDialog.tsx # apps/web/src/components/settings/ProviderInstanceCard.tsx # apps/web/src/components/settings/ProviderSettingsForm.tsx # apps/web/src/components/settings/StorageSettings.tsx # apps/web/src/components/sidebar/updateEverything.logic.test.ts # evals/mcp-tool-use/cases/visual-preview.ndjson # packages/provider-acp/src/server/adapter.ts # packages/provider-core/src/server/instanceEnvironment.ts
| idPrefix={idPrefix} | ||
| variant={variant} | ||
| onChange={onChange} | ||
| managedReason={managed?.keys.has(field.key) ? managed.reason : undefined} |
There was a problem hiding this comment.
🟡 Medium settings/ProviderSettingsForm.tsx:690
Local ACP instances under maxRuntimeMode still expose editable commandArgs, even though the server marks config.commandArgs as managed, so policy rejects the edits without explaining why. managedReason reaches only the schema-derived rows; pass the lock reason to ProviderCommandArguments and disable its Add, edit, and Remove controls when locked.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/components/settings/ProviderSettingsForm.tsx around line 690:
Local ACP instances under `maxRuntimeMode` still expose editable `commandArgs`, even though the server marks `config.commandArgs` as managed, so policy rejects the edits without explaining why. `managedReason` reaches only the schema-derived rows; pass the lock reason to `ProviderCommandArguments` and disable its Add, edit, and Remove controls when locked.
|
|
||
| const loadSettingsFromDisk = Effect.gen(function* () { | ||
| let settings = DEFAULT_SERVER_SETTINGS; | ||
| let settings = managed.defaults; |
There was a problem hiding this comment.
🟠 High src/serverSettings.ts:757
Policy-provided inline secrets are copied into the user secret store and can remain active after the policy is removed. Because loadSettingsFromDisk starts from managed.defaults, its migration path treats those values as user settings, redacts them, and persists the changed settings; keep policy-derived secrets out of that path or compare against equivalently transformed defaults without storing them.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/server/src/serverSettings.ts around line 757:
Policy-provided inline secrets are copied into the user secret store and can remain active after the policy is removed. Because `loadSettingsFromDisk` starts from `managed.defaults`, its migration path treats those values as user settings, redacts them, and persists the changed settings; keep policy-derived secrets out of that path or compare against equivalently transformed defaults without storing them.
| {rows.length > 0 ? ( | ||
| <div className="mt-3 min-w-0 space-y-2 pb-2"> | ||
| {rows.map((variable, index) => { | ||
| if (props.managed?.names.has(variable.name)) { |
There was a problem hiding this comment.
🟡 Medium settings/ProviderInstanceCard.tsx:437
A user-created or renamed variable disappears from the editor when its name matches a managed variable, leaving it impossible to correct or remove. This check hides rows based on their current draft name, and reconcileRowsDraft retains that draft when the environment is unchanged; reject managed names before updating the draft or distinguish policy-provided rows from user rows.
🚀 Reply "fix it for me" or copy this AI Prompt for your agent:
In file @apps/web/src/components/settings/ProviderInstanceCard.tsx around line 437:
A user-created or renamed variable disappears from the editor when its name matches a managed variable, leaving it impossible to correct or remove. This check hides rows based on their current draft name, and `reconcileRowsDraft` retains that draft when the environment is unchanged; reject managed names before updating the draft or distinguish policy-provided rows from user rows.
…able where needed - A policy whose settings hold a credential is refused: the server would move it into the user's secret store, where it would outlive the policy. - A user variable cannot be renamed to a pinned name, which would hide it as the policy's. - A locked ACP argument list shows read-only with the policy's reason. - A disabled webhook task keeps its fields visible while webhook tasks are off, so it can be fixed and saved. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
**v0.1.7-nightly.20261101.4048** · 47 changes from 33 contributors - **Organization-wide policies.** IT teams can centrally configure approvals, provider gateways, updates, and access to T3 Connect. - **Pointer lock in browser preview.** Test games and 3D viewers that capture the mouse in the desktop browser preview. - **GitLab review status.** See approval badges and filter merge requests by review status. - **PDF reading position.** PDF previews keep their page and zoom when you switch threads. ### Plus 39 more bug fixes and performance improvements. <details> <summary>Full changelog</summary> ## What's Changed * fix(cli): close legacy database before deleting it on Windows by @UtkarshUsername in pingdotgg/t3code#27629 * perf(cursor): load the agent SDK only when needed by @Yash-Singh1 in pingdotgg/t3code#28718 * fix(mobile): restore the reserved compose space on Android by @PixPMusic in pingdotgg/t3code#28819 * fix(mobile): thread links show their thread's title by @juliusmarminge in pingdotgg/t3code#26485 * fix(web): ChatView browser test no longer times out on CI by @juliusmarminge in pingdotgg/t3code#28834 * fix(mobile): sent messages rise into place instead of dropping in by @mozzius in pingdotgg/t3code#28660 * feat: managed policy for organization-wide T3 Code configuration by @juliusmarminge in pingdotgg/t3code#27197 * fix(server): force-stop timed out login shell probes by @hbw00111 in pingdotgg/t3code#7295 * fix(server): finish short reads before returning workspace text by @saphid in pingdotgg/t3code#14989 * perf(client-runtime): merge shell deltas once per received batch by @saphid in pingdotgg/t3code#18416 * perf(web): coalesce overlapping file tree refreshes by @saphid in pingdotgg/t3code#18421 * perf(clients): bound subagent preview processing to the visible prefix by @luke2x in pingdotgg/t3code#21277 * fix(server): avoid transient GitHub mergeability sync churn by @saphid in pingdotgg/t3code#22022 * test(server): stopped-queue steering test no longer races queue promotion by @tris203 in pingdotgg/t3code#22529 * fix(mobile): single-comment PR discussions say "1 comment", not "1 comments" by @MarcoDotIO in pingdotgg/t3code#22926 * fix(client): reject incomplete GitHub owner paths before lookup by @naveed949 in pingdotgg/t3code#23024 * fix(server): accept case-insensitive MCP bearer schemes by @Rish-it in pingdotgg/t3code#23782 * fix(shared): keep Azure clone URLs valid when switching protocols by @Zacharon in pingdotgg/t3code#24005 * fix(web): honor editableFocus in shortcut conditions by @unmanbearpig in pingdotgg/t3code#24134 * fix(scripts): launch check:changed on Windows without a vp shim by @Zacharon in pingdotgg/t3code#24164 * fix(web,mobile): time out client activity reports that never settle by @skyhong2002 in pingdotgg/t3code#24362 * fix(web): the welcome wizard keeps the project selection when you go back a stage by @Jacksondr5 in pingdotgg/t3code#24719 * fix(forgejo): merged and closed pull request lists only return their own rows by @rishabhx29 in pingdotgg/t3code#24772 * fix(clients): background work Stop stays pending until the stopped tasks leave by @Srijan-D in pingdotgg/t3code#25002 * fix(server): device list warnings keep the hub's error detail by @Jacksondr5 in pingdotgg/t3code#25248 * perf(client-runtime): index checkpoint answers in one bounded scan by @saphid in pingdotgg/t3code#25258 * fix(desktop): allow pointer lock in the built-in browser by @msfeldstein in pingdotgg/t3code#25275 * fix(desktop): keep preview download settings out of app exports by @mayesidev in pingdotgg/t3code#25428 * fix(web): PDF previews keep their page across thread switches by @ashx-j in pingdotgg/t3code#25496 * fix(server): unlink_pull_request reports a dismissed stack PR as not linked by @MarcoDotIO in pingdotgg/t3code#25561 * fix(client-runtime): re-pairing at a new address uses the new token by @abdulsalamgs in pingdotgg/t3code#25591 * docs(orchestration-v2): Codex rollback uses thread/revert on paginated threads by @SouthCarpet in pingdotgg/t3code#25705 * fix(web): keep inward mini browser resizes from growing by @diddletime in pingdotgg/t3code#26027 * fix(web): PR merge buttons stay in place while the stack loads by @GisTypical in pingdotgg/t3code#26077 * fix(web): Arabic paragraphs with many English terms now read right to left by @ahmedverona34-bot in pingdotgg/t3code#26095 * fix(web): dark-mode <picture> images load instead of showing "Image unavailable" by @Vikas-Maur in pingdotgg/t3code#26398 * fix(server): foreground Claude forks no longer show as finished the moment they start by @henrychu04 in pingdotgg/t3code#26459 * perf(server): hydrate only the requested project page by @Adamulek123 in pingdotgg/t3code#26655 * perf(effect-acp): frame large ACP stdout lines in linear time by @hieu-lee in pingdotgg/t3code#26686 * perf(shared): fold command output without copying scrollback per chunk by @hieu-lee in pingdotgg/t3code#26691 * perf(web): composer serializes each draft doc once per keystroke by @hieu-lee in pingdotgg/t3code#26720 * fix(gitlab): merge requests show their approval state by @oss96 in pingdotgg/t3code#26877 * test: remove unreachable sidebar helper suites by @t3-code-bot[bot] in pingdotgg/t3code#26920 * perf(clients): thread search reuses per-thread fields across keystrokes by @hieu-lee in pingdotgg/t3code#26936 * perf(mobile): reuse chat lists across navigation renders by @PixPMusic in pingdotgg/t3code#27026 * test(web): pass stale to the stack menu refusal test by @maria-rcks in pingdotgg/t3code#28887 * fix(server): keep a gateway's Claude reset time on a direct 429 by @tyler-b4innova in pingdotgg/t3code#25438 ## New Contributors * @mozzius made their first contribution in pingdotgg/t3code#28660 * @hbw00111 made their first contribution in pingdotgg/t3code#7295 * @Rish-it made their first contribution in pingdotgg/t3code#23782 * @Zacharon made their first contribution in pingdotgg/t3code#24005 * @Srijan-D made their first contribution in pingdotgg/t3code#25002 * @msfeldstein made their first contribution in pingdotgg/t3code#25275 * @abdulsalamgs made their first contribution in pingdotgg/t3code#25591 * @diddletime made their first contribution in pingdotgg/t3code#26027 * @GisTypical made their first contribution in pingdotgg/t3code#26077 * @ahmedverona34-bot made their first contribution in pingdotgg/t3code#26095 * @Vikas-Maur made their first contribution in pingdotgg/t3code#26398 * @oss96 made their first contribution in pingdotgg/t3code#26877 * @tyler-b4innova made their first contribution in pingdotgg/t3code#25438 **Full Changelog**: pingdotgg/t3code@v0.1.7-nightly.20261101.4035...v0.1.7-nightly.20261101.4048 </details> Upstream release: https://github.com/pingdotgg/t3code/releases/tag/v0.1.7-nightly.20261101.4048
… (Typechecking apps/server) Failing check: Typechecking apps/server "Typechecking apps/server" failed on 14 errors across three test files. The fixes below clear all of them: the apps/server typecheck now passes, `check_seams` passes, and the three test files pass (17 tests). **Why it failed.** Two upstream changes broke tests: - **Managed policy (pingdotgg#27197).** `AnalyticsService` and `ServerSelfUpdate.make()` now need a new `ManagedPolicy` service. Two fork-owned tests never provided it, so they didn't typecheck: `apps/server/src/fork/runtime-owner/selfUpdateSwitch.test.ts` and `apps/server/src/fork/telemetry-off/AnalyticsService.test.ts`. - **New scheduled-task tests.** Upstream added two tests to `apps/server/src/scheduledTasks/ScheduledTaskService.test.ts` that mock `ManagedProjectFolders` with `namedProjectsRoot`. The fork's `file-placement` seam removed that field, and the four older mocks in the same file already carry the seam. **What I changed.** - In both fork tests I now provide `ManagedPolicy.layerTest()`, the same way upstream's matching tests do. Upstream's selfUpdate test merges it next to `ServerConfig.layer`; I did the same in the switch test and added it to the platform layers in the telemetry-off test. - In `ScheduledTaskService.test.ts` I replaced the two new `namedProjectsRoot: "/projects"` lines with the existing `// fork: file-placement` marker line, just as the older four do. - I raised that file's `file-placement` count in `docs/fork/seams.tsv` from 4 to 6.
… (Running the fork's tests in apps/desktop) Failing check: Running the fork's tests in apps/desktop **What failed:** one test in `apps/desktop/src/backend/DesktopServerExposure.test.ts`, "starts local-only, Serve off, when a managed policy keeps clients local". Upstream added it in pingdotgg#27197 along with the managed policy feature. It turns Tailscale Serve on while a policy limits clients to `["desktop"]`, and it expects the choice to be saved but not applied. Instead, `setTailscaleServeEnabled` threw `DesktopTailscaleServeRefusedError` with reason `not-logged-in`. **Why:** the fork's `tailnet-ownership` seam in `setTailscaleServeEnabled` checks Serve on the machine before saving the setting, and refuses if a Serve claim would fail. Upstream's policy code (`withinPolicy` / `remoteAllowed`) now keeps Serve off whenever the policy keeps clients local. The seam didn't know about that, so it still ran the check. Tailscale isn't logged in on the test machine, so the check refused, though the backend would never have used Serve. **What I changed:** - **`apps/desktop/src/backend/DesktopServerExposure.ts`:** the seam's Serve check now runs only when the policy allows remote clients (`input.enabled && remoteAllowed`). I added a `// fork: tailnet-ownership` marker above that statement and a one-line comment explaining why. Upstream code is untouched. - **`docs/fork/seams.tsv`:** raised that file's marked-line count from 20 to 21. **Checks run:** - The test file now passes, 13 of 13. - `check_seams`, the format check and lint on the file all pass. - The desktop typecheck printed no errors, but its output ended at the cache-miss line, so I didn't see a final pass/fail result. The integration's full rerun will confirm it.
… (Running the fork's tests in apps/mobile) Failing check: Running the fork's tests in apps/mobile **What failed:** In apps/mobile, `src/reactCompilerCoverage.test.ts` failed on "compiles src/features/threads/ThreadSettingsSheet.tsx". The React Compiler skipped the file and reported `PreserveManualMemo: Existing memoization could not be preserved` twice at line 534. No other mobile test failed. **Why:** Line 534 is a `fork: library` seam: a `discardPendingModel` function wrapped in `useCallback(..., [])`. Upstream's managed-policy commit (pingdotgg#27197) changed the same component, `ThreadSettingsSessionProvider`, so it now runs `runtimeModeChoices` through `withManagedRuntimeModeLimits(..., [config?.managedPolicy])`. After that change the compiler would no longer keep the fork's manual `useCallback`, so it gave up on the whole file. Upstream's own code compiles fine. The fork seam is what tripped the check. **What I changed:** I edited only the seam lines in `apps/mobile/src/features/threads/ThreadSettingsSheet.tsx`. `discardPendingModel` is now a plain arrow function, the `fork: library` marker is still on it, and the compiler memoizes it itself. Its only body is two state setters, so it stays stable for the `useFocusEffect` in `ThreadSettingsDetailsScreen`. The `useCallback` import is still used there. I didn't touch any upstream code, and the file did not need to go on the bailout list. The full `reactCompilerCoverage.test.ts` now passes (1239/1239), and lint and `check_seams` report nothing on the change.
Companies that want to roll T3 Code out to their teams have no way to configure it centrally. Every setting belongs to whoever runs the app, so an IT team can't require approvals, keep phones and T3 Connect off a machine, pin providers to a company gateway, or control updates.
Docs: https://github.com/pingdotgg/t3code/blob/t3/managed-policy/docs/user/managed-policy.md
This adds a managed policy, which an administrator deploys with their usual device management tool, in one of these places:
com.t3tools.t3code/Library/Application Support/T3Code/policy.jsonPolicy(REG_SZ, JSON) underHKLM\SOFTWARE\Policies\T3Tools\T3CodeC:\ProgramData\T3Code\policy.json/etc/t3code/policy.jsonA machine with both a native policy and a file refuses to start rather than merging them.
The server on that machine enforces it for every client: desktop, browser, mobile, MCP agents, scheduled tasks and the CLI.
What a policy can do
settings: a default or a lock for any server setting, by path. Locked controls show "Managed by ⟨org⟩" on web and mobile. Policy values are never written into the user'ssettings.json, so removing the file brings back what users had chosen. Locks also apply inside per-project overrides.limits.maxRuntimeMode: the broadest permission mode allowed. It's enforced in the orchestrator for every command, including turn start, MCP agent sign-in, scheduled tasks and ACP session modes. A thread already above the cap is refused new turns, with a one-click switch down, and is never silently lowered. Under a cap, provider launch settings and the provider environment come from the policy only, because wrapper binaries, launch flags orNODE_OPTIONScould otherwise turn off a provider's approvals.limits.clients(desktop,local-browser,remote) andlimits.connect: the server refuses to bind wider than allowed, stops issuing pairing links, and revokes earlier pairings at startup. Withoutremote, T3 Connect is off too.limits.webhookTasks,browserCookieImportandtelemetry: turn each one off. Telemetry off covers product analytics and the server's and desktop's relay traces.limits.providers: an allowed driver list, plus fields set on every instance of a driver (env vars, base URL, binary path, launch args), locked or as defaults. This is how an admin routes Claude through Bedrock or a gateway.limits.updates:mode: "off", a desktopfeedUrlmirror, andallowNightly: false.t3 policy showandt3 policy check <file>.Any problem in the file stops the server: invalid JSON, an unknown key, or a wrong type. A key this build can't enforce would otherwise look enforced. Admin docs are in
docs/user/managed-policy.md.Not covered yet
~/.claude,~/.codex). The docs point admins to each provider's managed settings.feedUrlonly redirects the desktop app;t3 updatestill goes through npm and GitHub.Verification
vp run check:changedpasses: format, lint, knip, typecheck of 36 packages, and 75 test files (893 tests). It includes new tests for the settings overlay, lock vetting, the mode cap in the orchestrator, auth and Connect refusals, webhook tasks, provider hydration and update policy.Nothing has been checked in a running client yet, so there are no screenshots. Still to check by hand: the managed rows in web and mobile settings, the disabled mode options and switch-down button in the composer, and the provider environment editor locking.
Built with Claude Opus 5.5 in Claude Code, inside T3 Code.
🤖 Generated with Claude Code