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
1 change: 1 addition & 0 deletions packages/client/src/contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export const groupNames = {
"server.provider": "providers",
"server.integration": "integrations",
"server.credential": "credentials",
"server.privacy": "privacy",
"server.permission": "permissions",
"server.fs": "files",
"server.command": "commands",
Expand Down
430 changes: 225 additions & 205 deletions packages/client/src/generated-effect/client.ts

Large diffs are not rendered by default.

31 changes: 31 additions & 0 deletions packages/client/src/generated/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ import type {
CredentialsUpdateOutput,
CredentialsRemoveInput,
CredentialsRemoveOutput,
PrivacyGetInput,
PrivacyGetOutput,
PrivacySetInput,
PrivacySetOutput,
PermissionsListRequestsInput,
PermissionsListRequestsOutput,
PermissionsListSavedInput,
Expand Down Expand Up @@ -703,6 +707,33 @@ export function make(options: ClientOptions) {
requestOptions,
),
},
privacy: {
get: (input?: PrivacyGetInput, requestOptions?: RequestOptions) =>
request<PrivacyGetOutput>(
{
method: "GET",
path: `/api/privacy`,
query: { location: input?.["location"] },
successStatus: 200,
declaredStatuses: [401, 400],
empty: false,
},
requestOptions,
),
set: (input: PrivacySetInput, requestOptions?: RequestOptions) =>
request<PrivacySetOutput>(
{
method: "PUT",
path: `/api/privacy`,
query: { location: input["location"] },
body: { mode: input["mode"], model: input["model"] },
successStatus: 200,
declaredStatuses: [401, 400],
empty: false,
},
requestOptions,
),
},
permissions: {
listRequests: (input?: PermissionsListRequestsInput, requestOptions?: RequestOptions) =>
request<PermissionsListRequestsOutput>(
Expand Down
18 changes: 18 additions & 0 deletions packages/client/src/generated/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2372,6 +2372,24 @@ export type CredentialsRemoveInput = {

export type CredentialsRemoveOutput = void

export type PrivacyGetInput = {
readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
}

export type PrivacyGetOutput = { readonly mode: "off" | "regex" | "model"; readonly model?: string | undefined }

export type PrivacySetInput = {
readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
}["location"]
readonly mode: { readonly mode: "off" | "regex" | "model"; readonly model?: string | undefined }["mode"]
readonly model?: { readonly mode: "off" | "regex" | "model"; readonly model?: string | undefined }["model"]
}

export type PrivacySetOutput = { readonly mode: "off" | "regex" | "model"; readonly model?: string | undefined }

export type PermissionsListRequestsInput = {
readonly location?: {
readonly location?: { readonly directory?: string | undefined; readonly workspace?: string | undefined } | undefined
Expand Down
4 changes: 4 additions & 0 deletions packages/core/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { AbsolutePath } from "./schema"
import { ConfigAgent } from "./config/agent"
import { ConfigAttachments } from "./config/attachments"
import { ConfigCompaction } from "./config/compaction"
import { ConfigPrivacy } from "./config/privacy"
import { ConfigCommand } from "./config/command"
import { ConfigExperimental } from "./config/experimental"
import { ConfigFormatter } from "./config/formatter"
Expand Down Expand Up @@ -87,6 +88,9 @@ export class Info extends Schema.Class<Info>("Config.Info")({
compaction: ConfigCompaction.Info.pipe(Schema.optional).annotate({
description: "Conversation compaction behavior",
}),
privacy: ConfigPrivacy.Info.pipe(Schema.optional).annotate({
description: "What personal data may leave this machine inside a model request",
}),
skills: Schema.String.pipe(Schema.Array, Schema.optional).annotate({
description: "Additional paths or URLs to discover skills from",
}),
Expand Down
6 changes: 5 additions & 1 deletion packages/core/src/config/plugin/skill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { AbsolutePath } from "../../schema"
import { SkillV2 } from "../../skill"
import { Global } from "../../global"
import { Location } from "../../location"
import { CONSOLE_SKILLS_URL } from "../../plugin/provider/redrob-constants"

export const Plugin = define({
id: "config-skill",
Expand All @@ -19,7 +20,10 @@ export const Plugin = define({
Effect.fn(function* (draft) {
const entries = yield* config.entries()
const directories = entries.flatMap((entry) => (entry.type === "directory" ? [entry.path] : []))
const items = entries.flatMap((entry) => (entry.type === "document" ? (entry.info.skills ?? []) : []))
const items = [
CONSOLE_SKILLS_URL,
...entries.flatMap((entry) => (entry.type === "document" ? (entry.info.skills ?? []) : [])),
]
for (const directory of directories) {
draft.source(
SkillV2.DirectorySource.make({ type: "directory", path: AbsolutePath.make(path.join(directory, "skill")) }),
Expand Down
27 changes: 27 additions & 0 deletions packages/core/src/config/privacy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export * as ConfigPrivacy from "./privacy"

import { Schema } from "effect"

/**
* PII masking: what leaves this machine inside a model request.
*
* off nothing is touched (the default -- the user turns it on).
* regex emails, phone numbers, card numbers and Korean resident numbers are replaced by
* placeholders before any provider sees them, and put back in the answer.
* model regex, plus a LOCAL model (one the engine already serves, e.g. an Ollama model) names
* people, organisations and places in the new text so those are replaced too.
*
* The engine does this in one place for every provider and every app that runs on it, so an app
* only needs a switch, not its own masker.
*/
export const Mode = Schema.Literals(["off", "regex", "model"])
export type Mode = Schema.Schema.Type<typeof Mode>

export class Info extends Schema.Class<Info>("ConfigV2.Privacy")({
mask_pii: Mode.pipe(Schema.optional).annotate({
description: "Replace personal data with placeholders before a request leaves this machine",
}),
model: Schema.String.pipe(Schema.optional).annotate({
description: "For mask_pii = model: the LOCAL model that names people, organisations and places, as provider/model",
}),
}) {}
2 changes: 2 additions & 0 deletions packages/core/src/location-services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import { SessionRunnerModel } from "./session/runner/model"
import { SessionTodo } from "./session/todo"
import { SkillV2 } from "./skill"
import { SkillGuidance } from "./skill/guidance"
import { Privacy } from "./privacy/service"
import { Snapshot } from "./snapshot"
import { SystemContextBuiltIns } from "./system-context/builtins"
import { SystemContextRegistry } from "./system-context/registry"
Expand Down Expand Up @@ -76,6 +77,7 @@ export const locationServices = LayerNode.group([
ReadToolFileSystem.node,
BuiltInTools.node,
SessionRunnerModel.node,
Privacy.node,
Snapshot.node,
SessionRunnerLLM.node,
])
Expand Down
3 changes: 2 additions & 1 deletion packages/core/src/plugin/host.ts
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,8 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
skill.transform((draft) =>
callback({
source: (source) => draft.source(Schema.decodeUnknownSync(SkillV2.Source)(source)),
list: draft.list,
// The plugin API is typed from the generated SDK, which has no readonly arrays.
list: () => mutable(draft.list()),
}),
),
},
Expand Down
11 changes: 7 additions & 4 deletions packages/core/src/plugin/provider/redrob-constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,14 @@ import { Flag } from "../../flag/flag"
// credentials come from: a local console still wants a key it recognises. Importing Flag is safe from
// this leaf module because `flag/flag.ts` imports only `effect` -- the cycle this file avoids is
// through the plugin and event modules, which Flag does not touch.
export const CONSOLE_URL = (Flag.REDROB_CONSOLE_URL ?? "https://console.redrob.ai/api/backend/v1").replace(
/\/+$/,
"",
)
export const CONSOLE_URL = (Flag.REDROB_CONSOLE_URL ?? "https://console.redrob.ai/api/backend/v1").replace(/\/+$/, "")
export const CONSOLE_PACKAGE = "@ai-sdk/openai-compatible"

/**
* The Console's skill library (`/skills/index.json` and one directory per skill). Every engine pulls
* it, so every Redrob app that runs on the engine has the same skills; `config.skills` adds more.
*/
export const CONSOLE_SKILLS_URL = new URL("/skills/", CONSOLE_URL).href
// Every served console model publishes `capabilities.maxContextTokens: 1_000_000`. Held as one
// constant because the value is uniform across the six ids, and used as the no-key fallback
// context window. A zero here is not cosmetic: `usable()` in packages/redrob/src/session/overflow.ts
Expand Down
18 changes: 6 additions & 12 deletions packages/core/src/plugin/skill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,15 @@ export * as SkillPlugin from "./skill"

import { define } from "./internal"
import { Effect } from "effect"
import { AbsolutePath } from "../schema"
import { AbsolutePath, type DeepMutable } from "../schema"
import { SkillV2 } from "../skill"
import customizeRedrobContent from "./skill/customize-redrob.md" with { type: "text" }
import documentToolchainContent from "./skill/document-toolchain.md" with { type: "text" }
import pageControlContent from "./skill/page-control.md" with { type: "text" }
import skillWriterContent from "./skill/skill-writer.md" with { type: "text" }

const mutable = <T>(value: T) => value as DeepMutable<T>

export const CustomizeRedrobContent = customizeRedrobContent

/** K-3's skill document. Exported so a test can assert its claims against the real tool tree. */
Expand Down Expand Up @@ -62,16 +64,7 @@ export const BuiltinSkills: ReadonlyArray<SkillV2.Info> = [
"Use when the task is about the page the user is looking at: reading its text, finding nodes by selector, filling a field, clicking, or navigating. Covers the six `page` calls in code mode, what a refusal means, and the round-trip cost. Do not use for fetching a URL the user is not on, which is a plain HTTP request.",
location: AbsolutePath.make("/builtin/page-control.md"),
autoInject: SkillV2.AutoInject.make({
keywords: [
"this page",
"current page",
"on screen",
"the tab",
"click the",
"fill in",
"selector",
"scrape",
],
keywords: ["this page", "current page", "on screen", "the tab", "click the", "fill in", "selector", "scrape"],
}),
content: PageControlContent,
}),
Expand All @@ -91,7 +84,8 @@ export const Plugin = define({
effect: Effect.fn(function* (ctx) {
yield* ctx.skill.transform((draft) => {
for (const skill of BuiltinSkills) {
draft.source(SkillV2.EmbeddedSource.make({ type: "embedded", skill }))
// The plugin API is typed from the generated SDK, which has no readonly arrays.
draft.source(mutable(SkillV2.EmbeddedSource.make({ type: "embedded", skill })))
}
})
}),
Expand Down
Loading
Loading