Repository navigation
Derive every Core address from one public URL and report installation facts - #138
Merged
Merged
Conversation
… facts Core now takes the installation's address from one setting, AGENTS_API_PUBLIC_URL. The daemon WebSocket URL, the self-hosted remote_url, hosted Runtime bootstrap, node configuration and the sandbox deployment's core_url all derive from it. AGENTS_API_DAEMON_WS_URL and AGENTS_API_CONFIG_FILE are retired: setting either fails startup and names the replacement. The deployment no longer stores a Core address. POST and PUT /core/v1/sandbox/deployment reject a core_url member with 400 (param core_url); responses keep core_url as a read-only field, present before configuration. E2B selections return 409 sandbox_configuration_error while the public URL is loopback. A migration moves the saved address onto each enrolled node. Nodes record the address they enrolled with and report it as core_url; a node bound to another address receives no new sandboxes, while its retained sandboxes can still resume. GET /core/v1/installation (Core key) reports the public URL, API base URL, source commit, installation ID, the installer's settings snapshot (AGENTS_API_SETTINGS_FILE, validated at startup so a sensitive setting never carries a value) and counts of what is bound to the public URL. It is available before any sandbox deployment exists. AGENTS_API_DATABASE_PASSWORD_FILE keeps the database password out of the URL for the server, migrations and maintenance commands. Core and the console now apply PARSAR_LOG_LEVEL, PARSAR_LOG_FORMAT and PARSAR_LOG_ADD_SOURCE, which were documented but never read. The installer writes the new names only: AGENTS_API_PUBLIC_URL (or the loopback Core origin without --public-url), the password file and a URL without a password. The local node setup no longer sends core_url. The client drops core_url from deployment inputs, marks the deployment and node core_url read-only, adds AdminClient.retrieveInstallation with strict projection, and passes Core's public-URL rejection through for E2B writes unless it reflects the submitted key. Docs and the Core OpenAPI follow; application examples use OPENAI_BASE_URL and OPENAI_API_KEY.
…rule An E2B selection saved while the public URL was reachable stayed usable after the URL became loopback, so new hosted Sessions would have created sandboxes that cannot reach Core. Session placement now refuses them with 409 sandbox_configuration_error. The saved provider still loads, so existing E2B sandboxes remain reachable for cleanup and startup is not refused. The installer checks --public-url with valid_core_origin, which accepts exactly the origins Core's ValidateSandboxCoreURL accepts, including IPv6 literals. It now rejects underscores, trailing dots, non-ASCII hosts and non-canonical ports, which Core would refuse at startup. Both tests share the same cases. The client passes Core's public-URL rejection on as a new error with only its message and param. AGENTS_API_CONFIG_FILE now says to remove it instead of naming a replacement. A migration test covers the refused rollback of an E2B deployment. The upgrade guide lists the retired address settings and tells operators to reuse the deployment's core_url; docs no longer describe a saved or submitted Core origin, and the deployment contract says that work already placed on a node finishes there.
The sandbox wizard no longer asks for a Core address or sends core_url: its review shows the deployment's read-only address (config.json's public_url) with where it is set. When Core rejects a configuration it cannot serve (409 sandbox_configuration_error, such as E2B without an HTTPS public_url), the wizard keeps Core's message and adds the config file and apply command from GET /core/v1/installation; nothing is marked uncertain. Nodes warns when nodes are still bound to an old address.
One card, under the one-time key in the issued-key dialog and the pending key notice on Projects and keys: OPENAI_BASE_URL and OPENAI_API_KEY from the installation's API base URL and the new key, a curl request, a Python example with the pinned openai==3.13.0 and a note that execution needs a model. Without a public API address it says the API is only reachable locally. The key stays in the flow's memory only.
System lists the public address, API base URL, installation ID and source commit, then the config.json startup settings read-only: each value (sensitive ones only as configured or not set), default and fixed markers and the services a change restarts, under the copyable file and apply command. The sandbox section links to the Nodes page where it changes.
The fixture serves GET /core/v1/installation (public, local-only and stale-node modes), rejects core_url on deployment writes with 400, returns a read-only core_url and rejects E2B with 409 sandbox_configuration_error when public_url is not HTTPS. Acceptance covers the wizard without an address field, the E2B rejection, the how-to-call card, the startup settings and the stale-node warning.
DESIGN.md: the wizard shows Core's address read-only from config.json's public_url and explains an E2B rejection inline; the how-to-call card; the System page's sections; the Nodes old-address warning. PRODUCT.md: System and the OPENAI_BASE_URL/OPENAI_API_KEY guidance beside a new key. docs/web: the Platform row, the Agents API row, a GET /core/v1/installation row and the deployment row's read-only core_url.
The real client reports each node's enrolled core_url and requires api_base_url to be public_url followed by /v1, so a loopback install has a local API base URL and local_only set. The how-to-call card then shows the samples under a note that only the Core machine reaches them, and System marks the API base URL the same way. Test node fixtures carry core_url. The e2e fixture uses one UUID installation ID, derives api_base_url and the stale-node count from the nodes' core_url, and the add-node E2B test no longer fills the removed address field.
The card no longer points at a System page setting that ships separately: a model provider comes with each request, from the saved Agent or from the deployment default.
The Nodes warning now comes from the node snapshot the page already reads: a node whose core_url differs from the deployment's core_url (config.json's public_url). It updates with every refresh, removal and enrollment, names the nodes and says to remove and add them again, as a status rather than an alert. Refreshing the page also re-reads the installation the wizard uses.
The Shell sample exports OPENAI_BASE_URL and OPENAI_API_KEY together, so a shell that already exports one of them cannot send the new key to another base URL. Each sample's copy button reports a refused clipboard and selects the sample for a manual copy, as the key's own button does. Without a public address the card says to set public_url, and a skeleton replaces the loading line.
After a save attempt clears the E2B key, the review says to enter it again and links back to the E2B step instead of leaving Save silently disabled. A loopback public_url carries a caution that only the Core machine reaches it. The E2B rejection test also checks that the key is gone from the page.
A write that sends core_url gets Core's 400 (invalid_request_error, param core_url, its message), and E2B on a loopback public_url gets ErrSandboxPublicURLUnreachable's message. The installation's binding counts follow the fixture's allocations and executor credentials.
A local install's public_url is a loopback HTTP address, so the help no longer calls it the one HTTPS address.
Core rejects E2B whenever public_url is loopback, whatever its scheme, so the wizard's caution asks for a public_url other machines can reach, and comments and docs name the loopback rule.
Installation and Sandboxes show facts-only placeholders and Startup settings a table placeholder, all silent; one status says the page is loading while either first read is out. TableSkeleton stays silent without a label.
Setup answers 200, a core_url member is rejected before any state check, and hosted_sandboxes is the deployment's allocations plus pending for every provider. The setup test waits for Add node and the recorded write instead of the Runtime text, and the stale-node test proves removal alone clears the warning.
Stale node names join as a list in the current language. The samples' copy buttons read "Copy the Shell sample" / "复制示例:终端". A unit test ties the Python sample's openai pin to contracts/agents-api/upstream.json, and PRODUCT.md names the how-to-call card on the first-run screen.
ErrSandboxPublicURLUnreachable no longer ends with "then save again".
SaladDay
added a commit
that referenced
this pull request
Sep 25, 2026
…osted model gap (#140) * Store deployment model providers in Core and require a provider for hosted and self-hosted Sessions Model credentials get one visible home and the self-hosted model gap from the fc19bd5 acceptance (D2, D3) is closed. - Deployment default model providers are a runtime setting: one complete bundle per harness in PostgreSQL (migration 000075), sealed with a new harness-bound credential purpose, managed only with the Core key through GET /core/v1/harnesses and GET/PUT/DELETE /core/v1/harnesses/{harness}/model-provider. Reads return the safe view (api_key_configured, updated_at), never the key. Writes are audited as deployment-wide admin_audit_log entries with a null tenant/project scope. - Provider precedence stays Session, saved Agent, deployment default. The default applies to openai_hosted and none Sessions and is frozen through the existing encrypted Session snapshot; later edits never reach existing Sessions. self_hosted Sessions now accept Session and Agent bundles, delivered over the existing daemon connection, and never use the deployment default. - openai_hosted and self_hosted Sessions without a bundle are rejected at creation with 400 model_provider_required. Legacy rows without a snapshot are rejected at message admission and at dispatch instead of falling back to a built-in endpoint. deploy/install/model_provider_sessions.py counts them read-only before an upgrade. - AGENTS_API_EXECUTION_OPTIONS_FILE is retired and stops startup with the replacement named. Historical snapshots keep their frozen native options. The execution-configuration read shows the safe view of new deployment selections. - packages/agents-client: listHarnesses, retrieveHarnessModelProvider, setHarnessModelProvider, deleteHarnessModelProvider, CoreHarness, HarnessModelProvider; audit entries may have project_id null. - Contracts, docs and the Core OpenAPI are updated. Tests that create hosted or self-hosted Sessions use shared provider fixtures instead of relaxing the check. * Name only existing steps in the retired execution options startup error The error for AGENTS_API_EXECUTION_OPTIONS_FILE now says to remove the setting and set deployment default model providers in Web (System) or with PUT /core/v1/harnesses/{harness}/model-provider. The import command it named does not exist yet; the import work adds it back. * Record caller intent wherever the deployment default applies and address review P2-1: A same-key retry of a none Session conflicted after the deployment default changed, because the resolved provider fed the retry hash while caller intent was recorded only for openai_hosted. Caller intent is now recorded whenever the deployment default may apply (openai_hosted and none), and a provider key enters any stored hash only as a fingerprint keyed by the credential key (new Cipher.Fingerprint), never directly. Without the key, an intent lookup cannot match and creation reports the missing key. none Sessions now compare caller intent on retry, like hosted ones; rows without recorded intent keep the resolved-equivalence rule. Review follow-ups: - Input reserved before providers were required fails with model_provider_required (new environment_input_reservations.failure_code, migration 000075) instead of waiting for its deadline, and the Session reports that reason. - The 000075 down migration also refuses while deployment model providers exist. - Scope wording: the deployment default applies to openai_hosted and none, never to self_hosted, in the public API guide, the self_hosted 400 message, the client JSDoc and related docs. - Tests: none retry after the default is rotated and removed; a self-hosted preparation carries the frozen provider only to the bound executor; legacy reservations settle as failed. - Docs: a saved Agent's provider key reaches every self_hosted executor created with that Agent in the Project; legacy none Sessions run with the device's own environment and are not counted by the pre-upgrade check. * Test that intent is always recorded where the deployment default applies The vault intent unit test assumed a none Session without Vaults records no caller intent. none and openai_hosted now always record it; the unrelated inline identity case moves to self_hosted, where the default never applies. * Renumber the deployment model providers migration to 000076 Main's public URL binding (#138) took 000075. * Keep none retry equivalence and prove the keyed retry fingerprint P2-1: Recording raw caller intent for none Sessions broke the documented retry equivalence (omitted, null and explicit medium verbosity). none keeps the resolved-request rule again; caller intent stays hosted-only as before. The resolved hash now leaves out a deployment-sourced provider bundle and its configuration marker, so setting, rotating or removing the default does not change a none Session's retry identity. The earlier test edits for the intent-for-none rule are reverted. Review follow-ups: - withoutProviderKey fails closed: an intent whose extension or provider cannot be read is rejected, and the provider is re-encoded from its typed value with the key replaced by its keyed fingerprint. - Cipher.Fingerprint rejects a NUL in the purpose. - New test: the same request under two credential keys gets different request_hash and creation_request_hash; an unreadable provider intent is rejected. - The legacy test again asserts that a rejected submit queues nothing. - The none retry test also retries with a spelled-out default verbosity. - model-execution.md notes that key-bearing retries first sent before this release, or after a credential key change, conflict once.
SaladDay
added a commit
that referenced
this pull request
Sep 26, 2026
… parsar (#151) * Give every process setting one home in config.json, applied with parsar A new installation writes <install>/config.json, the only file an operator edits, with every setting that applies to its mode. The installer flags only seed it. deploy/install/config.schema.json describes it; config_model.py validates the small keyword subset the schema uses and a test keeps the schema inside that subset. The installation directory now holds secrets/ (one copy of each secret), state.json (identity and install facts) and generated/, which parsar apply derives from config.json: compose.json and core.env without secrets, the Core key digest file, the native unit, the settings snapshot Core serves at GET /core/v1/installation and runtime-history.json when set. Core reads the new AGENTS_API_PUBLIC_URL, AGENTS_API_DATABASE_PASSWORD_FILE and AGENTS_API_SETTINGS_FILE; the retired AGENTS_API_DAEMON_WS_URL and AGENTS_API_CONFIG_FILE are no longer generated. The parsar zipapp in the installation directory runs status, start, stop, apply and rotate-core-key without the bundle. apply refuses hand-edited generated files, changed fixed fields and changed secrets, confirms a public URL change that strands bound nodes, recreates only the services whose inputs changed (a Compose label carries each service's input digest) and restores the previous files when Core rejects the new ones. rotate-core-key cuts over at once. install.sh reruns read config.json, reject flags and repair; --status and --stop name their replacements. install.sh --convert moves an installation made before config.json to this layout and release. Its preflight changes nothing and stops on anything it can't convert; secrets are renamed, not copied; an interrupted conversion resumes. An AGENTS_API_EXECUTION_OPTIONS_FILE stays as it is until the model settings import, which hooks into parsar apply. scripts/config-reference.py renders the settings table in docs/configuration.md from the schema; make check-distribution checks it. * Describe config.json, parsar and --convert in the install and operations docs CONTRIBUTING now names config.json as the single operator file instead of forbidding a new configuration format. The install guide covers changing settings after installation, repair reruns and converting an earlier installation; the operations guide uses parsar status, start, stop and rotate-core-key. Key paths move from admin/ to secrets/. * Keep parsar status and start usable when config.json is invalid status and start report why config.json can't be used and fall back to the last applied settings, which the running services use. A missing retained execution options file names itself instead of failing generically. * Fix false hand-edit reports, native restarts and conversion edge cases Review round 1 on 8158ef0. - apply recomputes the files to write after stamping the snapshot, so settings.json is written whenever its digest is recorded. Rotation and applies that restore a file no longer report settings.json as edited. - An apply records the digests it is about to write before writing, then commits state.json. A run interrupted in between is finished by the next apply instead of being reported as hand edits; status says so. - Native Core restarts only when its inputs changed, including on the install.sh repair path where enable --now leaves an active unit alone. - A failed first start leaves the apply unfinished, and --convert finishes a converted installation whose first start failed. - Web-only conversion before its Core notes the older Core instead of failing; the next apply records the pairing. - The conversion preflight refuses secret files that are links or readable by others, starts an old native unit by path, lowercases an old public URL when case is the only problem and records no core.key digest. - A public URL change needs confirmation when the applied value can't be read; state.json records the applied public URL. - Rotation restores the old key only before the new apply is recorded; status reports a Core that rejects secrets/core.key and a mismatched digest file, and config.json fields that disagree with state.json. - The origin check refuses IPv6 literals, underscores, trailing dots, non-ASCII hosts and leading-zero ports. - rotate-core-key reads state under the lock, the Compose marker escapes $, and unused imports are gone. * Accept exactly the origins Core accepts, including bracketed IPv6 The public URL and web.core_url check now uses the rule of PR 1a's installer valid_core_origin, which matches Core's ValidateSandboxCoreURL: bracketed IP literals are accepted like Core accepts them, while underscores, a trailing dot, non-ASCII hosts and leading-zero ports stay refused. After 1a merges, config_model calls that one shared function. * Word first starts and repairs accurately in the installer output Found in the mx1 real check. A first apply says "Services to start" rather than restart, and only a new installation says it has no execution node; a repaired or converted installation keeps its nodes. * Converge on what runs instead of recorded apply state Review rounds 1 and 2 both found P1s in interruption bookkeeping, so apply no longer decides restarts from state.json. Every Compose service carries the digest of its inputs in the io.parsar.inputs label, and the native unit carries it as PARSAR_INPUTS, which apply reads back from the Core process. apply renders config.json, compares with the running services and recreates or restarts exactly those that differ, Core first. A Core that rejects secrets/core.key is restarted, then Web. An interrupted apply, rotation or rollback is finished by the next apply, and status reports services that run with other inputs than config.json renders. state.json keeps identity and install facts and the last digests written for each generated file, used only to detect hand edits: a file is edited only if it matches none of them and differs from the current render; a missing file is written again. Rollback happens only when every service ran with the previous files; rotation never rolls back. The applied and pending records are gone. --convert now accepts installs made by the #138 installer (public URL, password file, no retired variables) as well as earlier ones; retired variables may be missing and a hand-set AGENTS_API_PUBLIC_URL becomes public_url. The test fixtures come from both earlier generators, kept verbatim in deploy/install/testdata/. Also: rotation stops Web inside its error handling and a stale core.key.new is removed; failed first starts name the right rerun command, and a local node requested at install is enrolled by the repair; parsar refuses a symlinked or non-private installation, secrets/ or generated/ directory; a resumed conversion checks its bundle and may repeat --public-url; the old Runtime history file is removed once config.json holds it; the public URL confirmation lists only nodes on the old address; and a Web-only dry run sends no key to a changed web.core_url. * Say that an interrupted rotation was interrupted Found in the mx1 recovery check: the message had an empty reason. * Convert a local-only #138 install without a public URL, and check native hosts first Review round 3. At #138 the sandbox deployment's core_url is derived from AGENTS_API_PUBLIC_URL, including the loopback fallback, so conversion no longer adopts it when core.env names the address: the #138 value is used as is and the loopback fallback becomes public_url null, keeping Web's origin on its own port. Both --convert branches run the native preflight before anything changes. A fresh install writes state.json before config.json, and a directory left with only state.json gets a clear message. * Record rolled-back files and count bound nodes from Core's bindings Review round 3. A rollback records the digests of the files it restores, so repeated rejected applies no longer end in a false hand-edit report. The public URL confirmation counts bound nodes from /core/v1/installation's address_bindings, so a failed node list read still requires confirmation. The running-state read skips one-off Compose containers, parsar stop checks the directories, a failed command names its program, and a rollback that could not start the previous files says so. * Match Go's loopback rule, require format 1 and note native Web restarts Review round 3. valid_core_origin treats an IPv4-mapped loopback address as loopback, as Go does. format must be the integer 1, not true or 1.0. ports.core restarts Web too with native Core; the schema's native_restarts annotation carries that into the settings snapshot and the generated table. * Tell a live installation without config.json to restore it, never to start over Focused review. Only a directory that never applied anything (no recorded generated files, empty generated/) counts as an interrupted fresh install, including one stopped before state.json was written; an installation that lost config.json is told to restore it from a backup, with generated/settings.json for the last applied values, since deleting it would destroy the credential key and orphan the database. An unreadable installation.json gets conversion's own message. * Keep a hand edit visible after a rollback restores it Focused review. A rollback records only restored files that parsar wrote and that were not edited by hand, so a hand edit it puts back is still reported. * Refuse hand-set Core addresses that would move Web, and map loopback to none Focused review. A hand-set loopback AGENTS_API_PUBLIC_URL other than Core's own fallback is reported, and in all mode a hand-set address that differs from installation.json's public_url stops conversion until --public-url names the one to keep, instead of silently moving Web's origin. A pre-#138 deployment core_url equal to Core's loopback address means no public URL. The core.env value is not canonicalized twice, and its message points at config/core.env. The docs say the deployment is read only for pre-#138 installs. Tests: the deployment-read assertion now watches HTTP requests, the native core-only ports case is back, and the hand-set loopback and resume preflight paths are covered.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Phase 1a of the new-user install work: every Core address comes from one public URL, and a new read-only endpoint reports the installation's facts. This PR pairs the backend (Core, client, installer) with the Web changes (#135) because the sandbox deployment API change breaks the old wizard.
Core
AGENTS_API_PUBLIC_URLis now the only public-address setting. Everything else is derived from it:remote_url;core_url.AGENTS_API_DAEMON_WS_URLandAGENTS_API_CONFIG_FILEnow fail startup and name what to do instead. There is no fallback.core_urlinPOST/PUT /core/v1/sandbox/deployment. Sending it returns 400invalid_request_errorwithparam: core_url. Responses still carry it read-only, and it is always present.sandbox_configuration_error. With a saved E2B deployment, new hosted Sessions are refused the same way. Setup reads and cleanup of existing sandboxes keep working.000075_public_url_binding. It dropsruntime_deployment.core_urland records each node's enrolled address. Nodes on another address get no new sandboxes and must be re-added; work already placed on them finishes there. The down migration refuses when it can't restore the address.GET /core/v1/installation. Core key only,no-store, and it works before any deployment exists. It returns:AGENTS_API_SETTINGS_FILE, with sensitive items carrying onlyconfigured;AGENTS_API_DATABASE_PASSWORD_FILE. Core and migrate can read the database password from this file instead of the URL.PARSAR_LOG_*now takes effect in Core and Web;log.Initwas never called before. Withauto, logs are JSON when stderr is not a terminal.Client (
packages/agents-client)core_url.SandboxDeployment.core_urlandSandboxNode.core_urlare read-only.AdminClient.retrieveInstallation()returnsCoreInstallation, with a strict projection.sandbox_configuration_errorpasses through as a newAgentCoreError, only when neither the message nor the param contains the submitted key. Every other E2B write failure is still rewritten.Installer
valid_core_originapplies exactly Core's origin rule to--public-url.Web (#135)
OPENAI_BASE_URL/OPENAI_API_KEY, with curl and Python examples.Docs
OPENAI_BASE_URL/OPENAI_API_KEY.Review
1431360eand verified by tests and the gate, per the small-fix rule. One P3 is deferred to phase 2: enrollment should carry the node's own address.Verification
a1b623f4(the exact head, which includes main697c6a91): passed.make openapishows no drift, and migration tests cover up and down (Docker and E2B).Compatibility
AGENTS_API_DAEMON_WS_URLwithAGENTS_API_PUBLIC_URL, reusing the deployment's previouscore_urlso nodes stay current. They must also deleteAGENTS_API_CONFIG_FILE. See the upgrade table inoperations.md.install.sh --convert, which merges next with no release in between.Need help on this PR? Tag
@codesmithwith what you need. Autofix is disabled.