Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
11 changes: 11 additions & 0 deletions apps/desktop/src/electron/ElectronProtocol.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ describe("ElectronProtocol", () => {
response.headers.get("content-security-policy") ?? "",
"font-src 'self' t3code-dev: data:",
);
assert.include(
response.headers.get("content-security-policy") ?? "",
"frame-ancestors 'none'",
);
assert.equal(response.headers.get("x-frame-options"), "DENY");
}),
);

Expand Down Expand Up @@ -162,6 +167,11 @@ describe("ElectronProtocol", () => {
assert.equal(yield* Effect.promise(() => responses[1].text()), "<main>client</main>");
assert.equal(responses[0].headers.get("content-type"), "text/javascript; charset=utf-8");
assert.include(responses[1].headers.get("content-security-policy") ?? "", "default-src");
assert.include(
responses[1].headers.get("content-security-policy") ?? "",
"frame-ancestors 'none'",
);
assert.equal(responses[1].headers.get("x-frame-options"), "DENY");
assert.equal(netFetchMock.mock.calls.length, 0);
}),
).pipe(Effect.provide(electronProtocolLayer)),
Expand Down Expand Up @@ -282,6 +292,7 @@ describe("ElectronProtocol", () => {
"https://clerk.t3.codes",
"https://challenges.cloudflare.com",
]);
assert.deepEqual(directives["frame-ancestors"], ["'none'"]);
assert.deepEqual(directives["connect-src"], ["'self'", "http:", "https:", "ws:", "wss:"]);
// A Plannotator review is framed from the environment that owns the thread,
// which is never this renderer's origin.
Expand Down
4 changes: 4 additions & 0 deletions apps/desktop/src/electron/ElectronProtocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ export function makeDesktopContentSecurityPolicy(input: DesktopProtocolRegistrat

return [
"default-src 'self'",
// T3-CUSTOM(expbkt3): a remote agent frame must not redirect into the desktop shell.
"frame-ancestors 'none'",
`script-src ${scriptSources.join(" ")}`,
`connect-src ${connectSources.join(" ")}`,
`img-src 'self' ${input.scheme}: blob: data: http: https:`,
Expand All @@ -115,6 +117,8 @@ export function makeDesktopContentSecurityPolicy(input: DesktopProtocolRegistrat
function withContentSecurityPolicy(response: Response, policy: string): Response {
const headers = new Headers(response.headers);
headers.set("Content-Security-Policy", policy);
// T3-CUSTOM(expbkt3): legacy defense in depth for custom-protocol shell responses.
headers.set("X-Frame-Options", "DENY");
return new Response(response.body, {
status: response.status,
statusText: response.statusText,
Expand Down
19 changes: 19 additions & 0 deletions apps/server/src/agentui/AgentUiService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,25 @@ describe("AgentUiService", () => {
),
);

it.effect("preserves distinct fragments for same-origin URL renders", () =>
withService((service) =>
Effect.gen(function* () {
const firstUrl = "https://fixture.example.test/board?mode=collab#room=alpha,safe-key-a";
const secondUrl = "https://fixture.example.test/board#room=beta,safe-key-b";
const first = yield* service.show({ threadId, title: "First", url: firstUrl });
const second = yield* service.show({ threadId, title: "Second", url: secondUrl });

expect(first.renderId).not.toBe(second.renderId);
expect((yield* service.getRender({ threadId, renderId: first.renderId }))?.url).toBe(
firstUrl,
);
expect((yield* service.getRender({ threadId, renderId: second.renderId }))?.url).toBe(
secondUrl,
);
}),
),
);

it.effect("requires exactly one of html or url", () =>
withService((service) =>
Effect.gen(function* () {
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/environment/ServerEnvironment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,8 @@ export const make = Effect.gen(function* () {
threadPriority: true,
// T3-CUSTOM(expbkt3): durable manual Linear tags.
threadLinearIssue: true,
// T3-CUSTOM(expbkt3): durable Mattermost conversation link.
threadMattermostLink: true,
threadExternalSessionAttach: true,
// T3-CUSTOM(expbkt3): high-level durable bootstrap and defaults hierarchy.
durableThreadBootstrap: true,
Expand Down
11 changes: 11 additions & 0 deletions apps/server/src/http.ts
Original file line number Diff line number Diff line change
Expand Up @@ -308,6 +308,13 @@ export const attachmentUploadRouteLayer = HttpRouter.add(
}),
);

// T3-CUSTOM(expbkt3): the signed-in shell must never become a same-origin iframe
// after an agent-supplied cross-origin URL redirects back to T3.
export const T3_HTML_FRAME_HEADERS = {
"content-security-policy": "frame-ancestors 'none'",
"x-frame-options": "DENY",
} as const;

export const staticAndDevRouteLayer = HttpRouter.add(
"GET",
"*",
Expand Down Expand Up @@ -384,6 +391,8 @@ export const staticAndDevRouteLayer = HttpRouter.add(
return HttpServerResponse.uint8Array(indexData, {
status: 200,
contentType: "text/html; charset=utf-8",
// T3-CUSTOM(expbkt3): block redirect-based sandbox escapes on SPA fallbacks.
headers: T3_HTML_FRAME_HEADERS,
});
}

Expand All @@ -396,6 +405,8 @@ export const staticAndDevRouteLayer = HttpRouter.add(
return HttpServerResponse.uint8Array(data, {
status: 200,
contentType,
// T3-CUSTOM(expbkt3): only HTML is frame-sensitive; assets keep their normal headers.
...(contentType.startsWith("text/html") ? { headers: T3_HTML_FRAME_HEADERS } : {}),
});
}),
);
6 changes: 6 additions & 0 deletions apps/server/src/orchestration/Layers/ProjectionPipeline.ts
Original file line number Diff line number Diff line change
Expand Up @@ -762,6 +762,8 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
priority: event.payload.priority ?? null,
// T3-CUSTOM(expbkt3): no manual Linear tag at thread creation.
linearIssueUrl: null,
// T3-CUSTOM(expbkt3): the Mattermost link is bound after creation.
mattermostThreadUrl: null,
// T3-CUSTOM(expbkt3): session lineage stamped at creation.
parentThreadId: event.payload.parentThreadId ?? null,
// T3-CUSTOM(expbkt3): BEGIN — no work summary until one is requested.
Expand Down Expand Up @@ -1040,6 +1042,10 @@ const makeOrchestrationProjectionPipeline = Effect.fn("makeOrchestrationProjecti
...(event.payload.linearIssueUrl !== undefined
? { linearIssueUrl: event.payload.linearIssueUrl }
: {}),
// T3-CUSTOM(expbkt3): durable Mattermost conversation link.
...(event.payload.mattermostThreadUrl !== undefined
? { mattermostThreadUrl: event.payload.mattermostThreadUrl }
: {}),
// T3-CUSTOM(expbkt3): session lineage re-parent / detach.
...(event.payload.parentThreadId !== undefined
? { parentThreadId: event.payload.parentThreadId }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,8 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => {
priority: null,
// T3-CUSTOM(expbkt3): no manual Linear tag on this fixture.
linearIssueUrl: null,
// T3-CUSTOM(expbkt3): no Mattermost conversation on this fixture.
mattermostThreadUrl: null,
// T3-CUSTOM(expbkt3): session lineage.
parentThreadId: null,
// T3-CUSTOM(expbkt3): no work summary was ever requested here.
Expand Down Expand Up @@ -501,6 +503,8 @@ projectionSnapshotLayer("ProjectionSnapshotQuery", (it) => {
priority: null,
// T3-CUSTOM(expbkt3): no manual Linear tag on this fixture.
linearIssueUrl: null,
// T3-CUSTOM(expbkt3): no Mattermost conversation on this fixture.
mattermostThreadUrl: null,
// T3-CUSTOM(expbkt3): session lineage.
parentThreadId: null,
// T3-CUSTOM(expbkt3): no work summary was ever requested here.
Expand Down
11 changes: 11 additions & 0 deletions apps/server/src/orchestration/Layers/ProjectionSnapshotQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
snoozed_at AS "snoozedAt",
priority,
linear_issue_url AS "linearIssueUrl",
mattermost_thread_url AS "mattermostThreadUrl",
parent_thread_id AS "parentThreadId",
-- T3-CUSTOM(expbkt3): BEGIN — bulk session manager work summary (JSON blob).
work_summary AS "workSummary",
Expand Down Expand Up @@ -680,6 +681,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
snoozed_at AS "snoozedAt",
priority,
linear_issue_url AS "linearIssueUrl",
mattermost_thread_url AS "mattermostThreadUrl",
parent_thread_id AS "parentThreadId",
-- T3-CUSTOM(expbkt3): BEGIN — bulk session manager work summary (JSON blob).
work_summary AS "workSummary",
Expand Down Expand Up @@ -731,6 +733,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
snoozed_at AS "snoozedAt",
priority,
linear_issue_url AS "linearIssueUrl",
mattermost_thread_url AS "mattermostThreadUrl",
parent_thread_id AS "parentThreadId",
-- T3-CUSTOM(expbkt3): BEGIN — bulk session manager work summary (JSON blob).
work_summary AS "workSummary",
Expand Down Expand Up @@ -1191,6 +1194,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
snoozed_at AS "snoozedAt",
priority,
linear_issue_url AS "linearIssueUrl",
mattermost_thread_url AS "mattermostThreadUrl",
parent_thread_id AS "parentThreadId",
-- T3-CUSTOM(expbkt3): BEGIN — bulk session manager work summary (JSON blob).
work_summary AS "workSummary",
Expand Down Expand Up @@ -1263,6 +1267,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
snoozed_at AS "snoozedAt",
priority,
linear_issue_url AS "linearIssueUrl",
mattermost_thread_url AS "mattermostThreadUrl",
parent_thread_id AS "parentThreadId",
-- T3-CUSTOM(expbkt3): BEGIN — bulk session manager work summary (JSON blob).
work_summary AS "workSummary",
Expand Down Expand Up @@ -2228,6 +2233,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
snoozedAt: row.snoozedAt,
priority: row.priority,
linearIssueUrl: row.linearIssueUrl ?? null,
mattermostThreadUrl: row.mattermostThreadUrl ?? null,
parentThreadId: row.parentThreadId ?? null,
// T3-CUSTOM(expbkt3): BEGIN — bulk session manager work summary.
workSummary: mapWorkSummary(row.workSummary),
Expand Down Expand Up @@ -2548,6 +2554,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
snoozedAt: row.snoozedAt,
priority: row.priority,
linearIssueUrl: row.linearIssueUrl ?? null,
mattermostThreadUrl: row.mattermostThreadUrl ?? null,
parentThreadId: row.parentThreadId ?? null,
// T3-CUSTOM(expbkt3): BEGIN — bulk session manager work summary.
workSummary: mapWorkSummary(row.workSummary),
Expand Down Expand Up @@ -2733,6 +2740,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
snoozedAt: row.snoozedAt,
priority: row.priority,
linearIssueUrl: row.linearIssueUrl ?? null,
mattermostThreadUrl: row.mattermostThreadUrl ?? null,
parentThreadId: row.parentThreadId ?? null,
// T3-CUSTOM(expbkt3): BEGIN — bulk session manager work summary.
workSummary: mapWorkSummary(row.workSummary),
Expand Down Expand Up @@ -2924,6 +2932,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
snoozedAt: row.snoozedAt,
priority: row.priority,
linearIssueUrl: row.linearIssueUrl ?? null,
mattermostThreadUrl: row.mattermostThreadUrl ?? null,
parentThreadId: row.parentThreadId ?? null,
// T3-CUSTOM(expbkt3): BEGIN — bulk session manager work summary.
workSummary: mapWorkSummary(row.workSummary),
Expand Down Expand Up @@ -3257,6 +3266,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
snoozedAt: threadRow.value.snoozedAt,
priority: threadRow.value.priority,
linearIssueUrl: threadRow.value.linearIssueUrl ?? null,
mattermostThreadUrl: threadRow.value.mattermostThreadUrl ?? null,
parentThreadId: threadRow.value.parentThreadId ?? null,
// T3-CUSTOM(expbkt3): BEGIN — bulk session manager work summary.
workSummary: mapWorkSummary(threadRow.value.workSummary),
Expand Down Expand Up @@ -3492,6 +3502,7 @@ const makeProjectionSnapshotQuery = Effect.gen(function* () {
snoozedAt: threadRow.value.snoozedAt,
priority: threadRow.value.priority,
linearIssueUrl: threadRow.value.linearIssueUrl ?? null,
mattermostThreadUrl: threadRow.value.mattermostThreadUrl ?? null,
parentThreadId: threadRow.value.parentThreadId ?? null,
// T3-CUSTOM(expbkt3): BEGIN — bulk session manager work summary.
workSummary: mapWorkSummary(threadRow.value.workSummary),
Expand Down
56 changes: 56 additions & 0 deletions apps/server/src/orchestration/decider.priority.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -214,4 +214,60 @@ it.layer(NodeServices.layer)("thread priority decider", (it) => {
).toBeNull();
}),
);

// T3-CUSTOM(expbkt3): the Mattermost conversation a session is bound to.
it.effect("sets and clears a Mattermost link through thread metadata", () =>
Effect.gen(function* () {
const linked = yield* decideOrchestrationCommand({
command: {
type: "thread.meta.update",
commandId: CommandId.make("cmd-link-mattermost"),
threadId: ThreadId.make("thread-1"),
mattermostThreadUrl: "https://chat.example.com/beknown/pl/abc123",
},
readModel: makeReadModel(),
});
const linkedEvents = Array.isArray(linked) ? linked : [linked];
expect(
linkedEvents[0]?.type === "thread.meta-updated"
? linkedEvents[0].payload.mattermostThreadUrl
: undefined,
).toBe("https://chat.example.com/beknown/pl/abc123");

const cleared = yield* decideOrchestrationCommand({
command: {
type: "thread.meta.update",
commandId: CommandId.make("cmd-clear-mattermost"),
threadId: ThreadId.make("thread-1"),
mattermostThreadUrl: null,
},
readModel: makeReadModel(),
});
const clearedEvents = Array.isArray(cleared) ? cleared : [cleared];
expect(
clearedEvents[0]?.type === "thread.meta-updated"
? clearedEvents[0].payload.mattermostThreadUrl
: undefined,
).toBeNull();
}),
);

it.effect("leaves the Mattermost link untouched when the command omits it", () =>
Effect.gen(function* () {
const event = yield* decideOrchestrationCommand({
command: {
type: "thread.meta.update",
commandId: CommandId.make("cmd-rename-only-mattermost"),
threadId: ThreadId.make("thread-1"),
title: "Renamed",
},
readModel: makeReadModel(),
});
const events = Array.isArray(event) ? event : [event];
if (events[0]?.type === "thread.meta-updated") {
// undefined, not null: an omitted field must not clear a live binding.
expect(events[0].payload.mattermostThreadUrl).toBe(undefined);
}
}),
);
});
4 changes: 4 additions & 0 deletions apps/server/src/orchestration/decider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -928,6 +928,10 @@ export const decideOrchestrationCommand = Effect.fn("decideOrchestrationCommand"
...(command.linearIssueUrl !== undefined
? { linearIssueUrl: command.linearIssueUrl }
: {}),
// T3-CUSTOM(expbkt3): undefined leaves the Mattermost link unchanged.
...(command.mattermostThreadUrl !== undefined
? { mattermostThreadUrl: command.mattermostThreadUrl }
: {}),
// T3-CUSTOM(expbkt3): undefined leaves lineage unchanged; null detaches.
...(command.parentThreadId !== undefined
? { parentThreadId: command.parentThreadId }
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/orchestration/projector.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ describe("orchestration projector", () => {
priority: null,
// T3-CUSTOM(expbkt3): no manual Linear tag on a new thread.
linearIssueUrl: null,
// T3-CUSTOM(expbkt3): no Mattermost conversation bound to a new thread.
mattermostThreadUrl: null,
// T3-CUSTOM(expbkt3): session lineage.
parentThreadId: null,
deletedAt: null,
Expand Down
6 changes: 6 additions & 0 deletions apps/server/src/orchestration/projector.ts
Original file line number Diff line number Diff line change
Expand Up @@ -353,6 +353,8 @@ export function projectEvent(
priority: payload.priority ?? null,
// T3-CUSTOM(expbkt3): no manual Linear tag at thread creation.
linearIssueUrl: null,
// T3-CUSTOM(expbkt3): the Mattermost link is bound after creation.
mattermostThreadUrl: null,
// T3-CUSTOM(expbkt3): session lineage stamped at creation.
parentThreadId: payload.parentThreadId ?? null,
deletedAt: null,
Expand Down Expand Up @@ -528,6 +530,10 @@ export function projectEvent(
...(payload.linearIssueUrl !== undefined
? { linearIssueUrl: payload.linearIssueUrl }
: {}),
// T3-CUSTOM(expbkt3): durable Mattermost conversation link.
...(payload.mattermostThreadUrl !== undefined
? { mattermostThreadUrl: payload.mattermostThreadUrl }
: {}),
// T3-CUSTOM(expbkt3): session lineage re-parent / detach.
...(payload.parentThreadId !== undefined
? { parentThreadId: payload.parentThreadId }
Expand Down
6 changes: 6 additions & 0 deletions apps/server/src/persistence/Layers/ProjectionThreads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ const makeProjectionThreadRepository = Effect.gen(function* () {
snoozed_at,
priority,
linear_issue_url,
mattermost_thread_url,
parent_thread_id,
-- T3-CUSTOM(expbkt3): BEGIN — bulk session manager work summary (JSON).
work_summary,
Expand Down Expand Up @@ -94,6 +95,7 @@ const makeProjectionThreadRepository = Effect.gen(function* () {
${row.snoozedAt},
${row.priority},
${row.linearIssueUrl ?? null},
${row.mattermostThreadUrl ?? null},
${row.parentThreadId ?? null},
-- T3-CUSTOM(expbkt3): BEGIN — bulk session manager work summary (JSON).
${row.workSummary ?? null},
Expand Down Expand Up @@ -134,6 +136,7 @@ const makeProjectionThreadRepository = Effect.gen(function* () {
snoozed_at = excluded.snoozed_at,
priority = excluded.priority,
linear_issue_url = excluded.linear_issue_url,
mattermost_thread_url = excluded.mattermost_thread_url,
parent_thread_id = excluded.parent_thread_id,
-- T3-CUSTOM(expbkt3): BEGIN — bulk session manager work summary (JSON).
work_summary = excluded.work_summary,
Expand Down Expand Up @@ -181,6 +184,7 @@ const makeProjectionThreadRepository = Effect.gen(function* () {
snoozed_at AS "snoozedAt",
priority,
linear_issue_url AS "linearIssueUrl",
mattermost_thread_url AS "mattermostThreadUrl",
parent_thread_id AS "parentThreadId",
-- T3-CUSTOM(expbkt3): BEGIN — bulk session manager work summary (JSON).
work_summary AS "workSummary",
Expand Down Expand Up @@ -230,6 +234,7 @@ const makeProjectionThreadRepository = Effect.gen(function* () {
snoozed_at AS "snoozedAt",
priority,
linear_issue_url AS "linearIssueUrl",
mattermost_thread_url AS "mattermostThreadUrl",
parent_thread_id AS "parentThreadId",
-- T3-CUSTOM(expbkt3): BEGIN — bulk session manager work summary (JSON).
work_summary AS "workSummary",
Expand Down Expand Up @@ -288,6 +293,7 @@ const makeProjectionThreadRepository = Effect.gen(function* () {
snoozed_at AS "snoozedAt",
priority,
linear_issue_url AS "linearIssueUrl",
mattermost_thread_url AS "mattermostThreadUrl",
parent_thread_id AS "parentThreadId",
work_summary AS "workSummary",
pinned_at AS "pinnedAt",
Expand Down
Loading
Loading