-
Notifications
You must be signed in to change notification settings - Fork 0
extending stacks
Status: proposed.
extends:is not implemented yet. This guide is the spec: it fixes the merge semantics, the hard limits, and the security rules so the implementation (and any stack authored against it) has one source of truth. Nothing below works today.
A stack (catalog/stacks/<name>/stack.yaml) composes recipes, services, and a handful of
policy fields (permissions, instructions, state, credential forwarding). Once a few stacks
share the same base — the same house recipes, the same permission mode, the same identity text —
copy-paste starts to rot: you fix a base recipe list in one stack and forget the other four.
extends: names a base stack; the child is the base merged with its own manifest.
# catalog/stacks/team-base/stack.yaml
name: team-base
recipes: [repowise, gsd-core]
services: [ping]
permissions: auto
instructions: |
You are a team agent working in a shared repo.
# catalog/stacks/team-web/stack.yaml
name: team-web
extends: team-base
recipes: [playwright] # → repowise, gsd-core, playwright
services: [] # → ping (base's services still apply)
permissions: yolo # → yolo (child wins)team-web resolves to recipes: [repowise, gsd-core, playwright], services: [ping],
permissions: yolo, and the base's instructions.
-
extends:is a single stack name, not a list. No multiple inheritance, no diamond merge. - The base is resolved by name across the catalog roots, exactly like a stack on the command line:
user overlay (
~/.config/harnessed/catalog) first, then the repo catalog. This is the point of the feature — a stack in your overlay extends a base the repo ships. -
Chains are allowed (
aextendsbextendsc), merged base-first. Cycles are a hard error, named in the message. - A repo-catalog stack may not extend a user-overlay stack. Shared content cannot depend on private content that only exists on one machine.
-
name:is never inherited. It must still equal the stack's directory name.
Recipe varieties (beads/stealth, beads/team — one family dir, path refs) are the same tool
wired differently, and harnessed already treats two varieties of one family as mutually exclusive
within a stack. Inheritance would force a third rule on top of that: does a child's beads/team
replace the base's beads/stealth, or collide with it? Both answers are surprising, and both
turn the recipes list into something you have to mentally execute rather than read.
So: if a stack's recipes: names any variety (<family>/<variety>), that stack cannot be used
as an extends: target.
# catalog/stacks/team-base/stack.yaml
name: team-base
recipes: [repowise, beads/stealth] # ← declares a variety
# catalog/stacks/team-web/stack.yaml
name: team-web
extends: team-base # ERROR: stack 'team-base' declares a variety recipe
recipes: [playwright] # (beads/stealth) and cannot be extendedThe error is raised at load, against the base, and points at the fix: pull the varietied recipe out of the base and let each leaf stack name the variety it wants, or fork the base outright.
A child may name a variety freely — it just makes that child unextendable in turn. The
restriction only ever applies to being the target of an extends:.
| Field | Rule |
|---|---|
name |
Never inherited. Must match the stack's directory name. |
recipes |
Base's list first (in base order), then the child's. Duplicates collapse to their first occurrence. |
services |
Same as recipes. |
permissions |
Child wins when set; otherwise the base's. |
instructions |
Child replaces the base's text when set (no concatenation — two identity blocks read as a contradiction). |
hatago |
Child wins when set (whole block, not per-key). |
state |
Shallow per-key merge; a key the child sets wins. |
forward_git_credentials, forward_aws_sso
|
Inherited; the child may set either back to false. See the security note below. |
ssh_keys |
Never inherited. See below. |
There is no way to remove a recipe or service the base declares. If you need the base minus something, the base is wrong — split it.
Two of harnessed's rules exist because a stack manifest can come from a shared catalog that you did not write:
-
ssh_keysis not inherited, ever. Mounting a private key is the key owner's decision, not a base-stack author's. Today the launcher already dropsssh_keysfrom any stack outside your user overlay; inheritance would launder that check — your overlay stack extends a repo base, and the base'sssh_keyswould arrive looking like they came from you. Declaressh_keysin the stack you actually launch, in your own overlay. -
forward_git_credentials/forward_aws_ssoare inherited, but harnessed tells you when the value came from a stack outside your overlay — these forward secret-bearing material (the gh oauth token, STS creds), so "a base you didn't write turned this on" must be visible, not inferred. Set either tofalsein the child to opt out.
Everything else in the table is configuration, not credentials, and inherits without ceremony.
A built profile is a pure function of its catalog inputs, and harnessed hashes those inputs into a
staleness stamp. Under extends:, the inputs of a child stack are every stack.yaml in its
chain plus every recipe dir the merged list references. Edit team-base, and team-web reports
stale on the next harnessed list / launch and rebuilds — the same as if you had edited
team-web's own manifest.
- Composing stacks — the stack manifest itself, and the build/run lifecycle.
-
Recipe authoring — recipes and recipe varieties (
<family>/<variety>). -
src/harnessed/schema.py— the typedStackmodel.
Start Here
Guides
- Recipe authoring
- Service authoring
- Stacks
- Extending stacks (proposed)
- Recipe catalog
- System prompt & rules (proposed)
- Secrets
- AWS SSO
- Pulumi (host login forwarding)
- Egress & exposing services
- Container filesystem
- Git hooks
- Troubleshooting
- Pin management (harnessed update)
Codebase Map
Planning & Roadmap
- open work: GitHub Issues
Research & Prompts
- research/ (home-folder requirements per harness, browse in-repo)
- prompts/ (reusable prompt templates, browse in-repo)