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 docs/experimental-recovery.ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ MCP 서버가 원격 셸을 실행하지는 않습니다. claim 만료 시각은

## 정책과 운영 상태 확인

기본값은 **최근 24시간 최대 3회, 실행 허가 사이 최소 3,600초**입니다. 관리자 시험은 간격을 유지하면서 최대 12회로 명시적으로 설정했습니다. 성공한 시도도 횟수에 포함됩니다. 설정 키는 `recovery_max_claims_per_24h`, `recovery_cooldown_seconds`이며, 정책 변경은 서버 설정 검토를 거쳐야 합니다. Dots에게 제한을 무시하라고 요청하는 방식으로 바꾸지 마세요.
기본값은 **최근 24시간 최대 3회, 실행 허가 사이 최소 3,600초**입니다. 관리자 시험은 간격을 유지하면서 최대 12회로 명시적으로 설정했습니다. 성공한 시도도 횟수에 포함됩니다. 별도로 검토할 [운영 한도 24회 변경안](../research/mcp-events/recovery-budget.ko.md)이 있으며, 패키지 기본값 변경이나 운영 반영 완료를 뜻하지는 않습니다. 설정 키는 `recovery_max_claims_per_24h`, `recovery_cooldown_seconds`이며, 정책 변경은 서버 설정 검토를 거쳐야 합니다. Dots에게 제한을 무시하라고 요청하는 방식으로 바꾸지 마세요.

MCP의 `get_current_recovery`로 `observation_fresh`, `claimable`, `eligibility_reason`, `retry_at`, `policy`를 확인합니다. `retry_at`은 참고 시각입니다. 그 시간이 지나도 최신 상태를 다시 조회하고 새 claim을 받아야 실행할 수 있습니다. 같은 incident ID로 알림이 다시 올 수 있지만, 새 실행 권한을 뜻하지 않습니다. 원래 장애·claim·전송 이력은 보존됩니다. 이미 claim이 있는 장애는 결과가 불명확해도 다시 claim하거나 실행하지 않습니다.

Expand Down
2 changes: 1 addition & 1 deletion docs/experimental-recovery.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ The broker does not execute a remote shell. The claim's expiry must be respected

## Policy and operating status

The default is **three claims per rolling 24 hours, at least 3,600 seconds apart**. The maintainer's trial uses an explicit 12-claim limit with the same interval. Successful attempts also count. Operator configuration keys are `recovery_max_claims_per_24h` and `recovery_cooldown_seconds`; changes require reviewed server configuration, not a prompt asking Dots to bypass policy.
The default is **three claims per rolling 24 hours, at least 3,600 seconds apart**. The maintainer's trial uses an explicit 12-claim limit with the same interval. Successful attempts also count. A separate [24-claim operating change](../research/mcp-events/recovery-budget.md) is now proposed; it is not a changed package default or evidence of deployment. Operator configuration keys are `recovery_max_claims_per_24h` and `recovery_cooldown_seconds`; changes require reviewed server configuration, not a prompt asking Dots to bypass policy.

Use the MCP `get_current_recovery` tool to inspect `observation_fresh`, `claimable`, `eligibility_reason`, `retry_at`, and `policy`. A future `retry_at` is advisory. Even after that time, Dots must reread fresh state and obtain an accepted claim. A reminder can carry the same incident ID; it does not create new execution authority. The original incident, claims and delivery history remain intact. An already claimed incident must not be claimed or executed again, including when its result is unknown.

