feat(core): implement per-entry-type extension policies (#491) - #563
Danielobito009 wants to merge 827 commits into
Conversation
…FIXED (TegoLabs#270) * TegoLabs#145 feat(core): integrate HashiCorp Vault for key retrieval FIXED * chore(tests): split mock secrets to evade GitGuardian false positives * chore(tests): split more mock secrets to evade GitGuardian --------- Co-authored-by: AbdulmalikAlayande <114596864+AbdulmalikAlayande@users.noreply.github.com>
…FIXED (TegoLabs#270) * TegoLabs#145 feat(core): integrate HashiCorp Vault for key retrieval FIXED * chore(tests): split mock secrets to evade GitGuardian false positives * chore(tests): split more mock secrets to evade GitGuardian --------- Co-authored-by: AbdulmalikAlayande <114596864+AbdulmalikAlayande@users.noreply.github.com>
- Add docker-compose.devnet.yaml: Quickstart testing image, --limits unlimited, 30s polling cadence, debug logging, isolated named volumes - Enhance docker-compose.yaml: restart policies, JSON log rotation, parameterised ports, LOG_LEVEL/NODE_ENV env vars - Add .env.example: full environment variable reference with inline comments - Add tests/docker/devnet-compose.test.ts: 32 TDD assertions covering file presence, service config, volume isolation, network sharing, .env.example, and compose merge compatibility - Update .dockerignore: exclude compose files, systemd/, docs/, templates/ - Update .gitignore: allow .env.example via negation rule Acceptance criteria met: docker compose -f docker-compose.yaml -f docker-compose.devnet.yaml up boots daemon and mock RPC environment successfully. All 530 tests pass, 63 docker-specific tests, 5 skipped TODOs.
…des and acceptance criteria
…estimates - Add countExtensionsInLastHour() to repositories.ts to query extension_history for the past 60-minute window (issue TegoLabs#142) - Export HOURLY_RATE_LIMIT = 5 constant from extension.ts (issue TegoLabs#142) - Export isRateLimited() that gates on countExtensionsInLastHour >= limit (issue TegoLabs#142) - Enforce rate limit in runAutoExtensions(): skip + log when limit reached (issue TegoLabs#142) - Export ResourceEstimate interface and parseResourceEstimate() in rpc/client.ts to extract cpuInstructions, memoryBytes, minResourceFee from simulation responses (issue TegoLabs#133) - Add comprehensive TDD tests written before implementation: - tests/db/rate_limiter.test.ts: countExtensionsInLastHour edge cases - tests/core/rate_limiter.test.ts: isRateLimited, runAutoExtensions integration - tests/rpc/resource_estimate.test.ts: parseResourceEstimate + failure edge cases Closes TegoLabs#133 Closes TegoLabs#137 Closes TegoLabs#142
Central registration point for alert channel plugins. A contributor adding a new channel calls registerAlertChannel() with a ChannelDefinition instead of editing dispatcher.ts's channel map, the CLI's --type if/else chain, and a DB CHECK constraint.
Preserves existing behavior exactly: same target flags, same missing- target error text, same lazy dynamic import for discord/telegram, same webhook-only HMAC signing. This is the reference implementation new channel plugins should follow.
Replaces the hardcoded DEFAULT_CHANNELS object with a registry-backed lookup, so a plugin channel registered anywhere becomes deliverable without editing this file. Explicit channels overrides (used throughout the test suite) are unaffected — only the default when one is omitted changed source. deliverSingleAlert's channelType is widened from a fixed union to string for the same reason.
channel_type validity is now enforced by the alert channel registry at the application layer instead of a fixed SQL enum, so adding a channel no longer requires a schema change. The CHECK now only guards against an empty string.
…ration The SCHEMA comment-stripper (`--.*\n`) silently failed to match comments ending in \r\n, since JS's `.` excludes all line terminators including \r. On a CRLF checkout, an unstripped comment survives into the whitespace-collapsed script, and SQLite's own -- comment then runs to the string's end, swallowing every statement after it with no thrown error. Switched to `--[^\n]*\n`, which matches either line ending. Latent since schema.sql had no comments before this change. Also adds relaxChannelTypeChecks(), following the existing migrateAlertConfigsChannelTypeCheck() convention, to rebuild alert_configs and resource_alert_configs in place for databases created before the CHECK was relaxed.
AlertConfig, UndeliveredAlert, ResourceAlertConfig, and UndeliveredResourceAlerts previously hardcoded the built-in channel names in their type signatures. The registry is now the source of truth for valid channel names, so these widen to string.
The beforeEach block manually rebuilt alert_configs with a hardcoded 5-name CHECK on every test, a leftover workaround from before schema.sql had these columns natively. It silently undid the CHECK relaxation, since it ran unconditionally rather than detecting whether schema.sql already had the change. getDatabaseForTesting() already execs the current schema.sql into a fresh database, so the whole block was redundant even before this. Also adds coverage for plugin channel_type values and empty-string rejection on both alert_configs and resource_alert_configs.
Replaces the per-channel if/else chain with a lookup against the alert channel registry, so a plugin channel's --type, target flag, missing-target error, and signing behavior all come from its ChannelDefinition instead of a hardcoded branch in this file. All existing error message text is preserved exactly for the five built-in channels; the generic "unknown type" message is now built from whatever channels are actually registered.
- Add entry_type_policies table via migration (contract_id, entry_type PK with CHECK and CASCADE FK) - Add getEffectivePolicy() resolving type override then contract default then null - Add setEntryTypePolicy() with UPSERT semantics - Add deleteEntryTypePolicy() to remove type overrides - Update runAutoExtensions() to call getEffectivePolicy() per entry using entry.entryType - Keep isRateLimited() per-contract (not per-policy-row) so one type cannot starve another's rate limit budget - Add --entry-type flag to guard command (instance|wasm|persistent|temporary) - Write failing tests first (TDD), then implement - Tests: override beats default, fallback works, rate limit per-contract, guard flag routing Closes TegoLabs#491
|
@Danielobito009 Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
📝 WalkthroughSummary by CodeRabbit
WalkthroughPer-entry-type TTL policies are stored as contract-scoped overrides, configured through ChangesEntry-type extension policies
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related PRs
Suggested reviewers: Poem
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/core/extension.ts (1)
398-432: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy liftPartition extensions by resolved target TTL.
entryKeyscan include policies with different targets, but both RPC calls receive onlyfirstTarget. For example, instance=2000 and wasm=3000 submits both keys at 2000. Group entries by target and simulate/submit each group separately; enforce the contract-wide hourly limit per submitted group so one run cannot exceed its remaining transaction slots. The multi-type test currently masks this because it mocks returned TTLs instead of asserting RPC arguments.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/core/extension.ts` around lines 398 - 432, Update the extension flow around needsExtension, entryKeys, simulateExtension, and extendEntries to group entries by each entry’s resolved target TTL instead of using firstTarget for all entries. For every target group, simulate and submit only that group with its target, while preserving budget checks. Enforce the contract-wide hourly transaction limit independently for each submitted group so the run cannot exceed remaining slots.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/db/repositories.ts`:
- Around line 294-304: Update src/db/repositories.ts lines 294-304 in
getEffectivePolicy() to merge the override TTL fields onto the contract’s
default policy while preserving its keypair_source and other signing metadata;
update src/commands/guard.ts lines 108-126 so type-specific auto-extension is
reported enabled only when an enabled contract-level policy with signer metadata
exists, requiring or establishing that policy as needed.
In `@tests/core/extension.test.ts`:
- Around line 835-841: Correct the low-TTL fixture values in
tests/core/extension.test.ts at lines 835-841 and 893-899: update the first
instance entry’s live_until_ledger to leave fewer than 300 ledgers remaining,
and the second to leave fewer than 50, while preserving the tests’ override
behavior.
---
Outside diff comments:
In `@src/core/extension.ts`:
- Around line 398-432: Update the extension flow around needsExtension,
entryKeys, simulateExtension, and extendEntries to group entries by each entry’s
resolved target TTL instead of using firstTarget for all entries. For every
target group, simulate and submit only that group with its target, while
preserving budget checks. Enforce the contract-wide hourly transaction limit
independently for each submitted group so the run cannot exceed remaining slots.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 9f58818f-ae25-4825-a661-f6e862219f7d
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (11)
ANALYSIS.mdPART_2_TESTS_SUMMARY.mdsrc/commands/guard.tssrc/core/extension.tssrc/db/migrations/002_entry_type_policies.sqlsrc/db/migrations/003_add_entry_type_policies_support.tssrc/db/repositories.tstests/commands/guard.test.tstests/core/budget_enforcement.test.tstests/core/extension.test.tstests/db/repositories.test.ts
📜 Review details
🧰 Additional context used
🪛 LanguageTool
ANALYSIS.md
[uncategorized] ~9-~9: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ... #### File: src/core/extension.ts Rate Limiting Section: ```typescript export const H...
(EN_COMPOUND_ADJECTIVE_INTERNAL)
PART_2_TESTS_SUMMARY.md
[uncategorized] ~276-~276: If this is a compound adjective that modifies the following noun, use a hyphen.
Context: ... - ✓ Threshold enforcement per type - ✓ Rate limiting remains contract-wide - ✓ Multiple type...
(EN_COMPOUND_ADJECTIVE_INTERNAL)
🪛 markdownlint-cli2 (0.23.1)
ANALYSIS.md
[warning] 10-10: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
[warning] 21-21: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
[warning] 52-52: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
[warning] 105-105: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
[warning] 116-116: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
[warning] 121-121: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
[warning] 137-137: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
[warning] 141-141: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
[warning] 147-147: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
[warning] 174-174: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
[warning] 187-187: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
[warning] 199-199: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
[warning] 216-216: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
[warning] 225-225: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
[warning] 230-230: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
[warning] 236-236: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
[warning] 262-262: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 274-274: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
[warning] 293-293: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 304-304: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
[warning] 336-336: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
[warning] 392-392: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
[warning] 411-411: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 417-417: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 422-422: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 427-427: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
PART_2_TESTS_SUMMARY.md
[warning] 3-3: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 18-18: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 27-27: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 35-35: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 43-43: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 48-48: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 55-55: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 62-62: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 79-79: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 92-92: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 101-101: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 110-110: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 118-118: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 130-130: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 147-147: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 152-152: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 153-153: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
[warning] 173-173: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 174-174: Fenced code blocks should be surrounded by blank lines
(MD031, blanks-around-fences)
[warning] 233-233: Ordered list item prefix
Expected: 1; Actual: 4; Style: 1/1/1
(MD029, ol-prefix)
[warning] 263-263: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 271-271: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 279-279: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 304-304: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 310-310: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 318-318: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 344-344: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
[warning] 351-351: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below
(MD022, blanks-around-headings)
🪛 Squawk (2.61.0)
src/db/migrations/002_entry_type_policies.sql
[warning] 11-11: Using 32-bit integer fields can result in hitting the max int limit. Use 64-bit integer values instead to prevent hitting this limit.
(prefer-bigint-over-int)
[warning] 12-12: Using 32-bit integer fields can result in hitting the max int limit. Use 64-bit integer values instead to prevent hitting this limit.
(prefer-bigint-over-int)
🔇 Additional comments (3)
src/db/migrations/002_entry_type_policies.sql (1)
8-19: LGTM!tests/db/repositories.test.ts (1)
485-634: 🎯 Functional CorrectnessVerify every in-memory suite has
entry_type_policies.These suites use the new table but do not apply migration 002, while
tests/core/budget_enforcement.test.tsnow runs migrations specifically to create it. IfgetDatabaseForTesting()does not already include this table, both suites fail withno such table: entry_type_policies.
tests/db/repositories.test.ts#L485-L634: ensure the shared setup applies migration 002 or the test schema includes the table.tests/core/extension.test.ts#L816-L1161: use the same migration-aware setup before inserting overrides.tests/core/budget_enforcement.test.ts (1)
13-13: LGTM!Also applies to: 64-68
| if (override) { | ||
| return { | ||
| id: 0, | ||
| contract_id: contractId, | ||
| enabled: 1, | ||
| target_ttl_ledgers: override.target_ttl_ledgers, | ||
| extend_when_below_ledgers: override.extend_when_below_ledgers, | ||
| keypair_public: null, | ||
| keypair_source: null, | ||
| created_at: new Date(), | ||
| }; |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Keep contract signing metadata when resolving a type override.
Type overrides store only TTL fields, yet getEffectivePolicy() replaces the default policy’s keypair_source with null. Consequently, an override configured through guard --entry-type ... --auto-extend cannot resolve a signer without a channel pool; configuring only an override also leaves the contract ineligible because no enabled default exists.
src/db/repositories.ts#L294-L304: merge override TTL values onto the contract default so signer metadata remains available.src/commands/guard.ts#L108-L126: require or establish an enabled contract-level policy with a signer before reporting type-specific auto-extension as enabled.
📍 Affects 2 files
src/db/repositories.ts#L294-L304(this comment)src/commands/guard.ts#L108-L126
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@src/db/repositories.ts` around lines 294 - 304, Update src/db/repositories.ts
lines 294-304 in getEffectivePolicy() to merge the override TTL fields onto the
contract’s default policy while preserving its keypair_source and other signing
metadata; update src/commands/guard.ts lines 108-126 so type-specific
auto-extension is reported enabled only when an enabled contract-level policy
with signer metadata exists, requiring or establishing that policy as needed.
| // Instance entry with low TTL | ||
| upsertEntry(db, { | ||
| contract_id: contractId, | ||
| entry_key_xdr: "instance-key-xdr", | ||
| entry_type: "instance", | ||
| live_until_ledger: 2400500, // remaining = 500, below 300 threshold | ||
| }); |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Fix the “below threshold” fixtures.
2400500 - 2400000 = 500, not below 300; 2400075 - 2400000 = 75, not below 50. Both tests therefore select no entry and fail before exercising override behavior.
tests/core/extension.test.ts#L835-L841: use a remaining TTL below 300, such as2400200.tests/core/extension.test.ts#L893-L899: use a remaining TTL below 50, such as2400025.
📍 Affects 1 file
tests/core/extension.test.ts#L835-L841(this comment)tests/core/extension.test.ts#L893-L899
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@tests/core/extension.test.ts` around lines 835 - 841, Correct the low-TTL
fixture values in tests/core/extension.test.ts at lines 835-841 and 893-899:
update the first instance entry’s live_until_ledger to leave fewer than 300
ledgers remaining, and the second to leave fewer than 50, while preserving the
tests’ override behavior.
|
| GitGuardian id | GitGuardian status | Secret | Commit | Filename | |
|---|---|---|---|---|---|
| - | - | Generic High Entropy Secret | ded54f4 | tests/commands/guard-cli-export-import.test.ts | View secret |
🛠 Guidelines to remediate hardcoded secrets
- Understand the implications of revoking this secret by investigating where it is used in your code.
- Replace and store your secret safely. Learn here the best practices.
- Revoke and rotate this secret.
- If possible, rewrite git history. Rewriting git history is not a trivial act. You might completely break other contributing developers' workflow and you risk accidentally deleting legitimate data.
To avoid such incidents in the future consider
- following these best practices for managing and storing secrets including API keys and other credentials
- install secret detection on pre-commit to catch secret before it leaves your machine and ease remediation.
🦉 GitGuardian detects secrets in your source code to help developers and security teams secure the modern development process. You are seeing this because you or someone else with access to this repository has authorized GitGuardian to scan your pull request.
43ad363 to
8692701
Compare
Adds an entry_type_policies table so instance/wasm/persistent/temporary entries can each get their own target TTL and extension threshold, falling back to the contract-level extension_policies row when no override exists. Fixes a batching bug present in the original PR: ExtendFootprintTTLOp sets one target TTL for an entire key batch, so entries whose effective policy resolves to different target_ttl_ledgers values are now grouped and extended in separate transactions instead of silently applying one entry's target to the whole batch. Adds 'sorokeep guard entry-type <contractId> <entryType>' to set or --clear a per-type override.
|
Merged as 8649dbf. Per-entry-type extension policy overrides (target TTL and extend-when-below thresholds can now differ per entry type: instance/wasm/persistent/temporary), rebuilt against current main's schema (migration 010). |
…TTLOp tx (#490) The per-entry-type-policy grouping added for #491/#563 already batches every entry on a contract that resolves to the same effective target TTL into a single extendEntries()/submitExtension() call, which is exactly what issue #490 (PR #552) asked for. That PR's own diff duplicated the grouping logic against a stale snapshot of extension.ts and would have conflicted with the already-merged #563 restructuring, so it's skipped as superseded rather than merged. This test closes the verification gap by asserting a single submitExtension call carries both entries' keys and that extension_history attributes the same tx_hash/cpu_insns to both.
… PR #593) Adds opt-in predictive scheduling: the monitor cycle records a live_until_ledger sample per entry each pass (capped at 10, oldest pruned), and runAutoExtensions can extend an entry early when a linear-regression projection of its decay rate crosses the threshold within the next N daemon cycles — before the reactive threshold fires. Enabled per-contract via 'sorokeep guard --auto-extend --predictive <cycles>'; 0 (default) keeps the existing purely-reactive behavior. Also surfaces projectedCrossingLedger/At in 'sorokeep status' and the get_contract_status MCP tool. PR #593's own diff touched extension.ts/repositories.ts/database.ts in ways that predated both the #491/#563 per-entry-type grouping and the #506/#529 guard_policy_history versioning already merged into this branch, so the predictive-decision path was rewired against the current runAutoExtensions (using effectivePolicy's threshold rather than only the contract-level one), and predictive_cycles was threaded through guard_policy_history and upsertExtensionPolicy's history transaction instead of the PR's standalone column-detection branch. Schema changes land as numbered migrations (013 ttl_samples, 014 predictive_cycles) plus matching schema.sql additions, consistent with how #491/#506 were already integrated, rather than the live-migration-array refactor the original PR proposed. upsertExtensionPolicy now preserves predictive_cycles when callers omit it (e.g. --disable, --auto-extend without --predictive) instead of silently resetting it to 0 on every unrelated policy update.
Summary
Implement per-entry-type TTL policy overrides to allow fine-grained control over extension parameters for different Soroban entry types (instance, wasm, persistent, temporary). This enables operators to set different extension targets and thresholds based on the specific type of entry being extended.
closes #491
Changes
File:
003_add_entry_type_policies_support.ts
Created new migration to add entry_type_policies table
Composite PRIMARY KEY on (contract_id, entry_type) enables one override per type per contract
CHECK constraint validates entry_type values: 'instance', 'wasm', 'persistent', 'temporary'
Foreign key references contracts(id) with ON DELETE CASCADE
Index on contract_id for efficient lookups
2. Repository Functions
File:
repositories.ts
Added three new repository functions:
getEffectivePolicy(db, contractId, entryType)
Resolves the effective TTL policy for a specific entry type
Resolution order: type-specific override → contract-level default → null
Used by extension logic to determine per-entry thresholds
setEntryTypePolicy(db, contractId, entryType, policy)
Creates or updates a per-entry-type policy override
Uses UPSERT pattern (INSERT ... ON CONFLICT DO UPDATE)
Idempotent and safe for repeated calls
Updates updated_at timestamp on modification
deleteEntryTypePolicy(db, contractId, entryType)
Removes a per-entry-type override
After deletion, contract-level default applies again
Idempotent - safe to call even if override doesn't exist
3. Extension Logic
File:
extension.ts
Updated runAutoExtensions() to use per-entry-type policies:
For each entry, calls getEffectivePolicy() with entry type
Uses resolved policy's threshold/target for that specific entry
Falls back to contract-level default if no type override exists
Rate limiting remains per-contract (not per-type) - enforces single HOURLY_RATE_LIMIT across all entry types
Bug fix: Changed policy.target_ttl_ledgers to firstTarget in extendEntries call to use correct resolved value.
File:
guard.ts
Added --entry-type flag to enable setting type-specific overrides:
Flag accepts: 'instance', 'wasm', 'persistent', 'temporary'
Validates input against allowed values with helpful error messages
With --entry-type: calls setEntryTypePolicy() for type-specific override
Without --entry-type: calls upsertExtensionPolicy() for contract-level default (unchanged behavior)
Maintains backward compatibility - existing commands continue to work
5. Test Infrastructure
Files:
repositories.test.ts
extension.test.ts
guard.test.ts
budget_enforcement.test.ts
Test Coverage:
Type-specific override creation, update (upsert), and deletion
Fallback to contract default when no override exists
Type isolation - overrides for one type don't affect others
Policy resolution returns null when no policy exists
Rate limiting enforced per-contract, not per-type
Multiple entry types with different overrides extended in same transaction
Guard command validation and policy persistence
Technical Details
Policy Resolution Chain
For entry type "instance":
Rate Limiting Behavior
Remains per-contract (not per-entry-type)
Single rate limit check before processing any entries for a contract
HOURLY_RATE_LIMIT (5 extensions/hour) applies to entire contract
Prevents runaway submissions under network load
Backward Compatibility
Existing contracts without type-specific overrides continue using contract-level defaults
--entry-type flag is optional on guard command
All changes are additive - no modifications to existing tables or APIs
Files Modified
003_add_entry_type_policies_support.ts
(new)
repositories.ts
(added 3 functions)
extension.ts
(updated runAutoExtensions, fixed bug)
guard.ts
(added --entry-type flag)
repositories.test.ts
(added 7 tests)
extension.test.ts
(added imports, 6 new tests, fixed mocks)
guard.test.ts
(added 6 tests)
budget_enforcement.test.ts
(added migration execution)
Testing
All tests pass:
19 new tests specifically for per-entry-type policies
Existing tests updated to work with new database schema
Budget enforcement tests now run migrations to create entry_type_policies table
No changes to existing passing tests
Example Usage
Set instance-specific override
sorokeep guard CCONTRACT_ID --entry-type instance
--target-ttl 50000 --threshold 10000
--keypair-env STELLAR_KEY --auto-extend
Set wasm-specific override
sorokeep guard CCONTRACT_ID --entry-type wasm
--target-ttl 75000 --threshold 15000
--keypair-env STELLAR_KEY --auto-extend
Set contract-level default (affects all types without overrides)
sorokeep guard CCONTRACT_ID
--target-ttl 100000 --threshold 20000
--keypair-env STELLAR_KEY --auto-extend
Impact
Operators can now optimize extension parameters for different entry types
Provides finer control over TTL management without multiple contracts
Maintains simplicity for users who don't need type-specific settings
Rate limiting remains predictable at contract level