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
88 changes: 82 additions & 6 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,8 @@ that issue instead of expanding the PR. Continue with independent issues.
- **Execution client**: the product registers only `connector_type=agents_api`
and uses `packages/agents-client/v1` with the official OpenAI Go SDK.
Core owns daemon connections, engine selection and environments.
Product image builds include `contracts/agents-api/v1` for shared public
configuration validation, alongside the client package.

## Architecture boundaries

Expand Down Expand Up @@ -2372,7 +2374,8 @@ raw tool Items, general web-search control or text-verbosity levels. Router admi
for `environment:none` uses the available engine capability, not an engine name.
The independent API selects new Session engines through `AGENTS_API_ENGINE`
(`codex` by default, `claude_sdk` or `mcode`); existing Sessions keep their stored engine.
This is operator configuration, not a public harness-selector field. API admission,
This remains the deployment default; the optional Core harness extension selects
an enabled engine for one saved or inline Agent configuration. API admission,
device selection and the final preclaim check share the execution service's narrow
engine policy without importing native adapters. Selected engines require the common
durable execution capabilities. Codex retains its general search/verbosity checks;
Expand Down Expand Up @@ -2564,8 +2567,9 @@ or filesystem isolation. Automatic installation remains separate.
- Configure product-to-Core access with `PARSAR_CORE_WORKSPACES_FILE`: each
workspace maps to a distinct Core project and caller key file. See
[product deployment](docs/deploy/product-core.md) for the complete configuration.
Provider credentials belong in the independent Core service. An unconfigured
product can start; Core operations return an explicit unavailable error.
Workspace model Provider credentials belong to the Parsar catalog and are sent
through the write-only Core Session execution extension. Operator defaults remain
an independent Core deployment option. An unconfigured product can start; Core operations return an explicit unavailable error.
- Root Compose deploys the product and its database. It mounts
`PARSAR_CORE_CONFIG_DIR` read-only for workspace bindings and caller key files.
The installer creates an empty binding list without overwriting existing
Expand Down Expand Up @@ -2629,12 +2633,84 @@ or filesystem isolation. Automatic installation remains separate.
four platforms. Companion installation does not grant API authorization;
task-scoped uploads keep the current run requester and workspace checks.

### Product model catalog and execution credentials

Parsar owns workspace Model Providers, their write-only API keys, model catalog,
Agent model selection and business permissions. Reuse the existing `models` table
with workspace-owned Provider references; do not reactivate legacy product model
calls, probes or runtime adapters. Core has no product Provider CRUD or catalog.
Build → Models groups model rows under Providers using the Runs ledger pattern,
with Provider settings in the detail rail; Credentials remains for
third-party platform credentials. Members may read safe catalog metadata;
only workspace owners/admins may mutate it. Audit mutations without keys or input
payloads. Product keys require `PARSAR_MASTER_KEY` and authenticated workspace and
resource identity within the existing encrypted envelope.

An Agent saves `config.model_id` plus the exact resolved `model` string. The UI
selects from its workspace catalog and validates protocol/Harness compatibility.
Catalog identity and token limits are immutable; rename changes the display label
only. Anthropic Messages supports Claude Code and MiniMax Code; Responses supports
Codex. MiniMax Code requires explicit context/output limits. These checks establish
configuration compatibility, not availability of an arbitrary upstream model.

Before the first Core request, atomically freeze model identity and Provider settings
under the catalog row locks. Encrypt the private Provider snapshot separately from
ordinary `product_core_sessions.request`, bound to workspace and binding ID. Recover
an existing binding before consulting mutable catalog rows. Provider edits/deletion
and Agent edits apply to new Sessions only. Existing Sessions retain their model,
endpoint and key; key rotation does not rewrite them. A deleted selection fails new
execution explicitly. Omitted catalog selection on older Agents preserves their
existing operator-configured execution behavior.