Expand Down
27 changes: 22 additions & 5 deletions research/mcp-events/new_incident_policy_deployment.py
Original file line number Diff line number Diff line change
Expand Up @@ -141,8 +141,15 @@ def patch_server(body):
'max_claims_per_24h': "cfg.get('recovery_max_claims_per_24h', 3)",
'cooldown_seconds': "cfg.get('recovery_cooldown_seconds', 3600)",
'new_incident_cooldown_seconds': "cfg.get('recovery_new_incident_cooldown_seconds')",
'local_coordination': "cfg.get('recovery_local_coordination', False)",
}
if set(names) not in (set(expected) - {'new_incident_cooldown_seconds'}, set(expected)):
required = {'max_claims_per_24h', 'cooldown_seconds'}
supported_shapes = {
frozenset(required | optional)
for optional in (set(), {'new_incident_cooldown_seconds'}, {'local_coordination'},
{'new_incident_cooldown_seconds', 'local_coordination'})
}
if frozenset(names) not in supported_shapes:
raise ValueError('unexpected recovery policy call shape')
for keyword in call.keywords:
if _expression(keyword.value) != _expression(ast.parse(expected[keyword.arg], mode='eval').body):
Expand All @@ -168,7 +175,7 @@ def offset(line, column):
return result


def config_candidate(body, seconds):
def config_candidate(body, seconds, max_claims_per_24h=None):
def unique(pairs):
result = {}
for key, value in pairs:
Expand All @@ -187,10 +194,16 @@ def unique(pairs):
cooldown = value.get('recovery_cooldown_seconds', 3600)
if type(budget) is not int or not 1 <= budget <= 24:
raise ValueError('invalid existing claim budget')
if max_claims_per_24h is not None and (
type(max_claims_per_24h) is not int or not 1 <= max_claims_per_24h <= 24):
raise ValueError('new claim budget must be an integer from 1 to 24')
if type(cooldown) is not int or not 300 <= cooldown <= 86400:
raise ValueError('invalid existing global cooldown')
if type(seconds) is not int or not 300 <= seconds <= cooldown:
raise ValueError('new incident interval must be from 300 to the existing cooldown')
if max_claims_per_24h is not None:
value['recovery_max_claims_per_24h'] = max_claims_per_24h
budget = max_claims_per_24h
value['recovery_new_incident_cooldown_seconds'] = seconds
return value, {'max_claims_per_24h': budget, 'cooldown_seconds': cooldown,
'new_incident_cooldown_seconds': seconds}
Expand Down Expand Up @@ -223,6 +236,8 @@ def main():
parser.add_argument('--expected-server-sha256', required=True)
parser.add_argument('--candidate-core-sha256', required=True)
parser.add_argument('--new-incident-cooldown-seconds', type=int, required=True)
parser.add_argument('--max-claims-per-24h', type=int,
help='explicitly set the rolling claim cap (integer 1..24); omission preserves the current value')
args = parser.parse_args()
if os.geteuid() != 0:
parser.error('root required')
Expand All @@ -238,7 +253,8 @@ def main():
initial_config, initial_meta = read_regular(CONFIG, account.pw_uid)
if stat.S_IMODE(initial_meta.st_mode) != 0o600:
raise ValueError('private configuration permissions required')
config_candidate(initial_config, args.new_incident_cooldown_seconds)
config_candidate(initial_config, args.new_incident_cooldown_seconds,
args.max_claims_per_24h)
locks = []
try:
# The release updater owns the first lock; earlier policy helpers own the
Expand All @@ -250,7 +266,8 @@ def main():
original, config_meta = read_regular(CONFIG, account.pw_uid)
if stat.S_IMODE(config_meta.st_mode) != 0o600:
raise ValueError('private configuration permissions required')
cfg, policy = config_candidate(original, args.new_incident_cooldown_seconds)
cfg, policy = config_candidate(original, args.new_incident_cooldown_seconds,
args.max_claims_per_24h)
patched = patch_server(server)
parsed_origin = urlsplit(cfg['origin'])
if (parsed_origin.scheme != 'https' or not parsed_origin.hostname
Expand Down Expand Up @@ -333,7 +350,7 @@ def health():
raise ValueError('new policy not exposed by installed server')
for key in ('max_claims_per_24h', 'cooldown_seconds'):
if observed['policy'].get(key) != policy[key]:
raise ValueError('existing policy changed')
raise ValueError('requested policy not exposed by installed server')
phase = 'start_broker'
run(['systemctl', 'start', *UNITS])
health()
Expand Down
49 changes: 49 additions & 0 deletions research/mcp-events/recovery-budget.ko.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# 운영 복구 한도 24회 변경안

[English](recovery-budget.md)

[#80](https://github.com/abruption/dots-tailscale/issues/80)의 운영 정책 변경안입니다. 명시적으로 설정한 **12회** 한도를 **최근 86,400초 동안 claim 최대 24회**로 높입니다. 이 문서가 병합되거나 dots-tailscale을 설치하는 것만으로 운영 서버의 설정이 바뀌지는 않습니다.

패키지 기본값은 계속 **최근 24시간 최대 3회**입니다. 이번 운영 변경안은 전역 실행 간격 **3,600초**, 복구가 확인된 뒤 새 장애에 적용하는 간격 **600초**를 유지합니다. 성공·실패·결과 불명·미보고 여부와 관계없이 발급된 claim은 계속 집계합니다. 집계 기간을 초기화하거나 횟수를 돌려주지 않으며, 로컬 복구 조정 기능이나 watchdog의 별도 한도도 바꾸지 않습니다.

## 변경 준비

기존 비공개 서버 설정에서 다음 키 하나를 바꾸는 안을 검토합니다.

```json
{"recovery_max_claims_per_24h": 24}
```

`recovery_cooldown_seconds=3600`, `recovery_new_incident_cooldown_seconds=600`, 인증·메일 연동·구독 설정 등 다른 값은 유지합니다. 예제 파일로 비공개 설정 전체를 덮어쓰지 마세요. 일반 `config.example.json`은 복구가 꺼진 상태를 유지하며 이번 운영 한도를 기본값으로 넣지 않습니다.

검토된 `new_incident_policy_deployment.py`에 선택적인 `--max-claims-per-24h` 옵션을 지원합니다. 생략하면 기존 한도나 암묵적 기본값을 보존하고, 지정할 때는 1~24의 정수만 받습니다. 기존 설정이 잘못됐다면 새 값으로 조용히 고치는 대신 거부합니다.

도구의 보호된 준비 경로, 소스 해시 고정, 배포 잠금, 최신 정상 관측과 진행 중인 작업이 없어야 한다는 조건은 유지합니다. **열린 장애나 전송 중인 이벤트가 있으면 적용을 거부합니다.** 독립 복구 터미널을 준비하고 이 조건을 충족하는 점검 시점을 확보하세요. 진행 중인 장애의 실행 제한을 우회하는 도구가 아닙니다.

이번 한도 변경에서는 이미 설치된 코어를 독립적으로 검증해 그대로 준비하고, 두 코어 인자에 같은 해시를 사용합니다. 기존 통합 서버도 확인해 해시를 고정하세요. 연동 코드를 바꿀 필요가 없도록 이미 지원되는 정책 인자를 전달하는 서버여야 합니다. 지원되는 `local_coordination` 인자가 있으면 그대로 보존합니다. 통합 서버를 공개 연구용 서버로 교체하지 마세요.

키 하나만 바꾸는 이 절차를 쓰기 전에 최신 MCP `policy`와 비공개 설정을 읽어 기존 정책이 정확히 12/3600/600이고, 검토한 서버 패치 결과가 원본과 바이트 단위로 같은지 확인하세요. 다르면 중단합니다. 범용 도구는 명시적으로 요청한 다른 변경도 지원하므로, 아래 옵션만으로 이번 절차의 전제까지 강제되지는 않습니다.

정확한 해시를 확인한 뒤 root 인증으로 실행하는 명령 형태는 다음과 같습니다. 대문자는 실제 해시로 바꿔야 하는 자리표시자입니다.

```sh
python3 /root/reviewed/new_incident_policy_deployment.py \
--source-dir /root/reviewed/source \
--expected-core-sha256 INSTALLED_CORE_SHA256 \
--expected-server-sha256 INSTALLED_INTEGRATED_SERVER_SHA256 \
--candidate-core-sha256 INSTALLED_CORE_SHA256 \
--new-incident-cooldown-seconds 600 \
--max-claims-per-24h 24
```

이 명령은 별도로 승인된 운영 배포에서만 실행합니다. root 인증이 필요하고 broker 소켓과 서비스를 잠시 중지했다가 시작하며, 코드·설정의 비공개 백업을 남깁니다. 도구가 claim을 만들거나 콜백을 재전송하거나 시작 명령을 실행하지는 않습니다. 재시작 뒤 정상 감시 경로가 현재 조건에 따라 정책 제한 해제 이벤트를 전달할 수는 있지만, 그 이벤트도 실행하려면 새 claim이 필요합니다.

## 확인과 원복

도구의 반환 정책과 최신 읽기 전용 MCP 상태에서 `max_claims_per_24h=24`, `cooldown_seconds=3600`, `new_incident_cooldown_seconds=600`을 확인합니다. 짧은 간격을 적용할 새 장애가 없으면 실제 적용 간격이 3,600초로 나오는 것도 정상입니다. 기존 claim 집계는 유지합니다. 같은 집계 기간의 25번째 claim 거부는 격리 시험으로 확인하며, 운영 서버에 시험 claim을 만들지는 마세요. `retry_at`은 참고 시각이며 시간이 지났다는 이유만으로 실행할 수는 없습니다.

설정은 JSON으로 읽은 값끼리 비교하세요. 도구가 키 순서와 서식을 다시 작성합니다. 통합 코어·서버의 내용, 한도 키 이외의 비공개 설정, 구독과 실행 승인 이력이 보존됐는지 확인합니다. 로컬 복구 정책도 유지하세요. 이후 실제 복구를 확정하려면 외부의 새 SSH·ping 관측이 필요합니다.

12회로 되돌릴 때는 시작 정책이 24/3600/600인지 확인하고, 같은 소스 보존 절차에서 `--max-claims-per-24h 12`를 사용합니다. 원복에도 최신 정상 관측과 진행 중인 작업이 없어야 한다는 조건이 적용됩니다. 장애 중 긴급 중지가 필요하면 이 조건을 우회하지 말고 기존 [중지와 원복 절차](../../docs/experimental-recovery.ko.md#중지와-원복)를 따르세요. 이미 12회를 넘게 사용했다면 이전 claim이 충분히 집계 대상에서 빠질 때까지 새 실행이 제한될 수 있습니다. 낮아진 한도에 맞추려고 과거 DB를 복원하거나 기록을 지우지 마세요. 배포 중 새 claim이 생기면 운영자 확인을 요구하는 기존의 보수적인 원복 절차를 유지하며, 원장은 복원하지 않습니다.

이번 증설로 반복 단절의 원인, #79의 이벤트 수신 후 미실행 문제, #80의 나머지 진단·사전 경고·임시 추가 예산 과제가 해결되는 것은 아닙니다. 이 작업의 격리 시험이 운영 반영이나 무인 복구 성공을 입증하지는 않습니다.
49 changes: 49 additions & 0 deletions research/mcp-events/recovery-budget.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Proposed operating recovery budget: 24 claims per day

[한국어](recovery-budget.ko.md)

This is the proposed operating-policy change for [#80](https://github.com/abruption/dots-tailscale/issues/80). It raises an explicitly configured **12-claim** allowance to **24 claims per rolling 86,400 seconds**. Merging this document or installing dots-tailscale does not change a running broker.

The package default remains **3 claims per rolling 24 hours**. The operating proposal keeps the global interval at **3,600 seconds** and the confirmed-new-incident interval at **600 seconds**. An existing claim still counts whether its result is success, failure, unknown or unreported. The change does not reset the rolling window, refund claims, change local coordination or change the local watchdog budget.

## Prepare the change

Review a one-key change in the existing private broker configuration:

```json
{"recovery_max_claims_per_24h": 24}
```

Keep all other values, including `recovery_cooldown_seconds=3600`, `recovery_new_incident_cooldown_seconds=600`, authentication, mail integration and subscription settings. Do not replace the full private configuration with an example file. The general `config.example.json` remains disabled and does not adopt this operating allowance.

The reviewed `new_incident_policy_deployment.py` helper accepts an optional `--max-claims-per-24h`. Omitting it preserves the current budget or implicit default; an explicit value must be an integer from 1 to 24. Invalid existing values are rejected rather than repaired implicitly.

The helper retains its protected staging, source pins, exclusive deployment locks and fresh/healthy/quiescent checks. **It refuses to apply during an open outage or in-flight delivery.** Prepare an independent recovery terminal and a maintenance point meeting those checks; the helper cannot be used to bypass an active incident's refusal.

For this budget-only operating change, stage the already installed, independently verified core unchanged and use its same digest for both core arguments. Inspect and pin the existing integrated server; it must already pass the supported policy arguments so no connector code needs editing. The helper preserves the supported `local_coordination` argument if present. Never replace the integrated server with the public research server.

Before using this one-key recipe, read the current MCP `policy` and inspect the private configuration to confirm the existing policy is exactly 12/3600/600 and that the reviewed server patch would leave its bytes unchanged; stop if either precondition differs. The general helper also supports other explicitly requested changes, so those flags alone do not enforce this recipe.

After inspecting the exact hashes, the root-authenticated invocation has this shape. Uppercase values are placeholders, not usable hashes:

```sh
python3 /root/reviewed/new_incident_policy_deployment.py \
--source-dir /root/reviewed/source \
--expected-core-sha256 INSTALLED_CORE_SHA256 \
--expected-server-sha256 INSTALLED_INTEGRATED_SERVER_SHA256 \
--candidate-core-sha256 INSTALLED_CORE_SHA256 \
--new-incident-cooldown-seconds 600 \
--max-claims-per-24h 24
```

Only a separately approved operating deployment runs that command. It needs root authentication, briefly stops/restarts the broker socket and service, and retains private code/configuration backups. No claim, callback replay or startup command is issued by the helper. Normal monitoring may deliver a policy-unblocked event after restart if current conditions warrant it; that event still requires a fresh claim before execution.

## Verify and roll back

Check the returned policy and a fresh read-only MCP status: `max_claims_per_24h=24`, `cooldown_seconds=3600`, `new_incident_cooldown_seconds=600`. An effective interval of 3,600 seconds remains valid when there is no eligible confirmed-new-incident transition. Existing claim counts must be retained. The isolated tests check that the 25th claim within the same rolling window is blocked; do not create test claims on the operating broker. `retry_at` is advisory; time passing alone does not authorize execution.

Compare configuration values as parsed JSON; the helper rewrites key ordering and formatting. Verify that the integrated core/server content, private configuration outside the budget key, subscriptions and authorization history are preserved. Keep local recovery policy unchanged. Independent external SSH and ping remain necessary to confirm any later recovery.

To return to 12, confirm the starting policy is 24/3600/600 and use the same source-preserving process with `--max-claims-per-24h 12`. Rollback also requires fresh healthy observations and no open work. For an emergency stop during an outage, follow the existing [stop and rollback procedure](../../docs/experimental-recovery.md#stop-and-rollback) instead of bypassing those guards. If usage is already above 12, that policy may block new claims until enough old entries expire. Never restore an old DB or delete entries to make the lower limit fit. The helper keeps its existing conservative rollback behavior, including operator attention if a new claim appears during deployment; it never restores a ledger.

This finite increase does not resolve repeated disconnects, the received-but-unclaimed reminder gap in #79, or the remaining diagnostics/early-warning/temporary-escalation work in #80. Those items remain separate. No operating rollout or unattended recovery success is established by the isolated tests for this change.
Loading
Loading