Skip to content

Let agents opt out of the app default layer; fix PLUGIN_DIRS collapsing to one dir - #59

Open
simurg79 wants to merge 2 commits into
affandar:mainfrom
simurg79:feat/inherit-app-defaults-pr
Open

Let agents opt out of the app default layer; fix PLUGIN_DIRS collapsing to one dir#59
simurg79 wants to merge 2 commits into
affandar:mainfrom
simurg79:feat/inherit-app-defaults-pr

Conversation

@simurg79

Copy link
Copy Markdown
Collaborator

What

Two changes that together let an app author an agent which does not inherit the app's default.agent.md layer.

1. inheritAppDefaults frontmatter (feature)

An app's default.agent.md is force-merged into every session — both its prompt and its tools:. The only escape hatch was promptLayering.kind === "pilotswarm-system-agent", which app agents cannot reach. So the only way to author an agent that did not want those instructions was to weaken the default for everyone.

Agents can now opt out:

---
name: my-agent
inheritAppDefaults: false
---

An opted-out agent composes from the framework base plus its own prompt and tools only.

  • Default is inherit, so every existing agent is byte-identical.
  • Resolved from the agent definition at prompt-composition time, not persisted on the session — redefining an agent takes effect on its next turn, and no session migration is needed.
  • Inert for management agents, which already bypass app overlays.
  • Deliberately instructions-only. feat/capability-profiles-phase1 covers the skills/tools half (allowedSkills, toolPolicy); this stays orthogonal to it.

This is a composition choice, not a security boundary — invariants that must hold for every session still belong in the framework base layer, which cannot be opted out of.

2. PLUGIN_DIRS no longer collapses to one directory (bug fix)

PLUGIN_DIRS has always been documented and parsed as a comma-separated list, and the SDK worker loads every entry — but bootstrap-env.js resolved it down to dirs[0] and then wrote that single path back over process.env.PLUGIN_DIRS, destroying the config before embedded workers read it.

Any deployment configuring two plugin dirs silently lost all but the first, with no error. For an overlay holding default.agent.md, that means the app default layer vanishes silently.

  • --plugin now also accepts a comma-separated list.
  • system.md is resolved across all plugin dirs (first match wins).
  • Branding still comes from the first dir only, so single-dir setups are unchanged.

Same one-line collapse existed in packages/app/web/bin/serve.js and is fixed there too.

Why together

The second is a prerequisite for using the first the obvious way: putting the app default in its own plugin dir so opting out is visibly opting out of a named layer rather than out of something buried among the ordinary agents.

Tests

  • packages/sdk/test/unit/agent-inherit-app-defaults.test.mjs — 11 new tests
  • packages/app/tui/test/bootstrap-env-plugin-dirs.test.mjs — 6 new tests
  • SDK unit suite 97/97, tsc --noEmit clean, TUI plugin-dir tests 6/6, all on top of 0.5.28

Note for the maintainer

No version bump or CHANGELOG heading is claimed — both entries are filed under ## Unreleased. (An earlier local cut of this work claimed 0.5.27, which was taken upstream in the meantime; that commit was dropped.)

Bertan Ari and others added 2 commits July 28, 2026 10:49
An app's `default.agent.md` is force-merged into every session -- both its
prompt and its `tools:`. The only escape hatch was the internal
`pilotswarm-system-agent` prompt-layer kind, which app agents cannot reach, so
authoring one agent that did not want the app-wide instructions meant weakening
the default for every other agent in the deployment.

Agents can now declare `inheritAppDefaults: false`. Such an agent is composed
from the framework base plus its own prompt and tools only, and it is dropped
from the prompt layer manifest. Omitting the field inherits, so every existing
agent is unchanged.

The flag is resolved from the bound agent definition at prompt-composition time
rather than persisted on the session row, matching how agent redefinition is
expected to take effect on the next turn -- so no migration is needed and no
session can carry a stale copy of the decision. A single `inheritsAppDefaults()`
resolver gates all four consumption points (session prompt, subagent prompt,
app-default tool merge, layer manifest) so they cannot drift apart.

This is a composition choice, not a security boundary: invariants that must hold
for every session still belong in the framework base layer, which cannot be
opted out of.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
PLUGIN_DIRS is documented and parsed as a comma-separated list, and the SDK
worker loads every entry (worker.ts Tier 4 iterates config.pluginDirs). The
TUI/portal bootstrap did not: resolvePluginDir() returned dirs[0] and then
overwrote process.env.PLUGIN_DIRS with that single path, so every dir after
the first was discarded before the worker ever saw it.

The failure is silent and the blast radius is bad: an overlay dir holding
default.agent.md would simply not load, dropping the app default prompt and
its force-merged tools with no warning.

resolvePluginDirs() now returns the full list, --plugin accepts a
comma-separated value, and system.md is resolved across all dirs (first match
wins). Branding still reads only the first dir, so single-dir configurations
resolve to exactly the same values as before.

The same truncation in web/bin/serve.js --plugin handling is fixed too.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant