import { AiLiteracyConfirmSchema, AiLiteracyListSchema, AiLiteracyQuerySchema, AiLiteracyStatusSchema } from "../schemas/ai-literacy.js"; import { FIELD_NOTES_FILE } from "@intentic/constants"; import { procedure } from "../protocol/route-meta.js"; import { AgentDomainPolicySchema, AudienceAnswerSchema, AudienceStateSchema, BuiltinPromptSchema, MemberAudienceSchema, BuiltinPromptTextSchema, FieldNotesStatusSchema, GettingStartedSchema, REPO_CHECKS_FILE, RepoChecksAdoptSchema, RepoChecksListSchema, RuleFiringsSchema, SandboxSettingsSchema, SandboxSettingsWriteSchema, SavingsReportSchema, TimezoneOfferSchema, TimezoneStateSchema, } from "../schemas/settings.js"; import { OkSchema } from "../schemas/shared.js"; import { DayWindowQuerySchema } from "../schemas/providers/usage.js"; // Per-sandbox agent settings (.intentic/config/settings.json); `get` fills defaults for an absent file, `set` // overwrites whole. `savings` measures each token-reduction mechanism's worth over the same UTC day window the spend // ledger takes. `builtinPrompt` returns a built-in prompt's actual text to show or fork. export const settingsContract = { agentDomain: procedure .route({ method: "GET", path: "/settings/agent-domain", summary: "Read the agent execution domain" }) .meta({ panel: false, control: "never" }) .output(AgentDomainPolicySchema), setAgentDomain: procedure .route({ method: "POST", path: "/settings/agent-domain", summary: "Choose the agent execution domain" }) .meta({ floor: "owner", panel: false, control: "never" }) .input(AgentDomainPolicySchema) .output(OkSchema), aiLiteracy: procedure.route({ method: "GET", path: "/settings/ai-literacy/{locale}", summary: "Read the AI literacy notice and your confirmation" }) .meta({ guest: true }).input(AiLiteracyQuerySchema).output(AiLiteracyStatusSchema), confirmAiLiteracy: procedure.route({ method: "POST", path: "/settings/ai-literacy/confirm", summary: "Confirm your own AI literacy notice" }) .meta({ guest: true, floor: "viewer" }).input(AiLiteracyConfirmSchema).output(AiLiteracyStatusSchema), aiLiteracyConfirmations: procedure.route({ method: "GET", path: "/settings/ai-literacy/confirmations", summary: "List AI literacy confirmations" }) .meta({ floor: "maintainer" }).output(AiLiteracyListSchema), get: procedure .route({ method: "GET", path: "/settings", summary: "How this sandbox is configured", description: "Every setting that governs how agents behave here, with the defaults filled in for anything nobody has chosen.", }) .meta({ guest: true }) .output(SandboxSettingsSchema), set: procedure .route({ method: "POST", path: "/settings", summary: "Change the sandbox settings", description: "Writes the settings whole, so send the complete object rather than the fields you changed.", }) .input(SandboxSettingsWriteSchema) .output(OkSchema), savings: procedure .route({ method: "GET", path: "/settings/savings", summary: "What the token-saving measures were worth", description: "Measured rather than estimated: what each mechanism actually saved over a range of days. The same day range the spending ledger takes, so one calendar filters both.", }) .input(DayWindowQuerySchema) .output(SavingsReportSchema), builtinPrompt: procedure .route({ method: "GET", path: "/settings/system-prompt/{base}", summary: "Read a built-in system prompt", description: "The actual text behind one of the built-in modes, so a settings screen can show the prompt instead of asking anyone to trust a description of it, and so either can be forked into a custom one.", }) .input(BuiltinPromptSchema) .output(BuiltinPromptTextSchema), // Own route, not a settings field: a firing isn't an edit, so it shouldn't cost a settings write. firings: procedure .route({ method: "GET", path: "/settings/rule-firings", summary: "When each rule last did something", description: "A separate read rather than a field on the settings, because a rule firing is not somebody editing anything: folding it in would turn every firing into a settings write and put a self-changing value inside the object a screen edits.", }) .output(RuleFiringsSchema), // Read off the repositories themselves, not out of the settings file: the declaration is a tracked file in each // repository, and only the owner's answer to it lives here. repoChecks: procedure .route({ method: "GET", path: "/settings/repo-checks", summary: "What each repository asks to run on its own code", description: `Every repository that declares its own checks at \`${REPO_CHECKS_FILE}\`, what it declares, and whether you have switched it on. A repository declares what to run because the command belongs beside the scripts it names; nothing it declares runs until you say so.`, }) .output(RepoChecksListSchema), // Read off the file and the automation store rather than the settings manifest: nothing here is a choice anyone // made, so storing it would only give it a second place to be wrong. fieldNotes: procedure .route({ method: "GET", path: "/settings/field-notes", summary: "The state of this sandbox's field notes", description: `Whether \`${FIELD_NOTES_FILE}\` exists, when it was last rewritten, how much of it the current budget reaches, and whether a monthly rewrite is scheduled.`, }) .output(FieldNotesStatusSchema), // Its own route rather than a field on `set`, for one reason: this is the only write that must NOT overwrite. The // browser offers the zone it is in; the daemon takes it only if nobody has chosen one. Folding it into the // whole-object `set` would mean the second machine to open the workspace silently moved every schedule to its own // clock, and an owner in Warsaw would find their chores on a colleague's Tokyo hours. adoptTimezone: procedure .route({ method: "POST", path: "/settings/timezone", summary: "Offer this sandbox a clock, if it has none", description: "Sets which timezone this sandbox's schedules are meant in, but only while it has none set. Already answered, the stored zone wins and comes back unchanged, so any number of browsers can offer theirs without fighting over it. To change a zone that is already set, write the settings.", }) .input(TimezoneOfferSchema) .output(TimezoneStateSchema), // A person's own preference rather than a sandbox setting: kept per member, so every device a person opens this // sandbox on reads it, and nobody's answer changes another member's screen. Anyone signed in may read and give their // own, a guest included, since it changes nothing but the words they are shown. audience: procedure .route({ method: "GET", path: "/settings/audience", summary: "Which words the editor uses for you here", description: "Whether your editor speaks git's own words (developer) or plain ones (maker) on this sandbox, in every browser and on every device. Absent until you have chosen here.", }) .meta({ guest: true }) .output(MemberAudienceSchema), // A browser handing over the answer it kept on its own must not overwrite one the person already gave on another // device, which is what `offer` says; a plain answer replaces it. setAudience: procedure .route({ method: "POST", path: "/settings/audience", summary: "Choose which words the editor uses for you here", description: "Sets whether your editor speaks git's own words (developer) or plain ones (maker) on this sandbox, for every browser and device you open it on. Other members keep their own. With `offer` it is taken only while you have none kept, and the kept answer comes back unchanged.", }) .meta({ guest: true, floor: "viewer" }) .input(AudienceAnswerSchema) .output(AudienceStateSchema), // Like the audience, a person's own and not a sandbox setting: nobody's choice to put the checklist away or pass a // step over changes another member's screen. gettingStarted: procedure .route({ method: "GET", path: "/settings/getting-started", summary: "What you said about the getting-started checklist here", description: "Whether you put the editor's getting-started checklist away on this sandbox, and which of its steps you passed over. Whether a step is done is not kept here: the editor reads that from the sandbox itself.", }) .output(GettingStartedSchema), setGettingStarted: procedure .route({ method: "POST", path: "/settings/getting-started", summary: "Put away or bring back the getting-started checklist", description: "Replaces what you said about the editor's getting-started checklist on this sandbox: whether it is put away, and which steps you passed over. Other members keep their own.", }) .meta({ floor: "viewer" }) .input(GettingStartedSchema) .output(GettingStartedSchema), adoptRepoChecks: procedure .route({ method: "POST", path: "/settings/repo-checks/adopt", summary: "Switch a repository's own checks on or off", description: "Adopts exactly what that repository declares as it stands now. If the declaration changes afterwards it stops running until you adopt it again, so a command nobody has read cannot inherit the answer given to a different one.", }) .input(RepoChecksAdoptSchema) .output(OkSchema), };