Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ scratchpad/
__pycache__/
*.pyc
.env
config/
/config
12 changes: 8 additions & 4 deletions bin/fm-startup-memory-budget-lib.sh
Original file line number Diff line number Diff line change
Expand Up @@ -29,12 +29,16 @@ fm_startup_memory_budget_link_count() {
fi
}

# fm_startup_memory_budget_config_dir_safe <dir>
# The directory only has to resolve to a real directory. A symlinked config/ is
# a legitimate deployment layout - a home whose config/ is linked into a
# machine-configuration repo - and every other config consumer already resolves
# through it. Rejecting it here bought no integrity property either, because
# the test could only ever see the final path component: a symlinked ancestor
# passed silently. The per-file symlink and hardlink checks below are what
# actually stop this scalar from being aliased or shared.
fm_startup_memory_budget_config_dir_safe() {
local dir=$1
if [ -L "$dir" ]; then
fm_startup_memory_budget_fail "config directory is symlinked"
return 1
fi
if [ ! -d "$dir" ]; then
fm_startup_memory_budget_fail "config directory is not a directory"
return 1
Expand Down
5 changes: 3 additions & 2 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -161,8 +161,9 @@ There is no shared learnings file by captain decision.
The locked mutable bootstrap path materializes its visible default of `7500` estimated tokens in a primary home when the file is absent.
To select another allowance, replace the primary home's file with one valid positive value in the exact format below; the next locked bootstrap convergence or `bin/fm-config-push.sh` propagates it to registered secondmates.
A secondmate does not create an independent default and instead receives the primary value through the inherited-local-material contract in [`secondmate-provisioning`](../.agents/skills/secondmate-provisioning/SKILL.md).
The file must be one positive base-10 integer followed by exactly one newline in a regular, single-linked file beneath a non-symlinked `config/` directory.
Malformed, multi-line, symlinked, hardlinked, special, or otherwise unsafe values are rejected rather than treated as a default.
The file must be one positive base-10 integer followed by exactly one newline in a regular, single-linked file beneath a `config/` directory that resolves to a real directory.
A `config/` that is itself a symlink into a machine-configuration repo is a supported layout and is resolved through, exactly as every other setting on this page already is.
Malformed or multi-line values, a symlinked, hardlinked, or special budget file, and anything else unsafe are rejected rather than treated as a default.
Use `bin/fm-startup-memory-budget.sh read` to validate and print the effective value, or `bin/fm-startup-memory-budget.sh report` to account for the three files.
The stable local estimate is `ceil(UTF-8 bytes / 3)` per file, a conservative portable approximation rather than a provider-exact tokenizer.
An inherited `data/captain-shared.md` counts in a secondmate's total but remains primary-owned and read-only there.
Expand Down
32 changes: 29 additions & 3 deletions tests/fm-startup-memory-budget.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ run_bootstrap() {
}

test_primary_bootstrap_materializes_visible_default() {
local rec root home fakebin out second
local rec root home fakebin out second linked linked_config
rec=$(new_bootstrap_world materialize)
root=${rec%%|*}
home=${rec#*|}
Expand All @@ -111,6 +111,19 @@ test_primary_bootstrap_materializes_visible_default() {
[ "$(<"$home/config/startup-memory-budget")" = 321 ] \
|| fail "bootstrap replaced a valid captain-selected budget"

linked="$TMP_ROOT/materialize/linked-home"
linked_config="$TMP_ROOT/materialize/linked-config"
mkdir -p "$linked/data" "$linked/state" "$linked_config"
ln -s "$linked_config" "$linked/config"
out=$(run_bootstrap "$root" "$linked" "$fakebin")
case "$out" in
*STARTUP_MEMORY_BUDGET:*) fail "bootstrap rejected a symlinked config directory: $out" ;;
esac
[ "$(<"$linked_config/startup-memory-budget")" = 7500 ] \
|| fail "bootstrap did not materialize the default through a symlinked config directory"
[ "$(FM_HOME="$linked" "$BUDGET" read)" = 7500 ] \
|| fail "read command did not expose the default published through a symlinked config directory"

second="$TMP_ROOT/materialize/secondmate"
mkdir -p "$second/config" "$second/data" "$second/state"
printf '%s\n' sm > "$second/.fm-secondmate-home"
Expand Down Expand Up @@ -158,10 +171,23 @@ test_safe_parser_rejects_ambiguous_and_unsafe_values() {

rm -f "$home/config/startup-memory-budget"
rm -rf "$home/config"
ln -s "$TMP_ROOT/parser-config-target" "$home/config"
mkdir -p "$TMP_ROOT/parser-config-target"
printf '88\n' > "$TMP_ROOT/parser-config-target/startup-memory-budget"
expect_rejected_read "$home" 'config directory is symlinked'
ln -s "$TMP_ROOT/parser-config-target" "$home/config"
[ "$(FM_HOME="$home" "$BUDGET" read)" = 88 ] || fail "a symlinked config directory was not resolved through"

rm -f "$home/config/startup-memory-budget"
ln -s "$outside" "$TMP_ROOT/parser-config-target/startup-memory-budget"
expect_rejected_read "$home" 'file is symlinked'

rm -f "$home/config"
ln -s "$TMP_ROOT/parser-config-missing" "$home/config"
expect_rejected_read "$home" 'config directory is not a directory'

rm -f "$home/config"
printf '99\n' > "$TMP_ROOT/parser-config-file"
ln -s "$TMP_ROOT/parser-config-file" "$home/config"
expect_rejected_read "$home" 'config directory is not a directory'
pass "budget parser accepts one exact positive value and rejects malformed or unsafe inputs"
}

Expand Down
Loading