Only Session creation carries the typed write-only execution configuration; the
product connector adds it after loading its durable snapshot. Core encrypts it in
the Session transaction using the existing credential cipher and tenant/Session
binding. Creation idempotency includes the confidential intent by hash. The native
adapters consume this snapshot without operator credential fallback. Missing keys,
decryption failure or incompatible configuration fail closed. Public resources,
events and ordinary configuration must not expose the key. Current support requires
a qualified hosted environment. See [Session model execution](contracts/agents-api/model-execution.md).

### Harness selection and Agent defaults

Core accepts the optional `agent.x_agents_core.harness` extension through the
saved Agent and inline Session configuration paths. Define the extension once in
`contracts/agents-api/v1`; never use metadata or a competing top-level selector.
Resolve saved overrides before selecting the existing Session engine, and apply
that engine's execution policy before persistence. Omitted selection preserves
the deployment default; explicit unavailable selection fails without fallback.
Effective extension reads use the persisted engine; Sessions without the extension
retain the official Agent response shape. See the [extension contract](contracts/agents-api/harness-selection.md)
for null/retry behavior and operator configuration.

Hosted engine-to-provider selection belongs to Core composition. Admission and
initial allocation share the mapping; retained allocations use their persisted
provider identity. Runtime images must satisfy their existing qualification rules.
Transient model options are partitioned by engine and must not expose another
engine's credentials. Do not infer an engine from a model name or template.

Parsar Agents save default model, harness extension and a typed, non-confidential
`config.environment` selector. The Agent form requires an explicit harness before
saving; existing records without one remain unset in read-only views. The Agent
list shows the configured environment type and harness in both table and compact
layouts. The environment selector is product configuration;
it becomes the official Session `environment`, never part of the protocol Agent.
Only a template reference or supported environment selector is stored, not live
container identity or initialization secrets. An explicitly chosen conversation
environment retains its existing override behavior. Otherwise the first message
uses the Agent defaults. Creating an Agent or opening an empty chat performs no
Core Session or container creation. One conversation/Agent binding freezes the
request on first execution; later messages and observer retries reuse it. Separate
conversations get independent Sessions and environments. Edits affect future
Sessions only. Keep the existing product navigation and direct empty-chat composer.

### Product Core execution

- Product Agent configuration uses the complete pinned inline Agent contract:
model, instructions (mapped once from the product `system_prompt` field), tools,
service_tier, reasoning, text and multi_agent. Do not narrow this schema to the
current Core MVP. Reject old engine/device/provider settings; Core validates
current Core MVP. The explicit Core harness extension and product environment
defaults follow the rules above. Reject old engine/device/provider settings; Core validates
its current execution support. Business display/visibility stay in Parsar.
A supplied product `config` replaces the execution configuration; omitted config
preserves it. The separately supplied SP remains independent of that replacement.
Expand Down Expand Up @@ -3302,8 +3378,8 @@ wrapped rows grow naturally. Keep this behavior in the shared header, with
`actionClassName` reserved for page-specific action arrangements.

Agent forms edit the model, instructions and pinned upstream Agent configuration.
Environment templates belong to Core; Session creation chooses a template or
an explicit environment type. Preserve drafts when an upstream operation fails
Environment templates belong to Core; Agent forms save a default template or
explicit environment type for new Sessions. Preserve drafts when an upstream operation fails
and show unavailable features without inventing a successful local substitute.
New and cloned Agents default invocation scope to workspace. Scope choices
explain the Feishu gate without implying anonymous Web/API access.
Expand Down
1 change: 1 addition & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ RUN --mount=type=cache,target=/go/pkg/mod \

COPY internal ./internal
COPY catalog ./catalog
COPY contracts/agents-api/v1 ./contracts/agents-api/v1
COPY packages/agents-client ./packages/agents-client
COPY server ./server

