/** * Agent profiles — a named role applied at spawn (dev, marketing…): a system * prompt, native Claude permission guardrails (e.g. deny git writes), extra * secrets, and an optional model. GLOBAL: one profile is reusable across repos. * Stored at ~/.shadok-ai/profiles.json (600 — it can hold secrets). * * SOFT isolation: agents run as the same OS user, so this is a role + guardrail * layer, NOT a security sandbox (an agent that tries can read another's env). */ export interface Profile { name: string; /** * The role text. ABSENT on a shipped profile the user never edited: the build's * own prompt is resolved at spawn instead (`effectiveProfile`), so it can never * go stale. Present means the user owns it. */ systemPrompt?: string; /** * The shipped prompt this one was forked from, recorded when a shipped role is * first edited. It is what lets us say "the build has moved since" rather than * merely "you edited this". */ promptBase?: string; /** Claude permission deny patterns, e.g. "Bash(git commit:*)". */ deny?: string[]; /** Claude permission allow patterns (optional). */ allow?: string[]; /** Names of vault secrets to inject as env (references, not values). */ secrets?: string[]; model?: string; /** May this agent answer a dialog pending on one of ITS OWN children? * Opt-in on purpose: answering a child's permission prompt lets a * READONLY_DENY agent authorise a child to do what it cannot do itself, so * as an ambient right it would make the guardrail that forces delegation * bypassable BY delegating. */ canAnswerChildren?: boolean; } /** Deny patterns that keep an agent from writing to git — the read-only preset. */ export declare const READONLY_DENY: string[]; /** * Deny patterns that keep an agent out of the project's SOURCE, while leaving * everything else writable — the shape `READONLY_DENY` deliberately does not * have (that one blocks git and lets Write/Edit through, because its roles * deliver a file). * * Three directory names and no more. It cannot be complete and does not need * to be: every entry added here is a directory some other ecosystem uses for * something else, and on a layer this soft — an agent still writes anywhere * through a shell redirection — a false block costs more than a missed one. * The rule that actually governs is the one in the role's prompt. Each name is * anchored twice because a monorepo keeps its source at `packages//src` as * readily as at `src`. */ export declare const SOURCE_WRITE_DENY: string[]; /** * The reading rule every role that lands in an unknown project starts from. * * One constant rather than a paragraph copied into each prompt: this text is * the fix for a bug (a role naming THIS repository's convention files as if * every project had them), and three copies of a fix drift back one at a time. * `test/role-catalogue.test.ts` holds the rule for the whole catalogue. */ export declare const READ_THE_GROUND = "Start with whatever convention file this project actually carries \u2014 CLAUDE.md, AGENTS.md, CONTRIBUTING.md, the README \u2014 then its layout, and its recent history where there is version control. Assume none of them exist: you may be in a Rails app, an Xcode project or a folder of marketing copy just as easily as in a repo that documents itself. Their absence is information about the project, not a failed lookup \u2014 infer the conventions from what IS there, and never invent one."; /** * Starter profiles, seeded on first run and topped up by later releases * (`seedMissingPlan`, minus whatever the user deleted on purpose). Roles are * generic on purpose — project specifics live in whatever the agent reads on * the ground, never in a profile, because profiles are global and projects are * not. Secrets are left empty: the user ticks which vault secrets each one * injects, and a role that could grant itself one would not be a guardrail. * * A ROLE EARNS ITS PLACE ON GUARDRAILS, SECRETS OR METHOD — NEVER ON TOPIC. * Two roles differing only in subject matter are one role with two briefs, and * a catalogue nobody can choose from costs more than a missing role. Today's * four guardrail shapes, each a different answer to "what may this change?": * * none — Shadok-dev: writes anything, lands nothing. * READONLY_DENY — Boss / Marketing / Content / Support: git blocked, the * files open, because their deliverable IS a file. * SOURCE_WRITE_DENY — Shadok-QA: the source blocked, git open, because its * deliverable is a branch carrying a failing test. * both, or the file * tools outright — Shadok-Product (spec, not code) and Shadok-Release * (runs the deploy path, changes nothing it deploys). */ export declare const DEFAULT_PROFILES: Profile[]; /** Pure: a system-prompt line telling the agent which env-var secrets it has, * so it knows what's available without hunting. "" when there are none. */ export declare function envVarsNote(names: string[]): string; export declare function loadDeclined(): string[]; export declare function loadProfiles(): Profile[]; export declare function getProfile(name: string): Profile | undefined; /** Seed the starter profiles once, when the store is still empty. Idempotent. */ /** * Install the starter profiles on a fresh instance — WITHOUT their prompts. * * Storing the text would freeze it: profiles.json then owns a copy that no * later release can move. Seeding the guardrails only leaves each role tracking * the build's prompt (`effectiveProfile` resolves it at spawn), so an instance * installed today and one installed a month ago run the same role. */ export declare function seedDefaultProfiles(): string[]; export declare function profileNames(): string[]; export declare function upsertProfile(p: Profile): void; export declare function removeProfile(name: string): void; /** * The extra `claude` CLI args a profile contributes: an appended system prompt * (role), inline permission settings (deny/allow), and a model. Returns [] for * an undefined/empty profile so a no-profile spawn is unchanged. */ export declare function profileArgs(profile?: Profile | null): string[]; /** * Permission modes we accept. "default" is our sentinel for "pass no flag" (use * Claude's own default); the rest are the real `claude --permission-mode` * choices (v2.1.x). "auto" is the Shift+Tab "auto mode on"; "acceptEdits" only * auto-applies file edits. */ export declare const PERMISSION_MODES: readonly ["default", "manual", "acceptEdits", "auto", "dontAsk", "plan", "bypassPermissions"]; export type PermissionMode = (typeof PERMISSION_MODES)[number]; /** Whether `m` is a permission mode we recognize. */ export declare function isPermissionMode(m: string): m is PermissionMode; /** * The `--permission-mode` flag for a spawn, given the resolved mode. Empty / * invalid / "default" → [] (no flag, Claude's own default). Otherwise the * matching `claude --permission-mode ` flag. */ export declare function permissionModeArgs(raw: string | undefined): string[]; /** The managed profile behind the "Tweak Shadok-AI" CTA. */ export declare const TWEAK_PROFILE_NAME = "Shadok-Tweak"; /** * Pure: the profile after refreshing ONLY its system prompt. The prompt is * server-owned (it tracks `context/tweak-prompt.md` and would otherwise go * stale in the user's profiles.json), but a secret or model the user attached * in the editor is theirs and survives. */ export declare function withManagedPrompt(existing: Profile | undefined, name: string, systemPrompt: string): Profile; /** * One-off migration: every profile whose stored prompt merely repeats the * build's starts tracking it instead. Returns the names adopted (for the log). * * Idempotent, and a no-op on a fresh install. Nothing changes today — the * resolved prompt is byte-identical — but from now on those roles follow the * build instead of the copy their first boot happened to write. */ export declare function migrateToTracking(): string[]; /** Install/refresh the tweak profile from the repo's prompt file. Idempotent. */ export declare function seedTweakProfile(systemPrompt: string): void; /** The lead profile: the only one allowed to shape OTHER profiles (see below). */ export declare const BOSS_PROFILE_NAME = "Shadok-Boss"; export type PromptEdit = { ok: true; create: boolean; } | { ok: false; error: string; }; /** * Who may rewrite whose `systemPrompt` — the whole policy, in one pure place. * * An agent may reshape its OWN role and nothing else; the lead profile may * reshape any role and mint new ones. What NOBODY may touch through this path * is `deny`/`allow`/`secrets`/`model`: the guardrails stay the human's, behind * the same-origin gate on `PUT /profiles`. Letting an agent edit its own `deny` * would let a read-only agent hand itself git writes. * * Soft by construction: agents run as the same OS user and can rewrite * ~/.shadok-ai/profiles.json directly. This removes the accident and keeps the * capability off the documented surface — it is not a sandbox. */ export declare function promptEditVerdict(opts: { /** Profile of the calling session, null when it spawned bare. */ caller: string | null; /** Profile being edited. */ target: string; targetExists: boolean; /** Prompt owned by the server (refreshed from a repo file at every boot). */ managed: boolean; }): PromptEdit; /** Where a profile's prompt comes from, seen from THIS build. */ export type PromptOrigin = "tracked" | "edited" | "outdated" | "custom"; /** * What a stored profile's prompt IS, relative to the one this build ships. * * tracked — nothing stored: the build's prompt is used, always current. * edited — the user wrote their own, and the build has not moved since. * outdated — the user's own, but the build's has changed since they forked. * custom — a role this build knows nothing about; entirely theirs. * * A shipped role that is never touched stores NO prompt at all. That is the * whole point: a copy is what goes stale, so there is no copy. Before this, the * starter prompts were written to profiles.json on first boot and then owned by * that file forever — an instance installed weeks ago still ran the wording of * the day, untouched and silently behind, and only the managed Shadok-Tweak * role escaped it by being rewritten at every boot. * * Trimmed on both sides: a trailing newline from an editor is not a rewrite. */ export declare function promptOrigin(stored: Profile, shipped: Profile | undefined): PromptOrigin; /** * The profile to actually spawn with: a tracked prompt is filled in from the * build at the moment it is used, never from a copy. */ export declare function effectiveProfile(stored: Profile | undefined | null): Profile | undefined; /** * Drop a stored prompt that merely repeats what the build ships, so the profile * starts tracking it. Pure; the caller persists. * * This is the migration for every instance created before tracking existed: * their profiles.json holds the starter prompts verbatim. Dropping them changes * nothing today — the resolved prompt is identical — and means every later * improvement reaches them. A prompt that differs is the user's and is left * exactly as it is. */ export declare function adoptTracking(list: Profile[]): { profiles: Profile[]; adopted: string[]; }; /** The starter profile this build ships under `name`, if any. */ export declare function shippedProfile(name: string): Profile | undefined; /** * Which shipped roles are missing from this vault and should be installed. * * `seedDefaultProfiles` was all-or-nothing — it bailed the moment the vault * held anything — so a role added in a later release NEVER reached an existing * instance. Adding one is cheap now that prompts are tracked: only guardrails * are stored, and the text follows the build. * * `declined` is what makes it bearable: without it a role you deleted would * come back at the next boot, and we auto-update often enough that deleting * would be impossible in practice. */ export declare function seedMissingPlan(shipped: readonly Profile[], storedNames: readonly string[], declined: readonly string[]): string[]; /** * The declined list after removing or (re-)creating `name`. * * Deleting a SHIPPED role records a refusal; creating it again withdraws it — * an explicit choice must not leave the role silently skipped by later * releases. A role the user invented is never listed: nothing would re-seed it. */ export declare function declineList(declined: readonly string[], name: string, action: "remove" | "keep", shippedNames?: readonly string[]): string[];