Skip to content

feat: managed policy for organization-wide T3 Code configuration - #27197

Merged
juliusmarminge merged 54 commits into
mainfrom
t3/managed-policy
Nov 1, 2026
Merged

juliusmarminge merged 54 commits into
mainfrom
t3/managed-policy

Conversation

@juliusmarminge

@juliusmarminge juliusmarminge commented Oct 29, 2026 •

Copy link
Copy Markdown
Member

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:

OS Native File
macOS Configuration profile for com.t3tools.t3code /Library/Application Support/T3Code/policy.json
Windows Policy (REG_SZ, JSON) under HKLM\SOFTWARE\Policies\T3Tools\T3Code C:\ProgramData\T3Code\policy.json
Linux /etc/t3code/policy.json

A 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's settings.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 or NODE_OPTIONS could otherwise turn off a provider's approvals.
  • limits.clients (desktop, local-browser, remote) and limits.connect: the server refuses to bind wider than allowed, stops issuing pairing links, and revokes earlier pairings at startup. Without remote, T3 Connect is off too.
  • limits.webhookTasks, browserCookieImport and telemetry: 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 desktop feedUrl mirror, and allowNightly: false.
  • t3 policy show and t3 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

  • Providers still read their own config homes (~/.claude, ~/.codex). The docs point admins to each provider's managed settings.
  • Environment variables set where the server is started still reach providers.
  • feedUrl only redirects the desktop app; t3 update still goes through npm and GitHub.
  • Browser relay tracing isn't controlled by the policy.
  • With Connect off, the server unlinks at startup without telling the relay. Phones show the environment as offline rather than explaining the policy.

Verification

vp run check:changed passes: 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

juliusmarminge and others added 30 commits October 28, 2026 18:45
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>
Comment thread apps/server/src/policy/managedLimits.ts Outdated
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),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 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.

Comment on lines +922 to +926
const runtimeModeCeilingDenial = managedRuntimeModeDenial(managedPolicy, compatibleRuntimeMode);
const runtimeModeCeilingFallback =
runtimeModeCeilingDenial === undefined
? undefined
: clampManagedRuntimeMode(managedPolicy, compatibleRuntimeMode, compatibleRuntimeModes);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 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([

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 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) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 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}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟠 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)) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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>
@juliusmarminge
juliusmarminge merged commit d897d3f into main Nov 1, 2026
34 checks passed
@juliusmarminge
juliusmarminge deleted the t3/managed-policy branch November 1, 2026 20:10
github-actions Bot added a commit to omarcresp/t3code-flake that referenced this pull request Nov 2, 2026
**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
kylebegeman pushed a commit to kylebegeman/loom that referenced this pull request Nov 2, 2026
… (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.
kylebegeman pushed a commit to kylebegeman/loom that referenced this pull request Nov 2, 2026
… (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.
kylebegeman pushed a commit to kylebegeman/loom that referenced this pull request Nov 2, 2026
… (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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XXL 1,000+ changed lines (additions + deletions). vouch:trusted PR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants