Skip to content

Add packet block + unified project-root path resolution#105

Merged
mlund01 merged 6 commits into
mainfrom
claude/angry-hoover-353dea
Jun 6, 2026
Merged

Add packet block + unified project-root path resolution#105
mlund01 merged 6 commits into
mainfrom
claude/angry-hoover-353dea

Conversation

@mlund01
Copy link
Copy Markdown
Owner

@mlund01 mlund01 commented Jun 5, 2026

Summary

  • New top-level packet "name" { path = ..., description = ... } HCL block — read-only reference data bundles attached to missions. Agents address them as slot = "packet.<name>" on the existing file tools.
  • Single shared path-resolution rule (paths.ResolveConfigPath) used by packet.path, plugin.source (local sources), and load(). CWD is no longer consulted anywhere in HCL config.
  • @/foo anchors to the project root (the -c argument); ./foo / ../foo / bare foo anchor to the HCL file's own directory; absolute paths and .. escapes outside the project root are rejected.

Why

Packets fill a gap between memory (writable, Squadron-managed paths) and "I have a folder of reference docs I want agents to read." Memory is for state agents produce; packets are for material agents consume.

The path-resolution change came out of testing the new feature — relying on CWD for plugin.source and load() meant squadron -c <project> mission foo would silently break depending on where the user cd'd before invoking it. Now resolution is deterministic from the -c argument alone, and the same rule applies everywhere.

The block is named packet rather than the original "context" because "packet" is more pointed: it's a discrete, named bundle of read-only reference data, distinct from the many overloaded senses of "context" already in the codebase (HCL EvalContext, Go context.Context, "vars context," "the context an agent operates within," etc.).

Highlights

packet block

  • Read-only enforced at the tool layer: file_create / file_delete rejected, file_read rejects binary content, file_grep skips binaries.
  • .hcl files inside a packet path are excluded from config parsing — the folder is opaque reference data, not config. Per-file parse errors inside packets are silently suppressed.
  • Mission and task packets = [packets.X] for declaration (task-level is declarative, not isolating — every task in a mission shares the same MemoryStore).

Path resolution rule (paths.ResolveConfigPath)

Form Resolves to
./foo, ../foo, bare foo HCL file's own directory
@/foo Project root (the -c arg)
/foo, absolute rejected
.. escaping project root rejected (post-Clean containment)
Path resolving to project root itself (e.g. @/) rejected

load() has no per-callsite "HCL file dir" (cty functions don't get caller context), so relative forms anchor to project root for it specifically. The squadron plugin build <name> <path> CLI subcommand still uses ResolveProjectPath (CWD-based) — that's correct for a shell-typed path; ResolveProjectPath is marked Deprecated for HCL-attribute use.

Ordering matters

Packet blocks are parsed in a new Stage 1.4, between variables and storage. The HCL-exclusion filter must run before vault / storage / command_center / mcp_host iterate allParsedBlocks — otherwise an inadvertent variable "foo" { ... } block sitting inside a packet folder would leak into the config. This was caught in an earlier code-review pass (finding C1) and addressed in this PR.

Tests

  • 6 new packet tests (config-load, mission/task references, @/ marker, .. escape, root-itself rejection, sub-HCL anchoring).
  • 4 new aitools tests for packet-slot tool policy (binary rejection, read-only, IsPacketSlot).
  • 4 new memory_store_packets tests.
  • 4 plugin tests rewritten for the loader-resolves-before-validate pattern.
  • 4 load() tests rewritten for the no-CWD contract.
  • Full go test ./... clean across all 18 packages.

Live-tested against an external testground from an unrelated CWD: packet_smoke, memory_smoke, shell_smoke, pinger_smoke_go, pinger_smoke_py all pass; all six rejection paths (3 for packet.path, 2 for plugin.source, 1 for @/ root-itself) surface clear errors.

Docs

  • New docs/content/missions/packets.mdx with the path-resolution table, failure-mode table, and a full example showing both @/ and ./ forms.
  • docs/content/config/plugins.mdx rewritten Path Resolution subsection; old "current working directory" line removed.
  • docs/content/config/functions.mdx load() section rewritten — bare-name no longer "relative to working directory."
  • CLAUDE.md adds Stage 1.4 to the staged-evaluation list, a new Packets section, and a new Path resolution for config attributes section so the codebase doc reads the same as the user docs.

Test plan

  • go test ./... clean (verified locally)
  • squadron verify <project> succeeds from an unrelated CWD
  • At least one mission run with a packet "x" { path = ... } block that uses the file tools
  • Try rejection paths: path = "/abs", path = "../../escaped", path = "@/"

mlund01 added 2 commits June 4, 2026 21:43
Introduces a read-only `context "name" { path = ... }` HCL block for
attaching reference data bundles to missions, and unifies path resolution
for every config attribute that takes a path (context.path, plugin.source
local sources, load()) behind a single helper that anchors to the
project root rather than the process working directory.

## context block

- Top-level `context "name" { path = ..., description = ... }`.
- Mission and task opt in via `contexts = [contexts.X]`.
- Agents address contexts as `slot = "context.<name>"` on every file tool.
- `file_create` / `file_delete` rejected as read-only.
- `file_read` rejects binary content (NUL byte in first 8 KB).
- `file_grep` silently skips binary files.
- `.hcl` files inside a context path are excluded from config parsing —
  the context folder is opaque reference data, not config.

Loaded in a new Stage 1.4 (between vars and storage) so the HCL-exclusion
filter runs before vault / storage / command_center / mcp_host iterate
allParsedBlocks. Per-file parse errors are deferred and only surfaced
when they don't belong to a context path.

## Path resolution

`paths.ResolveConfigPath(projectRoot, hclFileDir, rawPath)` is the new
single source of truth, used by context.path, plugin.source, and load():

  ./foo, ../foo, bare foo  → HCL file's own directory
  @/foo                    → project root (the -c argument)
  /foo, absolute           → rejected
  .. escaping project root → rejected (post-Clean containment check)
  Path equal to project root itself (e.g. @/) → rejected

The process working directory is never consulted, so `squadron -c <dir>`
behaves identically regardless of where it was invoked from.

`load()` has no per-callsite "HCL file dir", so relative forms collapse
to the project root for it specifically.

The `squadron plugin build` CLI subcommand still uses ResolveProjectPath
(CWD-based) — that's correct for a shell-typed path. ResolveProjectPath
is marked Deprecated for HCL-attribute use.

## Tests

Six new context-specific tests, four rewritten plugin tests, four
rewritten load() tests. Full `go test ./...` is green across 18 packages.
Live-verified against an external testground from an unrelated CWD:
context_smoke, memory_smoke, shell_smoke, pinger_smoke_go,
pinger_smoke_py all pass, and the four rejection paths surface clear
errors.

## Docs

contexts.mdx, plugins.mdx (Path Resolution section), functions.mdx
(load() rules), and CLAUDE.md (staged-evaluation list, Contexts section,
Path resolution for config attributes section) all updated with the
unified rule, failure-mode tables, and cross-links so the three surfaces
read consistently.
The new HCL block, namespace, slot prefix, Go types, files, tests,
docs, and configuration attribute names all rename from "context" to
"packet." "Packet" is more pointed: it's a discrete, named bundle of
read-only reference data, distinct from the many overloaded senses of
"context" (HCL EvalContext, Go context.Context, "vars context," "agent
context" the LLM operates within, etc).

User-visible surface:

  packet "name" { path = ..., description = ... }    # HCL block
  packets = [packets.x]                              # mission/task attr
  slot = "packet.<name>"                             # tool slot parameter

Internal:

  config.Packet  (was config.Context)
  config.PacketSlotPrefix = "packet."  (was ContextSlotPrefix = "context.")
  aitools.IsPacketSlot                 (was IsContextSlot)
  Mission.Packets, Task.Packets        (was .Contexts)
  Config.Packets                       (was .Contexts)

Files:
  config/packet.go               (was context.go)
  config/packet_test.go          (was context_test.go)
  aitools/memory_tools_packet_test.go  (was _context_test.go)
  mission/memory_store_packets_test.go (was _contexts_test.go)
  docs/content/missions/packets.mdx    (was contexts.mdx)

Carefully NOT renamed:
  hcl.EvalContext, ctx context.Context, buildVarsContext etc — these
  are HCL evaluation contexts and the Go runtime context type, unrelated
  to our user-facing block.

All tests pass across 18 packages. The packet_smoke mission was rebuilt
against the new terminology and verified end-to-end on the testground.
@mlund01 mlund01 changed the title Add context block + unified project-root path resolution Add packet block + unified project-root path resolution Jun 5, 2026
mlund01 added 4 commits June 5, 2026 20:46
Smoke run on the renamed feature surfaced five spots the bulk rename
missed because they hide inside string literals and prose:

- aitools/memory_tools.go: error strings for file_read binary rejection
  and file_create/delete read-only rejection still said "context slots"
  and "context bundles"; doc comments on PacketSlotPrefix/IsPacketSlot
  still referenced "context bundle".
- agent/internal/prompts/prompts.go: the slot label in the agent system
  prompt still said "(context bundle — read-only reference data ...)".
- internal/paths/paths.go: ResolveConfigPath docstring still listed
  "context.path" in the example callers, and the rel=="." rejection
  comment still said "for context blocks specifically".
- config/packet_test.go + mission/memory_store_packets_test.go: stray
  prose in test descriptions/comments.

Verified by re-running packet_smoke from /tmp against the testground:
the agent's captured binary-rejection error now reads 'packet slots
accept UTF-8 text only' and the read-only error reads 'packet bundles
are immutable'.
Three spots the wholesale rename and the leftover-strings patch both
missed:

- aitools/memory_tools.go: the doc comment above the binary-content
  rejection said "Contexts are read-only reference data..." and the
  file_grep local variable was still named isContext.
- config/packet_test.go: a leftover code comment referenced the old
  helper name ResolveContextPath.

Genuine non-feature 'context' references remain on purpose: Go runtime
context.Context, HCL EvalContext, and generic prose like 'mission
context' (cancellation), 'item context' (iteration), 'agents context'
(HCL eval), 'context pruning' (commander feature). None of those are
the packet block.
Closes the bugs and refactor items the high-effort review flagged. The
ones the user explicitly skipped (#2 load() bare-name semantic change,
#3 load() .. now rejected, #6 inconsistent IsPacketSlot ordering, #8
JSON tag inconsistency) are left as-is.

#1 — Packets-inside-packets are now dropped from allPackets

Old: the Stage 1.4 packet loop ran over EVERY parsed file before the
HCL-exclusion filter knew about packet roots, so a `packet "inner"`
block declared in a .hcl file living inside another packet's path
ended up registered alongside the legitimate packets. Other block
types in the same file were correctly filtered later, but packets
weren't.

New: track each decoded packet with its source file, then drop any
packet whose source file lies inside ANOTHER packet's path. Test:
`packet "outer"` at top-level with `outer/inner.hcl` declaring `packet
"inner"` — only outer survives.

#4 — configDir is now the absolute -c argument, not Dir(files[0])

Discovered while writing the V1 test: filepath.WalkDir's lexical order
can surface a subdirectory file before its parent, so
`filepath.Dir(files[0])` could pick a child dir as configDir, breaking
`@/foo` resolution.

Fix: LoadDir/LoadFile absolutize the original input path and pass it
through to loadFromFiles as an explicit configDir parameter. The
CWD-independence promise now holds even when -c is `.` or `./relpath`.

#5 — wsbridge file browser now exposes packet slots

Old: collectMemoryInfos and resolveMemoryPath only walked cfg.Memories
and cfg.Missions[i].Memory. cfg.Packets was never consulted, so the
command-center file-browser sidebar showed zero packets and any
attempt to browse `packet.<name>` returned "memory not found".

New: both helpers iterate cfg.Packets. Each packet surfaces with
Editable=false so any future write affordance the UI builds stays
disabled. dedupStrings() keeps the Missions list clean when a packet
is referenced by both a mission and one of its tasks.

#7 — file_read peeks at 8KB before allocating the full file

Old: file_read on a packet slot called io.ReadAll for the whole file
(capped at 10MB), then ran looksBinary on the full content. A 9MB
binary in a packet allocated 9MB to be rejected.

New: for packet slots specifically, io.ReadFull a 8KB head, run
looksBinary, and only then proceed to the full read. Same pattern
file_grep already used.

#9 — missionSnapshot/taskSnapshot now include slot references

Old: neither snapshot helper emitted memories, packets, memory, or
scratchpad keys, so the persisted ConfigJSON had no record that a
mission or task referenced specific data bundles.

New: missionSnapshot emits memories, packets, memory (description
only — the path is runtime-derived), and scratchpad. taskSnapshot
emits packets. Audit views and dashboards can now show what data each
mission/task had access to without re-parsing HCL.

#10 — Centralized containment via paths.IsInside

Old: three independent containment implementations — ResolveConfigPath,
ResolveProjectPath, and the new isInsidePacket — with subtly different
forms. The packet filter used the loose `HasPrefix(rel, "..")` form
that false-positives on a filename literally starting with `..`
(e.g. `..keep`).

New: `paths.IsInside(root, p string, requireStrictDescendant bool)
bool` is the single source of truth. Strict-descendant mode rejects
p == root. All three callers route through it; ResolveConfigPath
keeps the helpful error message that distinguishes "is the root
itself" from "outside the root".
…ule to packets

Upstream f2cde48 added centralized block-label validation (lowercase
letters, digits, underscores; can't start with a digit) and removed the
last bits of `shared_folder` deprecation scaffolding from the parse
loop. Conflicts in config/config.go were both clean: keep `packet` from
this branch, drop `shared_folder` per main.

Additionally extend validateBlockNames to cover `packet` and add a
regression spec to blockname_test.go so the rule is enforced uniformly
for the new block type.
@mlund01 mlund01 merged commit 4f42338 into main Jun 6, 2026
3 checks passed
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