Expand Down
1 change: 1 addition & 0 deletions apps/web/src/components/layout/AdminLayout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ const menuGroups: MenuGroup[] = [
// and the platforms it can be let out on.
groupKey: "buildGroup",
items: [
{ id: "models", itemKey: "models", icon: Database },
{ id: "environments", itemKey: "environments", icon: Database },
{ id: "capabilities", itemKey: "capabilities", icon: Wrench },
{ id: "connections", itemKey: "connections", icon: Plug },
Expand Down
20 changes: 9 additions & 11 deletions apps/web/src/components/layout/SessionLauncher.tsx
Original file line number Diff line number Diff line change
@@ -1,19 +1,17 @@
import { useEffect, useState } from "react"
import { useEffect } from "react"
import { useWorkspaceId } from "../../lib/workspace"
import { useMyWorkspaces } from "../../lib/api-workspaces"
import { StartSessionDialog } from "../../pages/admin/core/StartSessionDialog"
import { useAdminView } from "../../lib/admin-router"

export function SessionLauncher() {
const workspaceID = useWorkspaceId()
const workspaces = useMyWorkspaces()
const role = workspaces.data?.workspaces.find(workspace => workspace.id === workspaceID)?.role
const [session, setSession] = useState<{ agentID?: string } | null>(null)
const { navigate } = useAdminView()
useEffect(() => {
const start = (event: Event) => setSession({ agentID: (event as CustomEvent<string | undefined>).detail })
const start = (event: Event) => {
if (!workspaceID) return
navigate("conversations", { focus: "compose", agent: (event as CustomEvent<string | undefined>).detail })
}
window.addEventListener("core:start-session", start)
return () => window.removeEventListener("core:start-session", start)
}, [])
return session && workspaceID && role && role !== "viewer"
? <StartSessionDialog key={`${workspaceID}:${session.agentID ?? ''}`} workspaceID={workspaceID} initialAgentID={session.agentID} onClose={() => setSession(null)} />
: null
}, [workspaceID, navigate])
return null
}
34 changes: 22 additions & 12 deletions apps/web/src/components/ui/ledger.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -255,32 +255,41 @@ export function LedgerGroup({
label,
count,
defaultOpen = true,
onSelect,
selected,
children,
}: {
label: React.ReactNode
count?: number
defaultOpen?: boolean
onSelect?: () => void
selected?: boolean
children: React.ReactNode
}) {
const [open, setOpen] = React.useState(defaultOpen)
return (
<section>
<button
type="button"
aria-expanded={open}
onClick={() => setOpen((v) => !v)}
className="flex h-7 w-full items-center gap-1.5 border-b border-line pl-[22px] pr-6 text-left text-xs text-fg-muted hover:app-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-accent/40"
aria-expanded={onSelect ? undefined : open}
aria-pressed={onSelect ? selected : undefined}
onClick={onSelect ?? (() => setOpen(v => !v))}
className={cn(
"flex h-7 w-full items-center gap-1.5 border-b border-line pl-[22px] pr-6 text-left text-xs text-fg-muted hover:app-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-accent/40",
onSelect && "h-9 text-sm",
selected && "app-selected",
)}
>
<ChevronDown
{!onSelect && <ChevronDown
className={cn(
"h-3.5 w-3.5 text-fg-muted transition-transform duration-200 ease-spring",
!open && "-rotate-90",
)}
strokeWidth={1.5}
aria-hidden="true"
/>
/>}
<span className="font-medium text-fg">{label}</span>
{count !== undefined && <span className="tabular-nums">{count}</span>}
{count !== undefined && <span className="text-xs tabular-nums">{count}</span>}
</button>
{open && <ul className="m-0 list-none p-0">{children}</ul>}
</section>
Expand All @@ -289,19 +298,20 @@ export function LedgerGroup({

export const LedgerRow = React.forwardRef<
HTMLLIElement,
React.LiHTMLAttributes<HTMLLIElement> & { selected?: boolean }
>(({ selected, className, children, ...props }, ref) => {
React.LiHTMLAttributes<HTMLLIElement> & { selected?: boolean; interactive?: boolean }
>(({ selected, interactive = true, className, children, ...props }, ref) => {
const { template, trailingActions, columns } = useColumns()
return (
<li
ref={ref}
role="option"
aria-selected={selected}
tabIndex={0}
role={interactive ? "option" : undefined}
aria-selected={interactive ? selected : undefined}
tabIndex={interactive ? 0 : undefined}
style={{ gridTemplateColumns: template }}
className={cn(
"group relative grid h-9 min-w-min cursor-default items-center gap-x-2.5 border-b border-line text-sm text-fg outline-none transition-colors duration-150 ease-settle hover:app-hover focus-visible:app-hover [&>*]:min-w-0 [&>*]:self-center",
"group relative grid h-9 min-w-min cursor-default items-center gap-x-2.5 border-b border-line text-sm text-fg outline-none transition-colors duration-150 ease-settle [&>*]:min-w-0 [&>*]:self-center",
gutterClass(trailingActions),
interactive && "hover:app-hover focus-visible:app-hover",
selected && "app-selected before:absolute before:bottom-0 before:left-0 before:top-0 before:w-0.5 before:bg-accent before:content-['']",
className,
)}
Expand Down
35 changes: 34 additions & 1 deletion apps/web/src/i18n/locales/en-US/admin.json
Original file line number Diff line number Diff line change
Expand Up @@ -2921,6 +2921,39 @@
"skillsPending": "Import, edit and version Skill assets in the capability library. Loading them for an Agent requires Core environment initialization support, which is not connected yet. Saving an asset does not activate it for execution.",
"memberEnvironment": "Choose an environment when starting a new conversation. Each conversation uses its own Core session and environment; an Agent does not own a permanent container or share a workspace across conversations.",
"templateSupport": "Name and enabled/disabled network access are connected. Packages, setup commands, environment variables, restricted network rules and capability installation are waiting for Core support.",
"assetManagementHint": "Manage Parsar capability assets and MCP configuration here. Importing or authorizing an asset does not enable Agent execution. Runtime loading belongs to Core and is not connected yet."
"assetManagementHint": "Manage Parsar capability assets and MCP configuration here. Importing or authorizing an asset does not enable Agent execution. Runtime loading belongs to Core and is not connected yet.",
"executionDefaults": "Default execution configuration",
"selectHarness": "Select a harness",
"harnessNotSet": "Not set",
"harnessHint": "Uses a harness configured in Core. Unavailable harnesses and unsupported model or environment combinations fail explicitly.",
"executionDefaultsHint": "Saving does not start an environment. The first message creates an independent environment, reused by this chat. Changes apply to new chats only."
},
"catalog": {
"title": "Models",
"model": "Model",
"selectModel": "Select a model",
"legacy": "Select a catalog model (previously {{model}})",
"unavailable": "Saved model is unavailable",
"agentHint": "Choose from Build → Models. Provider models require a hosted environment. Unsupported Harness combinations are disabled.",
"addProvider": "Add Provider",
"editProvider": "Edit Provider",
"addModel": "Add model",
"editModel": "Edit model name",
"providerHint": "Configure one API key per Provider, then add its models. Third-party platform credentials are managed separately in Credentials.",
"modelHint": "The model identifier is sent to Core unchanged. Provider and identifier cannot be changed after creation.",
"protocol": "Protocol",
"modelKey": "Model identifier",
"contextWindow": "Context window (tokens)",
"outputTokens": "Maximum output tokens",
"limitsHint": "Required for MiniMax Code. Enter the limits supported by this model; use 0 if you do not use MiniMax Code. Limits are fixed at creation.",
"keyUnchanged": "Leave blank to keep the current key",
"keyRequired": "Enter API key",
"configured": "Key configured",
"search": "Search Providers or models",
"empty": "No Providers yet",
"noModels": "Add a model under this Provider to use it in an Agent.",
"deleteTitle": "Delete {{name}}?",
"deleteHint": "Existing Sessions keep their frozen configuration. Agents using this model or Provider cannot start new Sessions until another model is selected.",
"incompatible": "Choose a compatible Harness and a hosted environment for this model."
}
}
Loading
Loading