affaan-m/ECC · error · ReplayError

tool.effect_forbidden

tool.effect_forbidden

Error message

tool ${name} is ${tool.effect_class}, above the allowed ${options.maxEffectClass || 'SE2'}

What it means

replayer.call throws ReplayError('tool.effect_forbidden') when the tool's effect rank exceeds options.maxEffectClass (default 'SE2'), refusing high-effect tools such as SE3+ (network, money-touching, counterparty-facing operations). A second, unconditional variant fires in replay mode for any tool ranked SE3 or above — such tools can never be replayed from fixtures regardless of maxEffectClass. The call is also recorded with status 'refused'.

Solutions

  1. For record mode with legitimate high-effect tools, raise the ceiling: createReplayer(tools, { mode: 'record', store, maxEffectClass: 'SE3' }).
  2. For replay mode, do not attempt to replay SE3+ tools — split them out of the replayed suite or replace them with a lower-effect, deterministic stub tool (e.g. SE1 fake) declared in the tools map.
  3. If the tool was declared with too high an effect class by mistake, correct its effect_class to the accurate level.
  4. Inspect r.calls for entries with status 'refused' and code 'tool.effect_forbidden' to find all offending calls before re-running.

Example fix

// before: replaying a money tool — forbidden
const tools = { chargeCard: { effect_class: 'SE3', determinism: 'nondeterministic', impl } };
r.call('chargeCard', { amount: 10 }); // throws even with fixture

// after: use a deterministic SE1 stub for replay
const tools = {
  chargeCard: { effect_class: 'SE3', determinism: 'nondeterministic', impl },
  chargeCardStub: { effect_class: 'SE1', determinism: 'deterministic', impl: () => ({ ok: true }) },
};
r.call('chargeCardStub', { amount: 10 });
Defensive patterns

Strategy: validation

Validate before calling

const { effectRank } = require('./envelope');
const maxRank = effectRank(options.maxEffectClass || 'SE2');
for (const name of plannedToolCalls) {
  const t = tools[name];
  if (effectRank(t.effect_class) > maxRank || (mode === 'replay' && effectRank(t.effect_class) >= effectRank('SE3'))) {
    throw new Error(`${name} (${t.effect_class}) cannot be called in this suite configuration`);
  }
}

Type guard

function callableInSuite(tool, maxEffectClass, mode, envelope) {
  const rank = envelope.effectRank(tool.effect_class);
  return rank <= envelope.effectRank(maxEffectClass || 'SE2') &&
    !(mode === 'replay' && rank >= envelope.effectRank('SE3'));
}

Try / catch

try {
  return replayer.call(name, args);
} catch (e) {
  if (e instanceof ReplayError && e.code === 'tool.effect_forbidden') {
    console.warn(`Refused high-effect tool ${name}; use a lower-effect stub or record mode`);
    return stubResponse(name, args);
  }
  throw e;
}

Prevention

When it happens

Trigger: Calling a tool declared SE3/SE4 with default options (maxEffectClass 'SE2'); in replay mode, calling any tool with rank >= SE3 even if maxEffectClass was raised; passing a maxEffectClass string whose rank is lower than the tool's (e.g. 'SE1').

Common situations: Trying to replay a live-payment or network tool in tests — the library intentionally forbids this; forgetting to raise maxEffectClass for a record-mode suite that legitimately exercises SE3 tools; new team members attempting to 'fix' fixture gaps by replaying expensive tools; running the same suite config in replay that was written for record.

Understand the failure class

Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.

Related errors


AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16). Data as JSON: /api/errors/bef1608d61f104bb. Report an issue: GitHub.

Appendix: source

Thrown at scripts/lib/eval-harness/replay.js:128

  const emit = (entry) => {
    calls.push(entry);
    if (typeof options.onCall === 'function') {
      options.onCall(entry);
    }
  };

  return {
    mode,
    calls,
    call(name, args = {}) {
      const tool = tools[name];
      if (!tool) {
        throw new ReplayError('tool.unknown', `tool ${name} is not declared`);
      }
      const rank = envelope.effectRank(tool.effect_class);
      if (rank > maxRank) {
        emit({ tool: name, mode, status: 'refused', code: 'tool.effect_forbidden' });
        throw new ReplayError('tool.effect_forbidden', `tool ${name} is ${tool.effect_class}, above the allowed ${options.maxEffectClass || 'SE2'}`, { tool: name });
      }
      if (mode === 'replay') {
        if (rank >= envelope.effectRank('SE3')) {
          emit({ tool: name, mode, status: 'refused', code: 'tool.effect_forbidden' });
          throw new ReplayError('tool.effect_forbidden', `tool ${name} (${tool.effect_class}) can never be replayed`, { tool: name });
        }
        const record = store.get(name, args);
        emit({ tool: name, mode, status: 'replayed', fixture_key: record.key, args_hash: record.args_hash, response_hash: record.response_hash });
        return record.response;
      }
      const response = tool.impl(args);
      const record = store.put(name, args, response);
      emit({ tool: name, mode, status: 'recorded', fixture_key: record.key, args_hash: record.args_hash, response_hash: record.response_hash });
      return response;
    },
  };
}

View on GitHub (pinned to 8321021c54)