{"record":{"id":"0f5ad5007886ae65","repo":"ruvnet/ruflo","slug":"scope-expired","errorCode":"scope-expired","errorMessage":"scope expired at ${new Date(currentScope.expiresAt).toISOString()}","messagePattern":"scope expired at (.+?)","errorType":"exception","errorClass":"AuthorizationPropagationError","httpStatus":null,"severity":"error","filePath":"v3/@claude-flow/security/src/authorization/propagator.ts","lineNumber":159,"sourceCode":"   *   - newly granted servers MUST be a subset of `currentScope.grantedServers`\n   *   - delegationDepth MUST decrement by ≥ 1 (must remain ≥ 0)\n   *   - principalId is propagated unchanged\n   *   - expiresAt cannot be extended; copied from the holder\n   */\n  wrapOutbound<T>(\n    payload: T,\n    currentScope: AuthScope,\n    requested: { tools?: ReadonlyArray<string>; servers?: ReadonlyArray<string> } = {},\n  ): SendMessageEnvelope<T> {\n    if (currentScope.delegationDepth <= 0) {\n      throw new AuthorizationPropagationError(\n        'depth-underflow',\n        `cannot delegate further — delegationDepth=${currentScope.delegationDepth}`,\n      );\n    }\n    const now = Date.now();\n    if (currentScope.expiresAt <= now) {\n      throw new AuthorizationPropagationError(\n        'scope-expired',\n        `scope expired at ${new Date(currentScope.expiresAt).toISOString()}`,\n      );\n    }\n\n    const reducedTools = subsetOrThrow(\n      currentScope.grantedTools,\n      requested.tools ?? currentScope.grantedTools,\n      'tools',\n    );\n    const reducedServers = subsetOrThrow(\n      currentScope.grantedServers,\n      requested.servers ?? currentScope.grantedServers,\n      'servers',\n    );\n\n    const reducedScope: AuthScope = {\n      principalId: currentScope.principalId,","sourceCodeStart":141,"sourceCodeEnd":177,"githubUrl":"https://github.com/ruvnet/ruflo/blob/fa13ee4ad60ac2090b1480656eb233521790d640/v3/@claude-flow/security/src/authorization/propagator.ts#L141-L177","documentation":"wrapOutbound refuses to propagate a scope whose expiresAt (unix ms) is at or past the current time — AuthorizationPropagationError code 'scope-expired'. Expiry is copied unchanged from the holder and can never be extended (adding time requires a new grant from the original principal), so long-running delegation chains can simply run out of clock.","triggerScenarios":"A delegated task sits in a queue past the scope's TTL before the next hop; distributed deployments with clock skew, since expiresAt is compared against local Date.now(); resumed or retried workflows reusing a stale scope.","commonSituations":"Long swarm workflows outliving a short TTL; NTP drift between nodes making a valid scope look expired; checkpointed sessions restored hours later with their original scopes.","solutions":["Obtain a fresh scope from the original principal — expiry cannot be extended in place","Check Date.now() < scope.expiresAt (with margin) before sending, and refresh proactively at ~80% of TTL","Sync clocks (NTP/chrony) across nodes if expiries fire suspiciously early","Size the root scope's TTL to the whole workflow duration, not a single hop"],"exampleFix":"// before\nconst envelope = propagator.wrapOutbound(msg, scope); // stale → scope-expired\n\n// after\nconst usable = scope.expiresAt - Date.now() > 30_000\n  ? scope\n  : await reissueScopeFromPrincipal(scope.principalId); // out-of-band re-grant\nconst envelope = propagator.wrapOutbound(msg, usable);","handlingStrategy":"validation","validationCode":"const TTL_MARGIN_MS = 30_000;\nfunction scopeStillValid(scope: AuthScope, now = Date.now()): boolean {\n  return scope.expiresAt - now > TTL_MARGIN_MS;\n}\nif (!scopeStillValid(scope)) {\n  scope = await reissueScopeFromPrincipal(scope.principalId);\n}\nconst envelope = propagator.wrapOutbound(msg, scope);","typeGuard":"function isAuthPropagationError(e: unknown, code?: string): boolean {\n  return e instanceof Error && e.name === 'AuthorizationPropagationError'\n    && (code === undefined || (e as { code?: string }).code === code);\n}","tryCatchPattern":"try {\n  return propagator.wrapOutbound(msg, scope);\n} catch (e) {\n  if (isAuthPropagationError(e, 'scope-expired')) {\n    const fresh = await reissueScopeFromPrincipal(scope.principalId);\n    return propagator.wrapOutbound(msg, fresh);\n  }\n  throw e;\n}","preventionTips":["Refresh scopes proactively at ~80% of TTL instead of waiting for the throw","Size root-scope TTL to whole-workflow duration","Keep nodes NTP-synced when scopes cross machine boundaries"],"tags":["security","authorization","expiry","clock-skew","delegation"],"backgroundTag":"authorization-scope-expired","analyzedSha":"fa13ee4ad60ac2090b1480656eb233521790d640","analyzedAt":"2026-08-18T21:34:22.708Z","contentChangedAt":"2026-08-18T21:34:22.708Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}