Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
67fd6a1
feat(projects): add explicit bounded project briefs
unlockdigital Oct 28, 2026
85e7eba
fix(web): allow clearing an initial project brief request
unlockdigital Oct 28, 2026
ba77f46
fix(codex): explain isolated project brief limitation
unlockdigital Oct 28, 2026
2740b32
feat(codex): generate bounded project briefs through exec
unlockdigital Oct 28, 2026
63e1556
fix(privacy): limit project briefs to reviewed documentation
unlockdigital Oct 28, 2026
853c69f
fix(privacy): restrict brief inputs to canonical READMEs
unlockdigital Oct 28, 2026
3e78f2a
fix(projects): align brief validation with README-only policy
unlockdigital Oct 28, 2026
4b2a4af
merge: combine project brief and released safety fixes
unlockdigital Oct 29, 2026
a1f97c6
chore: preserve local customizations for release candidate
unlockdigital Oct 31, 2026
f9f5549
merge: integrate upstream v0.1.6 with local customizations
unlockdigital Nov 1, 2026
2140647
fix: finish fork compatibility with upstream v0.1.6
unlockdigital Nov 1, 2026
7324d78
fix: validate v0.1.6 fork compatibility
unlockdigital Nov 1, 2026
41a4cf8
fix(web): keep model names readable beside reasoning controls
unlockdigital Nov 1, 2026
45e729f
fix(local): integrate November maintenance and preserve model labels
unlockdigital Nov 5, 2026
8819b23
fix(codex): share the session index lock before account startup
unlockdigital Nov 5, 2026
a2d9559
build: snapshot qualified local Teddy desktop source
unlockdigital Nov 8, 2026
eedcb00
chore(local): snapshot verified Teddy Default reconnect release
unlockdigital Nov 9, 2026
9c896d9
fix(server): release Codex conversation ownership on provider switch
unlockdigital Nov 11, 2026
b540955
feat(fork): integrate v0.1.9 with local Teddy and account behavior
unlockdigital Nov 13, 2026
0eec5e5
merge: retain local fixes ported into the v0.1.9 candidate
unlockdigital Nov 13, 2026
ed9458d
fix(claude): retain coordinator reads in read-only tool policy
unlockdigital Nov 13, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions apps/desktop/src/window/DesktopWindow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -914,9 +914,6 @@ export const make = Effect.gen(function* () {
// Desktop IPC only answers the main window, so register it before its renderer can call in.
yield* electronWindow.setMain(window);
loadApplication();
if (environment.isDevelopment) {
window.webContents.openDevTools({ mode: "detach" });
}

window.on("closed", () => {
if (!backendHasStarted) mainClosedDuringStartup = true;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { UpstreamUpdatesSection } from "./UpstreamUpdatesSection";
import { N2itionConnectionSection } from "./components/N2itionConnectionSection";
import * as Dialog from "../../components/ui/Dialog";
import { useAtom, useAtomValue } from "@effect/atom-react";
import { useNavigation, type StaticScreenProps } from "@react-navigation/native";
Expand Down Expand Up @@ -311,6 +312,7 @@ function EnvironmentDetail({ environmentId }: { readonly environmentId: Environm
{config.environment.capabilities.upstreamUpdates === true && (
<UpstreamUpdatesSection key={environmentId} environmentId={environmentId} />
)}
<N2itionConnectionSection key={environmentId} environmentId={environmentId} />
<SettingsSection title="Providers">
<SettingsActionRow
icon="arrow.clockwise"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { useAtomValue } from "@effect/atom-react";
import type { EnvironmentId } from "@t3tools/contracts";
import type { N2itionStatus } from "@t3tools/contracts/n2itionConnection";
import { useEffect, useState } from "react";
import { View } from "react-native";
import { AppText as Text } from "../../../components/ui/AppText";
import * as Dialog from "../../../components/ui/Dialog";
import { serverEnvironment } from "../../../state/server";
import { useAtomCommand } from "../../../state/use-atom-command";
import { SettingsRow as SettingsActionRow } from "./SettingsRow";
import { SettingsSection } from "./SettingsSection";

export function N2itionConnectionSection({
environmentId,
}: {
readonly environmentId: EnvironmentId;
}) {
const allowed = useAtomValue(serverEnvironment.n2itionReconnect.permissionAtom(environmentId));
const getStatus = useAtomCommand(serverEnvironment.n2itionStatus, { reportFailure: false });
const reconnect = useAtomCommand(serverEnvironment.n2itionReconnect, { reportFailure: false });
const control = useAtomCommand(serverEnvironment.n2itionControl, { reportFailure: false });
const refresh = useAtomCommand(serverEnvironment.n2itionRefresh, { reportFailure: false });
const [status, setStatus] = useState<N2itionStatus | null>(null);
const [error, setError] = useState<string | null>(null);
const [busy, setBusy] = useState(false);
// oxlint-disable-next-line t3code/no-unreasoned-state-sync -- Synchronize owner RPC status with the selected environment and discard stale responses.
useEffect(() => {
if (!allowed) return;
let closed = false;
void getStatus({ environmentId, input: {} })
.then((result) => {
if (!closed && result._tag === "Success") setStatus(result.value);
})
.catch(() => undefined);
return () => {
closed = true;
};
}, [allowed, environmentId, getStatus]);
const act = async (action: "check" | "reconnect" | "pause" | "resume" | "revoke" | "refresh") => {
if (!allowed || busy || status?.epoch === null || !status) return;
setBusy(true);
setError(null);
const request =
action === "check"
? getStatus({ environmentId, input: {} })
: action === "reconnect"
? reconnect({ environmentId, input: { expectedEpoch: status.epoch } })
: action === "refresh"
? refresh({ environmentId, input: {} })
: control({ environmentId, input: { action, expectedEpoch: status.epoch } });
await request
.then((result) => {
if (result._tag === "Success") {
setStatus((previous) =>
previous?.epoch !== null &&
previous?.epoch !== undefined &&
result.value.epoch !== null &&
previous.epoch > result.value.epoch
? previous
: result.value,
);
} else {
setError(
"The connection needs attention. Check status and verify this environment’s account setup.",
);
}
})
.catch(() => {
setError("Connection status is unavailable. Reconnect this environment and try again.");
});
setBusy(false);
};
if (!allowed || !status || status.state === "disabled") return null;
const revoked = status.localMode === "revoked";
return (
<SettingsSection title="Teddy · N2ITION">
<View className="gap-2 p-4">
<Text>{status.state.replaceAll("_", " ")}</Text>
<Text className="text-sm text-foreground-muted">
Read-only task context. Reconnection verifies the enrolled account and preserves the
current access limits.
</Text>
{error ? (
<Text accessibilityRole="alert" className="text-sm text-danger-foreground">
{error}
</Text>
) : null}
</View>
<SettingsActionRow
icon="arrow.clockwise"
label="Check status"
disabled={busy}
onPress={() => void act("check")}
/>
<SettingsActionRow
icon="checkmark"
label="Verify and reconnect"
disabled={busy || revoked}
onPress={() => void act("reconnect")}
/>
<SettingsActionRow
icon={status.localMode === "active" ? "stop.fill" : "play"}
label={status.localMode === "active" ? "Pause access" : "Resume access"}
disabled={busy || revoked}
onPress={() => void act(status.localMode === "active" ? "pause" : "resume")}
/>
<SettingsActionRow
icon="arrow.clockwise"
label="Refresh context"
disabled={busy || status.localMode !== "active"}
onPress={() => void act("refresh")}
/>
<SettingsActionRow
icon="xmark"
label="Revoke here"
disabled={busy || revoked}
onPress={() =>
Dialog.alert(
"Revoke N2ITION access?",
"This ends this environment’s access and clears its cached context. Reconnection requires a new owner setup.",
[
{ text: "Cancel", style: "cancel" },
{ text: "Revoke", style: "destructive", onPress: () => void act("revoke") },
],
)
}
/>
</SettingsSection>
);
}
178 changes: 178 additions & 0 deletions apps/mobile/src/features/threads/PrivateControls.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
import type {
EnvironmentId,
PrivateHelperPacket,
PrivateHelpersPolicy,
ThreadId,
} from "@t3tools/contracts";
import { AsyncResult } from "effect/reactivity";
import { useState } from "react";
import { Modal, Pressable, ScrollView, View } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { AppText as Text } from "../../components/ui/AppText";
import { Button } from "../../components/ui/Button";
import { serverEnvironment } from "../../state/server";
import { threadEnvironment } from "../../state/threads";
import { useAtomCommand } from "../../state/use-atom-command";

export function PrivateControls(props: {
environmentId: EnvironmentId;
threadId: ThreadId;
policy: PrivateHelpersPolicy;
canOperate: boolean;
}) {
const [open, setOpen] = useState(false);
const [packets, setPackets] = useState<readonly PrivateHelperPacket[]>([]);
const [reviewed, setReviewed] = useState<string | null>(null);
const [busy, setBusy] = useState(false);
const load = useAtomCommand(serverEnvironment.getPrivateHelperState);
const decide = useAtomCommand(serverEnvironment.decidePrivateHelper);
const update = useAtomCommand(threadEnvironment.updateMetadata);
const refresh = async () => {
const result = await load({
environmentId: props.environmentId,
input: { threadId: props.threadId },
});
if (AsyncResult.isSuccess(result)) setPackets(result.value.packets);
};
const decision = async (packet: PrivateHelperPacket, value: "approve" | "block") => {
setBusy(true);
try {
const result = await decide({
environmentId: props.environmentId,
input: {
threadId: props.threadId,
packetId: packet.id,
digest: packet.digest,
decision: value,
},
});
if (AsyncResult.isSuccess(result))
setPackets((current) =>
current.map((entry) => (entry.id === packet.id ? result.value : entry)),
);
setReviewed(null);
} catch (error) {
setBusy(false);
throw error;
}
setBusy(false);
};
return (
<>
<Pressable
accessibilityRole="button"
accessibilityLabel="Private controls"
onPress={() => {
setOpen(true);
void refresh();
}}
className="min-h-11 self-start justify-center px-3"
>
<Text className="text-sm text-foreground-muted">
{props.policy === "disabled" ? "Private" : "Private + helpers"}
</Text>
</Pressable>
<Modal
visible={open}
onRequestClose={() => setOpen(false)}
presentationStyle="pageSheet"
animationType="slide"
>
<SafeAreaView className="flex-1 bg-screen">
<ScrollView contentContainerStyle={{ padding: 20, gap: 16 }}>
<Text className="text-xl font-t3-bold text-foreground">Private controls</Text>
<Text className="text-foreground-muted">
Full conversation context uses Tinfoil. External helpers receive only the exact packet
you review and approve. Auto action permissions stay separate.
</Text>
<View className="gap-2">
<Button
label="Private only"
disabled={!props.canOperate || busy || props.policy === "disabled"}
onPress={() => {
void update({
environmentId: props.environmentId,
input: { threadId: props.threadId, privateHelpers: "disabled" },
});
}}
/>
<Button
label="Private + helpers · review each packet"
disabled={!props.canOperate || busy || props.policy === "review"}
onPress={() => {
void update({
environmentId: props.environmentId,
input: { threadId: props.threadId, privateHelpers: "review" },
});
}}
/>
<Button
label="Refresh helper packets"
disabled={busy}
onPress={() => void refresh()}
/>
</View>
<Text className="text-foreground-muted">
Helpers have no parent history, workspace, tools, T3 access, or credentials. Their
provider’s data terms apply to the approved packet. Reviews expire after 15 minutes or
a server restart.
</Text>
{packets.length === 0 ? (
<Text className="text-foreground-muted">
Your private coordinator can prepare a self-contained helper packet.
</Text>
) : null}
{packets.map((packet) => (
<View key={packet.id} className="gap-3 rounded-xl border border-subtle-strong p-3">
<Text className="font-t3-bold text-foreground">
{packet.modelSelection.instanceId} · {packet.modelSelection.model}
</Text>
<Text className="text-foreground-muted">{packet.status}</Text>
<Text className="text-foreground">{packet.task.objective}</Text>
<Button
label="Inspect exact outbound packet"
onPress={() => setReviewed(packet.id)}
/>
{reviewed === packet.id ? (
<Text selectable className="text-xs text-foreground">
{packet.prompt}
</Text>
) : null}
{packet.status === "awaiting-review" ? (
<View className="gap-2">
<Button
label="Approve packet once"
disabled={
!props.canOperate ||
busy ||
reviewed !== packet.id ||
props.policy === "disabled"
}
onPress={() => void decision(packet, "approve")}
/>
<Button
label="Block packet"
disabled={!props.canOperate || busy}
onPress={() => void decision(packet, "block")}
/>
</View>
) : null}
{packet.artifact === undefined ? null : (
<>
<Text selectable className="text-xs text-foreground">
{packet.artifact}
</Text>
<Text className="text-foreground-muted">
Review privately before applying or executing this artifact.
</Text>
</>
)}
</View>
))}
<Button label="Close" onPress={() => setOpen(false)} />
</ScrollView>
</SafeAreaView>
</Modal>
</>
);
}
11 changes: 11 additions & 0 deletions apps/mobile/src/features/threads/ThreadComposer.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { PrivateControls } from "./PrivateControls";
import { useSidebarWorkspaceTabs } from "../layout/workspace/useSystemWorkspaceTabBar";
import { Button } from "../../components/ui/Button";
import * as Dialog from "../../components/ui/Dialog";
Expand Down Expand Up @@ -823,6 +824,16 @@ export const ThreadComposer = function ThreadComposer(props: ThreadComposerProps
Platform.OS === "android" ? themeColorWithAlpha(composerPanel, 1) : undefined,
}}
>
{props.selectedThread.source.privateModelRoute !== undefined &&
props.serverConfig?.environment.capabilities.privateWorkflow === true ? (
<PrivateControls
key={`${props.environmentId}:${props.selectedThread.id}`}
environmentId={props.environmentId}
threadId={props.selectedThread.id}
policy={props.selectedThread.source.privateHelpers ?? "review"}
canOperate={props.canOperateThread}
/>
) : null}
{/* The backdrop gradient lives on a plain View: Reanimated's Animated.View
silently drops experimental_backgroundImage on Android, which left this
strip fully transparent and the feed text legible through the composer. */}
Expand Down
1 change: 1 addition & 0 deletions apps/mobile/src/features/usage/UsageLimitsPooled.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -767,6 +767,7 @@ export function UsageLimitsSection({
{accounts.length === 0 &&
notices.length === 0 &&
failedLabels.length === 0 &&
!cursorPrompt &&
externalLinks.length === 0 ? (
<Text className="py-12 text-center text-base text-foreground-muted">
{selected.size === 0
Expand Down
2 changes: 1 addition & 1 deletion apps/server/scripts/dependency-security.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const minimums: Readonly<Record<string, Readonly<Record<number, string>>>> = {
"body-parser": { 2: "2.4.0" },
seroval: { 1: "1.6.11" },
"proxy-addr": { 2: "2.0.8" },
hono: { 4: "4.13.11" },
hono: { 4: "4.13.16" },
"ip-address": { 10: "10.7.4" },
qs: { 6: "6.16.1" },
"fast-uri": { 3: "3.1.9" },
Expand Down
4 changes: 3 additions & 1 deletion apps/server/src/auth/RpcAuthorization.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,9 @@ export const RPC_REQUIRED_SCOPES = {
[WS_METHODS.serverRemoveKeybinding]: AuthSettingsWriteScope,
[WS_METHODS.serverGetStorageCleanupReport]: AuthOrchestrationReadScope,
[WS_METHODS.serverGetDatabaseStorage]: AuthOrchestrationReadScope,

[WS_METHODS.n2itionStatus]: AuthSettingsWriteScope,
[WS_METHODS.n2itionControl]: AuthSettingsWriteScope,
[WS_METHODS.n2itionRefresh]: AuthSettingsWriteScope,
[WS_METHODS.serverGetSettings]: AuthOrchestrationReadScope,
[WS_METHODS.accountRoutingPreview]: AuthOrchestrationReadScope,
[WS_METHODS.peerLinksList]: AuthAccessReadScope,
Expand Down
1 change: 1 addition & 0 deletions apps/server/src/device/DeviceService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,7 @@ const fixture = Effect.fn("fixture")(function* (
Effect.provideService(
ServerSettings.ServerSettingsService,
ServerSettings.ServerSettingsService.of({
setApprovalCoordinator: () => Effect.die("Unexpected coordinator mutation"),
updateProviderInstance: () => Effect.die("Unexpected provider mutation"),
withSettingsSnapshot: (use) => Effect.flatMap(Ref.get(settings), use),
start: Effect.void,
Expand Down
Loading
Loading