fix(webui): prevent skill cards from overflowing grid tracks - #1
Open
Preciousuche wants to merge 94 commits into
Open
fix(webui): prevent skill cards from overflowing grid tracks#1Preciousuche wants to merge 94 commits into
fix(webui): prevent skill cards from overflowing grid tracks#1Preciousuche wants to merge 94 commits into
Conversation
…ine-breaks fix(cli): wrap onboarding prompt input
Restore embedded Config layout and YAML sizing, use the Bankr brand asset when catalog metadata omits a logo, and add an explicit no-backup reset recovery action shared by chat controls. Refs use-agent-os#82, use-agent-os#84, use-agent-os#85
fix(frontend): repair settings, Bankr icons, and session reset
…ark latency ceiling
…atibility fix(platform): resolve windows compatibility type-checking and test s…
…7.25 chore(release): bump version to 2026.7.25
Use the gateway route channel metadata as the Telegram chat ID and map a thread-targeted reply_to value to message_thread_id. Add an end-to-end regression test from an inbound forum command through the Bot API payload. Fixes use-agent-os#50
…-gateway feat(provider): add OpenCAP gateway support
…ive-command-mentions-51 fix(telegram): handle native bot command mentions
…m-command-replies fix(telegram): preserve forum command reply targets
Acknowledge application commands before dispatch and complete deferred original responses for command, batch, and streaming reply paths.
Gracefully degrade Discord native command registration failures without aborting gateway startup. Adds regression coverage for REST errors and exhausted rate-limit retries.\n\nFixes use-agent-os#54
Derive Slack conversation types from channel ID prefixes and mark native slash forms as explicit interactions. Preserve group access controls while bypassing mention-only gating for slash commands. Refs use-agent-os#56
Grant operator.read to channel senders after access-policy admission while reserving operator.write for configured channel admins. Document the permission model and cover read/write authorization tiers. Refs use-agent-os#57
…view (use-agent-os#128) * test(memory): add a curated-memory retention benchmark "Memory works poorly" is a felt thing, not a number, so a rebuild has nothing to prove itself against. This drives the real turn path -- build_services -> TurnRunner.run -> nudge review -> the `memory` tool -- against a throwaway workspace and reports capture, recall, and noise. capture and recall are scored separately on purpose. A fact can be written to MEMORY.md and still be unreachable from a later session (injection budget, formatting, a store the prompt never reads), and a file diff alone scores that as a pass. Three measurement traps this had to avoid, each of which produced confident, wrong numbers on the way here: - Services must be built once and held open. The nudge review is a background turn scheduled after the reply is already on the wire, so building and tearing down services per turn kills it with "Storage not connected" while still reporting a healthy capture rate -- measuring only what the agent saved unprompted. - Both curated files ship with seeded template boilerplate that carries no delimiter. It snapshots as one blob and is re-serialized into many delimited entries on the first write, so subtracting entry-by-entry scores every template line as agent-written noise. - Turn capture is on by default: every turn is indexed and the retriever feeds matches back into the prompt, so a fact can be "recalled" having never reached MEMORY.md. `--isolate-curated` disables it, leaving the curated block as the only path a planted fact can travel. Three corpora: facts.jsonl states facts outright, facts_incidental.jsonl leaks them inside task requests, and facts_long_session.jsonl plants anchors then applies budget pressure. `--memory-char-limit` shrinks the budget so consolidation is reached without a hundred-turn run. The timezone fact deliberately plants UTC rather than a regional zone: the planted value has to differ from whatever the host reports, or a fabricated ambient timezone would score as a genuine capture. Reads and writes nothing outside a temp AGENTOS home. Turns cost real provider tokens and the model is stochastic, so a single trial is a smoke test -- use --trials 5+ before drawing conclusions. * test(memory): record the curated-memory baseline Five trials per corpus against bdacd6d, curated-only isolation. Capture 92%/100%, recall 84%/95% -- which contradicts the impression that memory "works poorly" and is the point of writing it down before a rebuild starts. Two findings the raw rates hide: The agent fabricates profile data. In 3 of 5 incidental trials it saved the host machine's timezone as a durable user fact, from a corpus that never mentions one. Worth being precise about the mechanism: the runtime block (agent.py:3795) emits only a bare UTC offset on this host, so the regional zone name in the saved entry was the model's own inference from that offset. The value existed nowhere in the input. Three prompt surfaces also name timezone as a canonical USER.md field, so the model was largely doing as instructed. The failure mode is precision, not retention. The nudge fired zero times across all ten trials. Its counter resets whenever the turn already used the memory tool, and this agent saves unprompted on nearly every turn, so the review path is dormant while the model is diligent -- and largely untested in practice. Recall is understated: three misses were a turn timeout and two provider 404s, which the harness cannot distinguish from a wrong answer. The explicit corpus's 0.2 noise is likewise a matcher artifact -- the agent split a two-part fact across two entries, which is arguably correct. Records conditions, caveats, and what these corpora do not cover, since the felt failure is likely in long sessions or cross-session accumulation rather than anything measured here. Raw run JSON is deliberately not committed: it embeds environment-derived model output, which is how the host timezone leaked in the first place. * test(hygiene): fail when a tracked file carries the host timezone A regional IANA zone locates a contributor as precisely as a home path does, and it arrives the same way: someone writes a fixture from the machine in front of them. That is exactly what happened to the memory benchmark corpus, and the value then propagated into recorded model output before anyone noticed. Banning regional zones outright would be wrong -- the scheduler suite needs real zones to exercise DST and the docs need example values, some 55 legitimate uses in all. What is never legitimate is *this* machine's zone appearing in a tracked file, so that is what the check looks for. It fires on the machine where the mistake is being made and skips in CI, which is the right trade for an authoring guard. Resolving the host zone needs more than the obvious one-liner: on macOS `datetime.now().astimezone().tzinfo` is a fixed-offset object with no `.key`, so the check would report nothing and pass vacuously on the very machines it is meant to protect. `/etc/localtime` is a symlink into the zoneinfo tree on macOS and Linux alike and carries the real name. Also swaps the one pre-existing occurrence, a chat time-prefix fixture, for `UTC` at a zero offset. It only needs to match the strip regex. * fix(memory): stop teaching the agent to save ambient context as user facts Benchmarking curated memory turned up a precision failure: in 3 of 5 trials the agent wrote the host machine's timezone into USER.md from a conversation that never mentioned one. It was doing as instructed. Three prompt surfaces named timezone as a canonical USER.md profile field, and the per-turn runtime block is concatenated into the user's own message before the model sees it (agent.py:3850), so an injected value is indistinguishable from something the user said. Worse, the block emits only a bare UTC offset on a host whose tzinfo carries no IANA key -- the regional zone name in the saved entry was the model's own inference from that offset, a value that existed nowhere in the input. Timezone comes out of all three lists, and the write guidance now says plainly that the runtime environment -- date, time, zone, workspace path, OS, shell -- is injected context and never a fact about the user. Two rules ported from hermes-agent (MIT, (c) 2025 Nous Research) come in alongside, both aimed at what memory is *for* rather than what it may contain: - Declarative, not imperative. "User prefers concise responses", not "Always respond concisely" -- imperative entries are re-read as standing directives in later sessions and can override the request in front of the agent. - The one-week test. A fact that will be stale in seven days is not durable: no PR numbers, commit SHAs, or task progress. Also drops the dead `## Current Date & Time` section. Its only content was `Time zone: {{ timezone }}`, and the sole production caller never passed the argument, so it always rendered `UTC` while the per-turn block reported the real offset -- two contradictory timezones per turn. The per-turn block is authoritative and stays. * fix(memory): let the review fire even when the agent saves unprompted The nudge review fired zero times across ten benchmark trials. The counter was cleared whenever a turn used the memory tool, on the reasoning that an agent curating unprompted needs no nudge -- but a diligent model saves on most turns, so the counter never reached the interval and the review never ran at all. The two are not substitutes. A per-turn save records the one fact that was salient in that turn; the review re-reads the whole conversation and consolidates. Only the second one prunes and merges. So a self-directed write now holds the counter instead of clearing it, mirroring the early-return that excluded run kinds already use. The review is delayed by that turn, not cancelled. The one test pinning the old behaviour is rewritten, and a second test pins the regression directly: saving on every turn must still reach a review. * feat(memory): report what the background review wrote The review writes to memory on the user's behalf with nobody watching, and its event stream was drained and discarded. That has two costs: a memory the user cannot see is one they cannot correct, and a silent review is indistinguishable from one that never ran. The second cost was not hypothetical. The counter bug fixed in the previous commit -- zero reviews across ten benchmark trials -- stayed hidden precisely because a review that never fires and a review that fires and says nothing produce identical output. Memory tool calls are now collected off the review's stream and logged as `memory_nudge.review_done` with a count and a short rendering of each write, which surfaces them in `agentos logs` and the Control UI logs page. Batches collapse to their operation count: naming every op would push a single line past anything readable, and the count already separates a consolidation from a one-off save. Entries are capped at five per line and truncated at 120 chars. The point is to show what landed, not to reproduce the store. * test(memory): record the measured effect of the memory fixes Same corpora, same matcher, re-run after the three changes. Fabrication is gone: the incidental corpus wrote no invented profile entry in any trial, down from 3 of 5, with capture and recall holding at 100%/95%. The review now runs -- seven reviews across five explicit trials, up from zero -- and each reports what it wrote. Records two things the table does not show. The incidental corpus still reports zero reviews, which is the honest limit of holding the counter rather than clearing it: every turn there carries save-worthy content, so the agent saves on all of them and the counter never advances. An agent that saves on literally every turn still gets no consolidation pass, and bounding the hold would close that. The explicit corpus's residual noise is the known matcher artifact, not a regression. * fix(memory): bound how long self-directed writes can defer the review Holding the counter instead of clearing it fixed the common case but left the pathological one: an agent that writes memory on every single turn still never advanced past the interval, so it was still never reviewed. The benchmark shows this is not theoretical -- on a corpus where every turn carries something worth saving, the model saves on all of them. Every user turn now advances the counter, including one that wrote memory. A write still defers the review, because running one immediately after the model curated is mostly wasted, but the deferral is capped at two intervals. Past that the review runs regardless. The two behaviours are not substitutes. A per-turn save records the fact that was salient in that turn; the review re-reads the whole conversation and consolidates, and only the second one prunes and merges. * test(memory): match planted facts at word starts Substring matching scored `go` against "Django" and `no ` against almost any sentence, so several rates were inflated by matches that were not really there. An alternative must now begin at a word boundary but may carry a suffix, so `mock` still matches "mocks" and `friday` matches "Fridays". Both halves are load-bearing: anchoring the trailing end too stopped a correctly-captured "never suggest mocks in tests" from matching `mock` at all, which read as a 40-point regression that had not happened. This matters beyond tidiness -- these numbers exist to be compared against a later rewrite, and a baseline wrong in either direction misreads the result. * test(memory): correct the measured effect and pin the matcher Re-measured with the corrected matcher, and the retention improvement reported earlier does not survive. Capture and recall are unchanged -- 92%/84% explicit, 100%/90% incidental -- and the 92% -> 96% / 84% -> 92% seen in an earlier pass was run-to-run variance on a stochastic model, not an effect. BASELINE.md now says so plainly rather than leaving a flattering number in a table someone will quote. What did move are the two things the fixes aimed at, and neither depends on the matcher: fabrication is gone (no invented profile entry in any trial, down from 3 of 5, and a fabricated entry matches no planted fact under any rule), and the review runs (twelve and ten, up from none, with counts taken straight off the log stream). Adds a test suite for the matcher itself, which was wrong twice in opposite directions -- substring matching scored `go` against "Django", then anchoring both ends stopped `mock` matching "mocks" and read as a 40-point regression that had not happened. It decides whether a planted fact counts as reaching memory, so a bug in it moves every rate the benchmark reports, and both failures are now pinned.
…available actions (use-agent-os#129) * feat(env): look for a credential before asking someone to produce one A variable reported as missing often is not. The operator has usually already run `gh auth login`, and telling them to go find a token they effectively lost is worse than looking where it lives. credential_sources registers the places a credential may already exist and answers two separate questions about each. Probing asks "could this supply it" using the source's own status check — `gh auth status`, never `gh auth token` — so a listing can offer an import without any call touching a secret. Reading happens only from an explicit import, because "AgentOS took my GitHub token and handed it to an agent" is not a surprise anyone should get. A test asserts the module exposes nothing that could hydrate on its own. Probe results are cached for a minute: a listing asks about every unset variable it knows, which would otherwise be one subprocess per row per refresh. env.list reports availableFrom on unset variables, `agentos env import NAME` and a "Use GitHub CLI" button act on it, and all three say the same thing afterwards — the value is a copy and will not follow the source's own rotation. Better stated once at import than discovered as a mystery 401. Also fixes a defect from use-agent-os#127: env_key is not always a variable name. Providers that authenticate by OAuth carry the literal string "OAuth" there, meaning "no API key involved", and taking it at face value put a variable called OAuth on the Environment screen that nobody could ever set. Entries now have to look like environment variables — upper case — which also holds for any future sentinel without this needing to know its name. * feat(skills): tell the agent what to do about a missing requirement, per surface skill_view now appends a setup note when a skill's requirements are unmet, and what it says depends on who is listening. A chat channel is told the secret must not be collected there, because a value typed into Telegram is stored in the conversation. An unattended run is told nobody is available and to continue with what works while stating what does not. An interactive session gets the actual command. Getting this wrong is not cosmetic: an agent handed "ask the user for the value" on a messaging surface will faithfully do it, and the credential ends up in a chat log. The skill still loads in every case. An agent that knows what is missing can do the parts that work and say plainly which parts cannot, which is more useful than refusing to open it. When the credential is already reachable — GITHUB_TOKEN with the GitHub CLI authenticated — the note leads with that instead of asking for a value. Ineligibility the operator cannot act on from here, like an OS mismatch, produces no instruction at all; inventing one would be worse than silence. skill_list drops the `Fix: env_set(...)` line shipped in use-agent-os#127. env_set is hidden by default, so that line pointed the model at a tool it usually could not call — the same dead-end this feature exists to remove. The listing keeps the diagnosis; how to fix it belongs in skill_view, where the agent is actually trying to use the skill, not repeated once per entry across a listing of fifty. * docs(web-ui): document the credential import offer on the Environment screen
…s#132) * feat(skills): make publisher real data behind a server-side allowlist Skill cards showed partner branding from a name heuristic, so who stands behind a skill was never actually recorded. Add a SkillPublisher field that a SKILL.md may select by id, and resolve that id against an allowlist so a third-party manifest cannot claim a partner's name, link, or logo — the frontmatter picks a publisher, it never describes one. The snapshot cache gets the same check, and its schema version is bumped so a stale v9 file is rejected instead of silently restoring every skill unbranded. Also drops _LAYER_ORDER, which nothing read; the precedence it documented is the iteration order of _get_layer_dirs(), where the comment now lives. * fix(skills): let the model actually see the skill list The injected skills block spent most of its budget on a <location> line nothing reads — skill_view resolves by name, and its not-found text tells the model not to go looking on disk. It also put the operator's home directory in every system prompt. Removing it drops full mode from 20953 to 16375 chars and compact mode from 6661 to 2083. Full mode still did not fit the 8000-char default, so every default install fell through to name-only: the model has never seen a single skill description. Raise the default to 24000 so descriptions fit with room for installed skills. Truncation kept a prefix of a bundled-first list, so a cut always landed on the skills the operator installed on purpose. Sort by layer precedence first (stable, so within-layer order is untouched) and return the dropped names, which the pipeline step now logs and republishes; skill_count and filtered_skill_ids describe what reached the prompt instead of what was thrown away. Eligibility was built once at import and caches negative which()/env lookups forever, so installing a binary never took effect until restart while the Skills page — which rebuilds per call — reported it ready. Build it per turn instead. * feat(skills): make skill availability an answerable question Nothing could say whether a skill was actually being offered to the agent, or why not: the gate lived inside the engine step, ran only during a turn, and recorded its verdict solely by a skill's presence in or absence from the injected prompt. Move the gate and the budget decision into agentos.skills.availability as gate_skills / plan_injection — pure functions over their inputs — and have filter_skills call them. Both now yield a SkillAvailability per skill (offered, reason, human-readable detail), published as ctx.metadata["skill_availability"], so the same two functions answer the question for a turn and for a caller asking before any turn has run. Ineligibility routes through diagnose_eligibility, so the detail names the missing binary, the missing variable and what it is for, and the install command, rather than a bare "ineligible". Details never carry a filesystem path. Also swaps the /home/... stand-in in test_skills_injector.py for /opt/..., which the public-release-hygiene path check reads as a user home. * feat(skills): make how a skill was acquired a first-class fact The Installed tab grouped skills by loader layer, which answers "which directory did this come from" — not the question the UI actually asks, which is "did I install this, and can I remove it". The lockfile knew the real answer and only the Community "Installed" chip ever read it. SkillAcquisition (shipped | hub | local) derives that from the lockfile per request. It is deliberately not a SkillSpec field and not written to the skill snapshot: it changes without any SKILL.md mtime changing, so caching it would go stale in a way the manifest check cannot detect. build_skill_inventory() is the one row builder — spec, eligibility, acquisition, publisher, availability — so the surfaces that each rolled their own answer can converge on the same facts. Two guards, because an Uninstall button that half-succeeds is worse than none: the lockfile path is resolved from the state root while the managed dir is config-overridable, so when they disagree the row reports hub with removable=False and says why; likewise for an entry whose directory was removed by hand. Update stays available in both cases — it re-fetches by identifier into the current managed dir. Hub installs now carry their publisher: a fetched skill has no publisher: block of its own, so the catalog row's provider is recorded in the lockfile and resolved through the same server-side allowlist a manifest goes through. A hub cannot mint brand identity either — an unrecognized provider is kept for diagnostics and renders unbranded. * feat(skills): give every surface one answer about a skill The CLI, the Web UI, and the agent each assembled a skill row by hand and only one of them read the lockfile, so the same install described itself three different ways. skills.list, skills.status, skills.get, `agentos skills list` and the agent's skill_list now all render from build_skill_inventory, and the payload gains publisher, acquisition and availability alongside every key it already had. Browse also stops hiding installs. A catalog is free not to list something the user installed — a GitHub install by URL is in no catalog at all — so skills.search unions the router results with rows synthesized from the lockfile, deduped by identifier and appended after the catalog rows. The installed chip now joins names to names and identifiers to identifiers instead of pooling both into one set, which flagged a catalog row whose name happened to equal an unrelated skill's identifier. * refactor(skills-ui): group the Installed tab on publisher and provenance The Installed tab grouped on `layer`, which is a location — which directory a SKILL.md was loaded from — so one partner's skills split across two headings the moment one of them was installed from a hub. Group on the data instead: Partners (any allowlisted `publisher.id`), then shipped / hub / local from `acquisition.kind`. A skill lands in exactly one group and Partners wins, so Bankr and Robinhood sit under one heading. Delete `isRobinhoodSkill` / `robinhoodSkills`. They inferred a brand from the skill's own name and homepage and leaned on `layer === 'bundled'` to stop a community skill wearing the banner. That guard now lives server-side, where the declared publisher is resolved against an allowlist before it reaches the wire, so the client trusts `publisher.id` and reads neither name nor homepage. Add the availability derivations: a skill can be installed, eligible and still never offered to the agent. An absent block means "not computed" — the CLI never computes one — and must not render as not-offered. `filterRegistry` re-filtered the server's own search results over a narrower matcher than the server used, dropping legitimate hits; the server also matches tags, which are not on the wire, so no client matcher can reproduce it. Pass `serverFiltered` once the rows on screen are the server's answer and only the category chip narrows them. The text pass still runs (now including category) while a request is in flight, where narrowing a stale list is harmless. `mergeRegistryRows` unions two registry lists by key with base winning, so a just-installed row shows on the same tick instead of after the refetch. SkillsPage.tsx gets only the mechanical substitutions needed to keep the build green; its Partners rendering is the next change. Its fixtures now carry the publisher and acquisition blocks the gateway sends, and the layer-heading assertion is updated because it pinned exactly the grouping this changes. * feat(skills-ui): make the Skills page tell one story The Installed tab now groups on provenance instead of the loading layer, so Bankr and Robinhood sit under one Partners heading whether a skill shipped with AgentOS or came from that partner's hub. The layer moves to a per-card detail chip and stays in the dialog, so precedence is still debuggable. Update and Remove come off `acquisition.removable` / `.updatable` rather than `layer === 'managed'`. A custom `skills.managed_dir` used to render an Uninstall button that deletes the lockfile entry and leaves the files; that skill now explains why there is no button instead. A hand-copied directory inside the managed dir loses both buttons, which is what it always deserved. The partner card's hardcoded `bundled` source label becomes the row's real acquisition, since a partner hub install reaches the same tab. Availability is the page's third state: installed, eligible, and still not offered to the agent. The card carries the reason as text next to the dot, and the dialog spells out the gateway's explanation — a user who installed twelve skills can now see why the twelfth is not reaching chat. The Community list stops swapping between the browse snapshot and the live query: it merges, so a row installed while searching survives the search being cleared even before the refetch lands. Three defects in the same code path go with it — the open dialog no longer unmounts when the list underneath it changes, uninstall invalidates the catalog so the Installed chip is not stale, and a failed live search renders as an error rather than "no results". Client text filtering is skipped once the rows on screen are the server's own answer, which the server matches on tags the client never receives. Group headings become real h2s. The test mock returned the same `skills.search` result for every call, which is why the swapping list had no failing test; it is query- and source-aware now. * docs(skills): document publisher, acquisition, and availability The Skills surfaces now report four separate facts about a skill - where its files are, how it was acquired, whose name is on it, and whether the agent is actually being offered it - and the docs described only the first. - src/agentos/skills/bundled/agentos/SKILL.md: layer is no longer THE organizing concept; add acquisition, the allowlisted publisher, and the availability.reason table, and rewrite the "skill missing from prompt" troubleshooting so a budget drop is diagnosable at all. - docs/cli.md: the new publisher/acquisition keys on skills list --json, and why availability is deliberately absent there. - docs/features/skills.md: a section separating the four facts, and a troubleshooting flow keyed on the reason the surface reports. - docs/web-ui.md: the Installed tab's new headings, ready-vs-offered, and the community browse behaviour. - docs/configuration.md: max_skills_prompt_chars, now defaulting to 24000. - CHANGELOG.md: merged into the existing Unreleased headings, with the two user-visible changes called out under a new Changed section. The env feature's promise that setting a variable applies without a restart is now true for the chat path as well; web-ui.md says so explicitly instead of leaving the agent's own view unstated. THIRD_PARTY_NOTICES.md is untouched - the two Robinhood manifests gained a publisher block, not a provenance change. * fix(skills): restrict who may claim a partner's name The publisher allowlist stopped a manifest from *describing* a brand, but not from *selecting* one: any directory dropped into a writable skills path could write `publisher: {id: robinhood}` and land in the Partners group with Robinhood's name and link. Allowlisting the fields only moved the forgery from the fields to the id. So a manifest may now select a publisher for itself only when it ships inside the wheel. Everything reachable from a writable skills path — managed, personal, project, workspace, extra — is operator- or hub-supplied text and gets no brand from its own frontmatter; an installed partner skill such as Bankr is branded by the hub catalog row that installed it, carried in the lockfile, so it keeps its heading. The snapshot cache is re-resolved through the same gate, which also corrects a cache written before it. Also collapse the three disagreeing prompt-budget defaults onto one constant: the turn pipeline still fell back to 8000 when a turn arrived without a skills config, which is the exact value that forced default installs into name-only mode. Two assertions changed because they pinned the behaviour this reverses: `test_a_declared_publisher_wins_over_the_lockfile` asserted a managed skill could override its hub's brand, and the third-party impersonation test placed its skill in the bundled layer, where the guard does not apply. * fix(skills): keep a partner's brand on installs that predate publisher ids Every lockfile written before publisher_id existed carries an empty one, so a Bankr skill already installed on the previous release resolved to no publisher and fell out of the Partners group into "Installed from a hub" — the exact split heading issue use-agent-os#130 set out to remove, on exactly the machines that filed it. _derive_publisher now falls back to entry.source, the same selector install time already falls back to, so an old entry resolves to the same allowlisted record a fresh one does. An unrecognized source still grants no brand. Also: the installer never wrote LockEntry.version, which was invisible until acquisition.version went on the wire; and the truncation docs claimed bundled skills are cut before anything an operator installed, when the cut follows layer precedence and takes `extra` dirs first. * fix(skills): stop the Bankr source installing skills it does not publish A hub install records its publisher from the source it came through, so BankrSource.fetch/inspect delegating to GitHubSource unchecked meant skills.install(identifier="<any github url>", source="bankr") installed arbitrary code and recorded it as published by Bankr — rendering it under Bankr's name and link in the Partners group. Gate both delegating methods on the allowlist this source already declares, so the boundary applies to installs and not only to catalog listings. * fix(skills): answer the budget question without a turn, and stop three lies Four follow-ups from the review of the source-of-truth change: - The prompt-budget verdict was computed only inside a turn, so a row could never carry `prompt_budget` and the Skills page could not deliver the half of the promise that says "or gives a specific reason". The budget gate is not turn-specific — it depends on the gated set and the configured budget, both known when a row is built — so the inventory runs it too. Retrieval stays out: it ranks against one message's wording. Docs corrected to say which reasons a row can carry. - With `[tools] enabled = false` the Skills page answered against the full process registry while chat answered against a turn with no tools, so a tool-gated skill read as available and was then withheld. - A stale lockfile entry whose name collided with a shipped skill made that skill render as hub-acquired, with a source label, a partner's brand, and a Remove button that could not apply. Nothing can install into the packaged bundled directory, so such an entry belongs to a different install. - Update/Remove read `acquisition?.updatable === true`, which is false when the key is absent, so a stale frontend against an older gateway lost both buttons instead of falling back to the layer gate they used before. * fix(skills): stop the skills block arguing against its own contents The guidance above <available_skills> opened with "Skills are optional task playbooks" and told the model to load one "only when a listed entry clearly matches the user's current request". In compact mode — which a stock install always fell into, because full mode never fit the budget — that same block listed nothing but names. A bare name matches nothing clearly, so the instruction was unfollowable and the honest reading of it was "skip it". The two failure modes are not symmetric. Loading a skill that turned out to be unnecessary costs a little context; skipping one that carried the right endpoints, commands, or conventions produces a confidently wrong answer, and a skill also encodes how the user wants the task done here, which is not inferable from the request. Say that, ask for a load on partial relevance, and in compact mode state plainly that a name alone cannot rule a skill out. Full mode grows 16375 -> 16589 chars against a 24000 budget, leaving room for roughly thirteen more skills before compact mode is reached. The truncation test derived its budget from a magic number, so editing this prose changed which case it exercised; it now derives the budget from a real render. A sibling test could pass vacuously once truncation stopped firing, so it now asserts something was dropped. * fix(skills): notice skills that other agents add and remove The skill directories are not exclusively ours. `agentos skills install` runs in its own process, and `~/.agents/skills` is a cross-agent convention that Codex, Cursor, and others write into while an AgentOS gateway is already up. Two separate assumptions meant neither reached a running gateway: - `load_all()` returned its in-memory cache unconditionally, and the cache is only cleared by AgentOS's own install/update/remove paths. A skill written by anyone else sat on disk, absent from `skills.list`, absent from the prompt, with nothing logged. It is now validated against the same file manifest the on-disk snapshot already compares — one stat sweep, measured at 0.6 ms for 65 skills, against a `load_all()` that every caller invokes once per operation. - `~/.agents/skills` and `<workspace>/.agents/skills` resolved once at boot and collapsed to "no such layer" when absent, so the first cross-agent install on a machine stayed invisible until a restart. The managed directory was already exempt for exactly this reason; these two now match it. Naming a directory that does not exist costs nothing — `load_all()` and `_build_manifest()` both skip a missing path. Each new test fails against the previous implementation. * fix(config): lift an existing skills budget of 8000 on upgrade max_skills_prompt_chars is materialised into every saved config.toml, so raising the default would have reached new installs only — and 8000 is exactly the value that cannot fit the shipped skills' descriptions, which is why it was raised. Existing installs would have stayed on a name-only skill list with nothing telling them why, or that a number they never chose was the cause. This joins the config migrations that already run on the next gateway start and take a timestamped backup, so an upgrade is enough; nobody has to know to edit the file. Only the exact old default is rewritten, the same rule the legacy model ids use — a budget someone picked deliberately is left alone, and an unset key is not materialised. One test asserts the point rather than the number: the value it lifts to has to actually fit the shipped set in full mode, or the migration is cosmetic.
The Ollama plain-text example hardcoded model = "qwen2.5:7b" and agent_max_iterations = 8. Copying the snippet could overwrite a user's configured model or point at a model that is not installed locally, and the tool-loop cap is irrelevant while tools are disabled. Model selection is now a separate step (ollama list + agentos configure provider), and the TOML block shows only the settings plain-text mode requires. Co-authored-by: Preciousuche <85787225+Preciousuche@users.noreply.github.com> Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…intext-model-selection docs(config): separate model selection from plain-text mode settings
Cut the 2026.7.27 release across every touchpoint the release consistency and install-script guards check: pyproject.toml, uv.lock, CHANGELOG.md ([Unreleased] moved into a [2026.7.27] section with an empty [Unreleased] reopened), RELEASES.md, README.md install examples and wheel URL, install.sh / install.ps1 defaults and usage text, and the CURRENT_VERSION / CURRENT_RELEASE_TAG constants in tests. No runtime code changes. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
….7.27 chore(release): bump version to 2026.7.27
# Conflicts: # frontend/src/views/skills/SkillsPage.tsx
Standardizes the route title, sidebar item, page heading, browser tab title, and docs on `Agent Setup`. Closes use-agent-os#123
…-os#131) Cmd/Ctrl+Shift+O starts a new chat from anywhere in the console, reusing the same startNewChat flow as the button. The tooltip shows the platform-appropriate hint. Closes use-agent-os#120
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
….7.28 chore(release): bump version to 2026.7.28
The three CI jobs that need the bundled ONNX weights each checked out with `lfs: true`, so every run pulled ~45 MB (bge + MiniLM + pilot) three times. That exhausted the account's Git LFS bandwidth quota and blocked checkout on every job with "This repository exceeded its LFS budget". All three genuinely need the real weights, so the fix is not to drop LFS: `test_pilot_encoder.py` loads the MiniLM export through ONNX Runtime, and `test_build_wheelhouse_zip.py` asserts the built wheel carries a hydrated multi-megabyte ONNX rather than a 130-byte pointer. Instead the jobs now check out without LFS, restore `.git/lfs/objects` from the Actions cache (free, and not billed as LFS bandwidth), and only fall back to a network `git lfs pull` on a cache miss. The cache key is the sha256 of the sorted LFS oids, so it invalidates exactly when the weights change. A hydration check runs after the restore. Pointer files still satisfy the `.is_file()` guards in the pilot encoder tests, so an incomplete restore would not skip those tests — it would fail later with an opaque ONNX parse error. Failing fast at the checkout step keeps that diagnosable. The release workflows keep the direct `lfs: true` + `git lfs pull` path; their hydration asserts are the last line of defense against shipping a pointer inside a published wheel. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…s-objects ci: restore Git LFS objects from the Actions cache
Bankr publishes in two places. The skills in BankrBot/skills are what the Bankr source has been able to read; anyone can also publish from bankr.bot, where a skill lives under its author's wallet address and is served by api.bankr.bot/public/skills/<wallet>/<slug> as JSON with the body inline. There is no repository to clone and no catalog.json, so those skills were invisible to browse and un-installable — adding the slug to the existing allowlist just 404s. Carry them on a second allowlist with their own load path: the SKILL.md is synthesized from the payload (frontmatter from the JSON fields, body verbatim; a body that already ships frontmatter is left alone) and the catalog load fans out to both halves on one client. The request is deliberately unauthenticated — the GitHub token the repo half carries has no business being sent to another host — and the payload is bounded, because it is community-controlled text. The install path is gated the same way as the repository half: bankr.bot serves every published skill from one host, so an ungated source would let `skills.install(source="bankr")` pull any author's skill and record it as having come through Bankr's hub. A wallet-published skill is credited to its author and resolves to no recognized publisher, so it renders unbranded rather than in the Partners group, and author avatars are dropped rather than widening the console's img-src CSP. Ships stock-premium-lp-manager as the first such skill. Fixes use-agent-os#149
# Conflicts: # CHANGELOG.md
…ills feat(skills): install Bankr skills published from bankr.bot
An external contributor opened a PR adding a point-in-time SECURITY_AUDIT.md at the repository root. The existing policy covers where to send vulnerability details but says nothing about audit documents, reward programs, or how researchers get credited, so there was nothing to point at when declining it. Add an "Audit Reports and Scanner Output" section covering all three: route findings through the private advisory form rather than a PR, no bug bounty exists or is planned, and researchers whose reports lead to a fix are credited in that fix's release notes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…udit-report-policy docs(security): reject audit reports as PRs and state no bounty program
Two changes land since v2026.7.28: Bankr skills published from bankr.bot under an author's wallet address are now browsable and installable through the Bankr source (use-agent-os#150), and SECURITY.md states where audit reports belong, that there is no bug bounty program, and how researchers are credited (use-agent-os#154). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
….7.29 chore(release): bump version to 2026.7.29
Preciousuche
pushed a commit
that referenced
this pull request
Aug 11, 2026
Review follow-up on the stale-bundle warning. The detection was right; how it was wired made it unable to change its mind, and it leaked into unrelated tests. The verdict was memoized for the process lifetime. Gateway boot calls it once so caching bought nothing there, but doctor.status is a long-lived RPC: an operator ran the rebuild the FixStep told them to run, re-ran `agentos doctor`, and got the same warning — until they restarted the gateway. The evidence contradicted itself too, because bundleMtime was re-stat'd while stale and sourceMtime came from the cache: doctor #1: stale=True sourceMtime=...300 bundleMtime=...200 doctor use-agent-os#2: stale=True sourceMtime=...300 bundleMtime=...400 Nothing is memoized now, and inspect_control_ui_bundle() returns one report computed in a single pass, so the three fields can no longer disagree. The collector also broke two existing doctor tests. It read the real repo and was not stubbed by _patch_ready_support_surfaces, so a plain `git checkout` — which rewrites frontend source mtimes without touching the gitignored dist/ — leaked a degrades finding into tests asserting exact impact counts. CI only stayed green because ci.yml builds the Control UI immediately before pytest. The helper is stubbed now, and the finding is readiness_impact="optional" rather than "degrades": mtimes are a hint, and a heuristic that git checkout can trip must not turn the whole report yellow. Also: - Moved to health/control_ui.py. The top-level module existed to dodge the import contract, since health has no approved outgoing edge to gateway. Under health/ the evaluator imports it intra-package and the gateway reaches it over the already-approved ("gateway", "health") edge — no dodge, and the mirrored DIST_REL constant is gone because both gateway callers already own _DIST_DIR and now pass the bundle path in. - The doctor collector takes ctx and honours config.control_ui.enabled, which boot already did. No more warning about a bundle nobody serves. - The walk starts at frontend/ instead of a named allowlist, which makes the node_modules/dist prune load-bearing (it pruned nothing before, since the walk roots were frontend/src and frontend/public) and picks up inputs the list had missed: eslint.config.js, components.json, and scripts/check-bundle-budget.mjs, which npm run build invokes. - checkout_root() looks for the checkout markers instead of counting parent levels, so relocating the module cannot silently retarget it above site-packages. - The boot ordering (missing outranks stale) lives in control_ui_boot_warning so it is testable without standing up a gateway; both consumers now have coverage, and a guard test keeps boot.py going through the helper. - CHANGELOG blank line, `key != "stale"`, and the redundant lambda. Gate: build_control_ui, npm check (1738), ruff, mypy (592), pytest (7349), uv build --wheel.
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
Prevent Web UI skill cards from ignoring their CSS grid track and overlapping adjacent cards when skill names are long.
min-width: 0to.sk-cardand.sk-card__headflex/grid containers inskills.css.min-width: 0,overflow: hidden,text-overflow: ellipsis, andwhite-space: nowrapon.sk-card__name.skills-css.test.ts.Closes use-agent-os#135