@orkastery/cli 0.2.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +86 -65
- package/adapters/README.md +30 -15
- package/adapters/claude-code/.claude-plugin/plugin.json +11 -5
- package/adapters/claude-code/README.md +110 -8
- package/adapters/claude-code/commands/check.md +4 -3
- package/adapters/claude-code/commands/goal.md +3 -2
- package/adapters/claude-code/commands/master.md +3 -2
- package/adapters/claude-code/commands/onboarding.md +21 -0
- package/adapters/claude-code/commands/ork.md +99 -27
- package/adapters/claude-code/hooks/hooks.json +6 -1
- package/adapters/claude-code/hooks/ork-guard.js +1 -1
- package/adapters/claude-code/hooks/ork-sensor.js +66 -0
- package/adapters/codex/skills/ork/SKILL.md +57 -0
- package/adapters/hermes/README.md +161 -0
- package/adapters/hermes/bin/ork-abrir-thread.sh +2 -0
- package/adapters/hermes/bin/ork-brain.sh +4 -0
- package/adapters/hermes/bin/ork-hitl-answer.py +82 -0
- package/adapters/hermes/bin/ork-maestro.sh +6 -0
- package/adapters/hermes/bin/ork-master-enviar.py +84 -0
- package/adapters/hermes/bin/ork-pulse-enviar.py +47 -0
- package/adapters/hermes/hermes.plugin.json +24 -4
- package/adapters/hermes/hitl-ingress/__init__.py +356 -0
- package/adapters/hermes/hitl-ingress/plugin.yaml +4 -0
- package/adapters/hermes/skills/orkastery-devmaster/SKILL.md +75 -22
- package/adapters/openclaw/README.md +136 -27
- package/adapters/openclaw/bin/ork-brain.sh +4 -0
- package/adapters/openclaw/construir.sh +19 -0
- package/adapters/openclaw/dist/hitl-ingress.js +199 -0
- package/adapters/openclaw/dist/index.js +437 -0
- package/adapters/openclaw/openclaw.plugin.json +39 -134
- package/adapters/openclaw/package.json +26 -0
- package/adapters/openclaw/src/hitl-ingress.ts +180 -0
- package/adapters/openclaw/src/index.ts +460 -0
- package/adapters/openclaw/src/tipos-openclaw.d.ts +58 -0
- package/adapters/openclaw/tsconfig.json +15 -0
- package/assets/docs/markdownlint-cli2.jsonc +22 -0
- package/assets/docs/padroes/documentacao-de-produto.md +133 -0
- package/assets/docs/padroes/roadmap-de-produto.md +98 -0
- package/assets/docs/produto/README.md +14 -0
- package/assets/docs/produto/_modelo-feature.md +59 -0
- package/assets/docs/roadmap/README.md +14 -0
- package/assets/docs/roadmap/_modelo-item.md +73 -0
- package/assets/orkmind-native-schema.json +32 -0
- package/assets/orkmind_bridge.py +357 -0
- package/assets/orkmind_fixture.py +67 -0
- package/assets/orkmind_prospective.py +138 -0
- package/assets/reference-tariffs-i07.json +23 -0
- package/dist/adapters/claude-bg.js +488 -39
- package/dist/adapters/codex-controller-sensor.js +449 -0
- package/dist/adapters/codex-controller-worker.js +361 -0
- package/dist/adapters/codex-controller.js +252 -0
- package/dist/adapters/codex-events.js +205 -0
- package/dist/adapters/codex-question.js +26 -0
- package/dist/adapters/codex-runner.js +133 -0
- package/dist/adapters/codex.js +394 -0
- package/dist/agents-md.js +84 -0
- package/dist/auditoria.js +12 -9
- package/dist/auditrun.js +5 -4
- package/dist/board.js +174 -23
- package/dist/branch-de-estado.js +136 -0
- package/dist/canarios-hitl.js +177 -0
- package/dist/canarios-i43.js +543 -0
- package/dist/canarios-pulse.js +147 -0
- package/dist/canarios-sensores.js +129 -0
- package/dist/canarios.js +111 -2
- package/dist/catalogo.js +9 -0
- package/dist/ci.js +223 -0
- package/dist/ciclos.js +2 -1
- package/dist/claim-lint.js +64 -0
- package/dist/claims.js +60 -0
- package/dist/company-brain-capture.js +195 -0
- package/dist/company-brain-cli.js +123 -0
- package/dist/company-brain-client.js +61 -0
- package/dist/company-brain-contract.js +166 -0
- package/dist/company-brain-journal.js +192 -0
- package/dist/company-brain-mcp.js +33 -0
- package/dist/company-brain-migration.js +60 -0
- package/dist/company-brain-source.js +177 -0
- package/dist/company-brain-worker.js +18 -0
- package/dist/conducao-texto.js +61 -0
- package/dist/conducao.js +876 -0
- package/dist/contrato-publico.js +39 -0
- package/dist/creation-operation-store.js +251 -0
- package/dist/creation-operation.js +148 -0
- package/dist/decisao-autonoma.js +183 -0
- package/dist/delegation.js +79 -0
- package/dist/demo.js +120 -0
- package/dist/docs.js +828 -0
- package/dist/doctor.js +184 -25
- package/dist/entrega-pr.js +117 -0
- package/dist/escopo-escrita.js +71 -0
- package/dist/estado-thread.js +222 -0
- package/dist/evalrunner.js +12 -0
- package/dist/fabrica-estado.js +329 -0
- package/dist/fabrica-publicar.js +76 -0
- package/dist/fix.js +22 -1
- package/dist/gates.js +59 -11
- package/dist/handoff.js +39 -23
- package/dist/hitl-canais.js +333 -0
- package/dist/hitl-classificacao.js +131 -0
- package/dist/hitl-contract.js +465 -0
- package/dist/hitl-estado.js +136 -0
- package/dist/hitl-gates.js +554 -0
- package/dist/hitl-ingress-receipt.js +382 -0
- package/dist/hitl-local-atestado.js +57 -0
- package/dist/hitl-local-receipt.js +328 -0
- package/dist/hitl-local.js +143 -0
- package/dist/hitl-lock.js +139 -0
- package/dist/hitl-lote.js +223 -0
- package/dist/hitl-native-offer.js +97 -0
- package/dist/hitl-native.js +65 -0
- package/dist/hitl-presentation.js +209 -0
- package/dist/hitl-public-receipt.js +176 -0
- package/dist/hitl-resumo.js +209 -0
- package/dist/hitl-sessions.js +475 -0
- package/dist/hitl.js +711 -0
- package/dist/horario.js +269 -0
- package/dist/hosts.js +198 -22
- package/dist/index.js +1741 -94
- package/dist/indice.js +99 -0
- package/dist/init.js +28 -3
- package/dist/integracoes-locais.js +17 -0
- package/dist/leases.js +65 -21
- package/dist/ledger-stats.js +272 -0
- package/dist/ledger.js +109 -2
- package/dist/licoes.js +221 -0
- package/dist/liveness.js +218 -0
- package/dist/maestro-actions.js +51 -0
- package/dist/maestro-authority.js +152 -0
- package/dist/maestro-cli.js +97 -0
- package/dist/maestro-contract.js +55 -0
- package/dist/maestro-discovery.js +132 -0
- package/dist/maestro-runtime.js +268 -0
- package/dist/maestro-snapshot.js +86 -0
- package/dist/maestro-sources.js +224 -0
- package/dist/manifest.js +165 -6
- package/dist/maquina.js +102 -0
- package/dist/master-audit.js +101 -0
- package/dist/master-batch.js +43 -0
- package/dist/master-digest.js +149 -0
- package/dist/master-migracao.js +155 -0
- package/dist/master.js +361 -77
- package/dist/mcp-artifacts.js +234 -0
- package/dist/mcp-git.js +433 -0
- package/dist/mcp-install.js +310 -0
- package/dist/mcp-maestro.js +25 -0
- package/dist/mcp-server.js +469 -0
- package/dist/mcp-ship.js +436 -0
- package/dist/mcp-verify.js +173 -0
- package/dist/memoria-humana.js +207 -0
- package/dist/memoria.js +314 -78
- package/dist/memory-migration.js +279 -0
- package/dist/memory-prospective.js +148 -0
- package/dist/modos-migracao.js +191 -0
- package/dist/modos.js +151 -15
- package/dist/monitor-lock.js +110 -0
- package/dist/objective.js +449 -0
- package/dist/ocupacao.js +267 -0
- package/dist/onboarding.js +303 -0
- package/dist/orkmind.js +389 -90
- package/dist/orquestracao.js +96 -69
- package/dist/phase.js +612 -151
- package/dist/playbook-capabilities.js +150 -0
- package/dist/playbook-contracts.js +188 -0
- package/dist/policies.js +72 -0
- package/dist/portfolio-context.js +61 -0
- package/dist/portfolio.js +180 -0
- package/dist/preflight.js +207 -0
- package/dist/process-audit.js +112 -0
- package/dist/project-state.js +93 -0
- package/dist/prompts.js +25 -5
- package/dist/prova-minima.js +112 -0
- package/dist/pulse-cadencia.js +163 -0
- package/dist/pulse-consentimento.js +260 -0
- package/dist/pulse-delivery.js +323 -0
- package/dist/pulse-resposta.js +542 -0
- package/dist/pulse.js +262 -0
- package/dist/ratelimit.js +35 -2
- package/dist/recall.js +80 -3
- package/dist/redacao-saida.js +45 -0
- package/dist/redacao-url.js +66 -0
- package/dist/retry.js +640 -118
- package/dist/roadmap-reservas.js +243 -0
- package/dist/runtime-ambiente.js +38 -0
- package/dist/runtime-context.js +215 -0
- package/dist/runtime-profiles.js +869 -0
- package/dist/runtimes.js +103 -0
- package/dist/sandbox.js +9 -0
- package/dist/session-events.js +203 -0
- package/dist/session-watcher-claude.js +637 -0
- package/dist/session-watcher.js +700 -0
- package/dist/sessoes-adopt.js +154 -0
- package/dist/sessoes-inventario.js +159 -0
- package/dist/sessoes.js +16 -35
- package/dist/setup.js +691 -0
- package/dist/ship.js +168 -19
- package/dist/slug.js +1 -1
- package/dist/thread-close.js +115 -0
- package/dist/thread.js +142 -28
- package/dist/tokens.js +1 -1
- package/dist/util.js +4 -2
- package/dist/verify-sandbox.js +265 -0
- package/dist/verify.js +270 -25
- package/dist/versao.js +60 -0
- package/dist/worktree.js +31 -2
- package/dist/write-activation.js +291 -0
- package/dist/yaml.js +5 -1
- package/eval/casos/master-metrics.json +1 -1
- package/eval/casos/onboarding.json +102 -0
- package/eval/casos/orkastery-bootstrap.json +37 -3
- package/eval/casos/scope-check-capability-map.json +45 -7
- package/eval/casos/ship-release.json +3 -3
- package/eval/fixtures/b0-slug-e-modos/caso.json +35 -22
- package/eval/fixtures/b2-master-log/caso.json +1 -1
- package/eval/fixtures/fx-adapter-editado-detectado/caso.json +19 -0
- package/eval/fixtures/fx-auto-quiet/caso.json +11 -0
- package/eval/fixtures/fx-blanket-approve/caso.json +10 -0
- package/eval/fixtures/fx-check-runtime-cruzado/caso.json +20 -0
- package/eval/fixtures/fx-codex-dry/caso.json +18 -0
- package/eval/fixtures/fx-donewhen-executavel/caso.json +15 -0
- package/eval/fixtures/fx-estado-dividido/caso.json +15 -0
- package/eval/fixtures/fx-fase-orfa/caso.json +116 -0
- package/eval/fixtures/fx-happy/caso.json +6 -4
- package/eval/fixtures/fx-hitl-latency/caso.json +20 -0
- package/eval/fixtures/fx-indice-reversao/caso.json +25 -0
- package/eval/fixtures/fx-listagem-abertas/caso.json +16 -0
- package/eval/fixtures/fx-maestro-bootstrap/caso.json +21 -0
- package/eval/fixtures/fx-modo-aposentado-escritor/caso.json +21 -0
- package/eval/fixtures/fx-modo-aposentado-leitor/caso.json +16 -0
- package/eval/fixtures/fx-objective-oscillation/caso.json +14 -0
- package/eval/fixtures/fx-omnicanal/caso.json +17 -0
- package/eval/fixtures/fx-sensores-runtime/caso.json +17 -0
- package/monitor/company-brain.cjs +17 -0
- package/monitor/pulse-scope.cjs +45 -0
- package/monitor/pulse.cron +18 -0
- package/monitor/varredura-pulse.sh +27 -0
- package/package.json +28 -7
- package/references/definition-of-done.md +2 -2
- package/schemas/claims.schema.json +25 -0
- package/schemas/company-brain.schema.json +1158 -0
- package/schemas/creation-operation.schema.json +365 -0
- package/schemas/maestro-snapshot.schema.json +2716 -0
- package/skills/README.md +3 -3
- package/skills/core/onboarding/SKILL.md +50 -0
- package/skills/core/orkastery-bootstrap/SKILL.md +95 -65
- package/skills/core/thread-state/SKILL.md +3 -1
- package/skills/governance/decision-triage/SKILL.md +4 -4
- package/skills/governance/narrative-guardian/SKILL.md +1 -1
- package/skills/governance/roadmap-keeper/SKILL.md +1 -1
- package/skills/governance/scope-check-capability-map/SKILL.md +17 -7
- package/skills/observability/thread-tracing/SKILL.md +1 -1
- package/skills/phases/check-quality/SKILL.md +2 -2
- package/skills/phases/goal-definition/SKILL.md +1 -1
- package/skills/phases/master-metrics/SKILL.md +9 -7
- package/skills/phases/plan-specification/SKILL.md +1 -1
- package/skills/phases/ship-release/SKILL.md +1 -1
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Entrega uma página de digest com escolhas que chegam ao Hermes como texto humano."""
|
|
3
|
+
import argparse
|
|
4
|
+
import asyncio
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
import re
|
|
9
|
+
import sys
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def validar(payload, target):
|
|
13
|
+
if not re.fullmatch(r'telegram:-?\d+(?::\d+)?', target):
|
|
14
|
+
raise ValueError('destino Telegram explícito obrigatório')
|
|
15
|
+
if not isinstance(payload, dict) or payload.get('contrato') != 'ork.master-digest-page/v1':
|
|
16
|
+
raise ValueError('contrato de página inválido')
|
|
17
|
+
texto = payload.get('texto')
|
|
18
|
+
if not isinstance(texto, str) or not texto.strip() or len(texto.encode('utf-16-le')) // 2 > 3900:
|
|
19
|
+
raise ValueError('texto ausente ou maior que uma página')
|
|
20
|
+
opcoes = payload.get('opcoes')
|
|
21
|
+
if not isinstance(opcoes, list) or len(opcoes) > 12:
|
|
22
|
+
raise ValueError('opções inválidas')
|
|
23
|
+
if any(not isinstance(opcao, str) or not opcao.strip() or len(opcao) > 200 for opcao in opcoes):
|
|
24
|
+
raise ValueError('opção inválida')
|
|
25
|
+
partes = target.split(':')
|
|
26
|
+
return partes[1], int(partes[2]) if len(partes) == 3 else None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
async def entregar(payload, target, bot):
|
|
30
|
+
chat, topico = validar(payload, target)
|
|
31
|
+
# Teclado de texto usa o fluxo humano já autenticado pelo gateway do Hermes.
|
|
32
|
+
# Não depende de um callback desconhecido nem executa comando ao enviar o digest.
|
|
33
|
+
argumentos = {'chat_id': chat, 'text': payload['texto']}
|
|
34
|
+
if topico is not None:
|
|
35
|
+
argumentos['message_thread_id'] = topico
|
|
36
|
+
if payload['opcoes']:
|
|
37
|
+
argumentos['reply_markup'] = {
|
|
38
|
+
'keyboard': [[{'text': opcao}] for opcao in payload['opcoes']],
|
|
39
|
+
'resize_keyboard': True,
|
|
40
|
+
'one_time_keyboard': True,
|
|
41
|
+
'input_field_placeholder': 'Escolha a classe para ratificar a proposta',
|
|
42
|
+
}
|
|
43
|
+
resposta = await bot.send_message(**argumentos)
|
|
44
|
+
recibo = getattr(resposta, 'message_id', None)
|
|
45
|
+
if not isinstance(recibo, int) or recibo <= 0:
|
|
46
|
+
raise ValueError('Telegram não confirmou a mensagem')
|
|
47
|
+
return {'success': True, 'message_id': recibo, 'platform': 'telegram'}
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
async def enviar_pelo_hermes(payload, target):
|
|
51
|
+
validar(payload, target)
|
|
52
|
+
home = Path(os.environ.get('HERMES_HOME', Path.home() / '.hermes'))
|
|
53
|
+
agent = Path(os.environ.get('HERMES_AGENT_DIR', home / 'hermes-agent'))
|
|
54
|
+
sys.path.insert(0, str(agent))
|
|
55
|
+
from dotenv import load_dotenv
|
|
56
|
+
load_dotenv(home / '.env', override=False)
|
|
57
|
+
from gateway.config import load_gateway_config, Platform
|
|
58
|
+
from telegram import Bot
|
|
59
|
+
config = load_gateway_config().platforms.get(Platform.TELEGRAM)
|
|
60
|
+
if not config or not config.enabled or not config.token:
|
|
61
|
+
raise ValueError('Telegram não configurado no Hermes')
|
|
62
|
+
async with Bot(token=config.token) as bot:
|
|
63
|
+
return await entregar(payload, target, bot)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def main():
|
|
67
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
68
|
+
parser.add_argument('--target', required=True)
|
|
69
|
+
args = parser.parse_args()
|
|
70
|
+
try:
|
|
71
|
+
bruto = sys.stdin.read(65537)
|
|
72
|
+
if len(bruto.encode('utf-8')) > 65536:
|
|
73
|
+
raise ValueError('payload excede o limite')
|
|
74
|
+
recibo = asyncio.run(enviar_pelo_hermes(json.loads(bruto), args.target))
|
|
75
|
+
print(json.dumps(recibo))
|
|
76
|
+
return 0
|
|
77
|
+
except Exception as exc:
|
|
78
|
+
# Configuração, credenciais e resposta bruta nunca entram no erro de transporte.
|
|
79
|
+
print(json.dumps({'success': False, 'erro': type(exc).__name__}))
|
|
80
|
+
return 1
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
if __name__ == '__main__':
|
|
84
|
+
sys.exit(main())
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Transporte de texto do pulse pela API de mensagem do Hermes, sem inferência."""
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
import re
|
|
8
|
+
import sys
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def texto_seguro(message):
|
|
12
|
+
message = re.sub(r'(?i)MEDIA\s*:', 'MEDIA [texto]:', message)
|
|
13
|
+
return re.sub(r'(?i)\[\[(?:audio_as_voice|as_document)\]\]', '[diretiva em texto]', message)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def main():
|
|
17
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
18
|
+
parser.add_argument('--target', required=True, help='Destino explícito: telegram:chat_id[:topic_id]')
|
|
19
|
+
parser.add_argument('--message', required=True)
|
|
20
|
+
args = parser.parse_args()
|
|
21
|
+
if not re.fullmatch(r'telegram:-?\d+(?::\d+)?', args.target):
|
|
22
|
+
parser.error('use identificador explícito do chat Telegram autorizado')
|
|
23
|
+
home = Path(os.environ.get('HERMES_HOME', Path.home() / '.hermes'))
|
|
24
|
+
agent = Path(os.environ.get('HERMES_AGENT_DIR', home / 'hermes-agent'))
|
|
25
|
+
sys.path.insert(0, str(agent))
|
|
26
|
+
try:
|
|
27
|
+
from dotenv import load_dotenv
|
|
28
|
+
load_dotenv(home / '.env', override=False)
|
|
29
|
+
from tools.send_message_tool import send_message_tool
|
|
30
|
+
# Conteúdo do runtime é texto. Não pode pedir ao host que anexe arquivo local.
|
|
31
|
+
message = texto_seguro(args.message)
|
|
32
|
+
result = send_message_tool({'action': 'send', 'target': args.target, 'message': message})
|
|
33
|
+
data = json.loads(result) if isinstance(result, str) else result
|
|
34
|
+
if (not isinstance(data, dict) or data.get('success') is not True or data.get('skipped')
|
|
35
|
+
or not (data.get('message_id') or data.get('message_ids'))):
|
|
36
|
+
print(json.dumps({'success': False, 'erro': 'Hermes não confirmou a entrega'}))
|
|
37
|
+
return 1
|
|
38
|
+
print(json.dumps({k: data[k] for k in ['success', 'message_id', 'message_ids', 'platform'] if k in data}))
|
|
39
|
+
return 0
|
|
40
|
+
except Exception as exc:
|
|
41
|
+
# Não imprimir configuração, credencial ou resposta bruta de uma API.
|
|
42
|
+
print(json.dumps({'success': False, 'erro': type(exc).__name__}))
|
|
43
|
+
return 1
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
if __name__ == '__main__':
|
|
47
|
+
sys.exit(main())
|
|
@@ -5,17 +5,37 @@
|
|
|
5
5
|
"description": "Conducao de looping threads em 6 fases pelo nucleo `ork`. O Hermes vira podio: le a #TAG do pedido, chama o `ork` e apresenta os gates ao humano.",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"kind": "skill-router",
|
|
8
|
-
"skills": [
|
|
8
|
+
"skills": [
|
|
9
|
+
"./skills/orkastery-devmaster"
|
|
10
|
+
],
|
|
9
11
|
"bin": {
|
|
10
|
-
"
|
|
12
|
+
"ork_maestro": "./bin/ork-maestro.sh",
|
|
13
|
+
"ork-abrir-thread": "./bin/ork-abrir-thread.sh",
|
|
14
|
+
"ork_brain": "./bin/ork-brain.sh",
|
|
15
|
+
"ork_gate_answer": "./bin/ork-hitl-answer.py",
|
|
16
|
+
"ork_session_answer": "./bin/ork-hitl-answer.py"
|
|
11
17
|
},
|
|
12
18
|
"requires": {
|
|
13
19
|
"ork": ">=0.1.0",
|
|
14
20
|
"comando": "{{ork_bin}}"
|
|
15
21
|
},
|
|
16
22
|
"conduction": {
|
|
17
|
-
"tags": [
|
|
23
|
+
"tags": [
|
|
24
|
+
"#Classic",
|
|
25
|
+
"#Maestro",
|
|
26
|
+
"#Auto",
|
|
27
|
+
"#Fast"
|
|
28
|
+
],
|
|
29
|
+
"aposentadas": [
|
|
30
|
+
"#Look",
|
|
31
|
+
"#Ork"
|
|
32
|
+
],
|
|
18
33
|
"parse": "ork modos --do-pedido \"<pedido>\"",
|
|
19
|
-
"nota": "O host transporta a tag. A validacao contra conduction.allowed_modes e do nucleo."
|
|
34
|
+
"nota": "O host transporta a tag. A validacao contra conduction.allowed_modes e do nucleo. Uma #TAG aposentada recebe recusa tipada modo.aposentado do nucleo, com saida != 0; o host nao a traduz nem a ignora."
|
|
35
|
+
},
|
|
36
|
+
"onboarding": {
|
|
37
|
+
"pauta": "ork onboarding",
|
|
38
|
+
"estado": "ork onboarding show --json",
|
|
39
|
+
"nota": "Etapas e validação são do núcleo; valores secretos somente em ~/.hermes/.env."
|
|
20
40
|
}
|
|
21
41
|
}
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
"""Ingresso nativo Hermes: callback síncrono, I/O limitado no loop do host.
|
|
2
|
+
|
|
3
|
+
D12: este host é o canal `hermes`. O canal entra no corpo assinado do envelope, não
|
|
4
|
+
num campo solto: Hermes e OpenClaw compartilham transporte, mas têm chaves e identidades
|
|
5
|
+
próprias; sem o canal dentro do HMAC o recibo perderia essa proveniência.
|
|
6
|
+
"""
|
|
7
|
+
import asyncio
|
|
8
|
+
import hashlib
|
|
9
|
+
import hmac
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
|
+
import re
|
|
14
|
+
import subprocess
|
|
15
|
+
import time
|
|
16
|
+
from datetime import datetime, timezone
|
|
17
|
+
|
|
18
|
+
CANAL = 'hermes'
|
|
19
|
+
COMMAND = re.compile(r'^/ork (gate|session) ([a-zA-Z0-9][a-zA-Z0-9._-]{0,79}) ([a-zA-Z0-9][a-zA-Z0-9._-]{0,79}) ([\s\S]{1,4096})$')
|
|
20
|
+
NATIVE_COMMAND = re.compile(r'^/ork (offer|gate|session) ([a-zA-Z0-9][a-zA-Z0-9._-]{0,79}) ([a-zA-Z0-9][a-zA-Z0-9._-]{0,79})(?: ([\s\S]{1,4096}))?$')
|
|
21
|
+
# I-41 (GO-FIX 1): as duas formas da resposta ao resumo do pulse, que o dono digita sem barra e
|
|
22
|
+
# sem identificador: "P4EJ a" ao resumo e "1a 2c" as perguntas. As MESMAS fontes vivem no nucleo
|
|
23
|
+
# (GRAMATICA_DO_PULSE, core/src/pulse-resposta.ts) e um teste do nucleo confere que continuam
|
|
24
|
+
# iguais. O codigo comeca por letra e tem digito: conversa comum nao tem esta forma, e ele
|
|
25
|
+
# nunca se confunde com a resposta ao lote, que comeca pelo numero da pergunta.
|
|
26
|
+
PULSE_CONSENT = re.compile(r'^[ \t]*(?=[A-HJKMNP-TV-Z][2-9A-HJKMNP-TV-Z]{0,2}[2-9])[A-HJKMNP-TV-Z][2-9A-HJKMNP-TV-Z]{3}[ \t]+[^\r\n]{1,40}$', re.IGNORECASE)
|
|
27
|
+
PULSE_LOTE = re.compile(r'^[ \t]*[0-9]{1,2}[ \t]*[a-zA-Z](?:[ \t,;]*[0-9]{1,2}[ \t]*[a-zA-Z])*[ \t]*[.!]?[ \t]*$')
|
|
28
|
+
# I-50 (RM-039): a tag da cadencia do resumo, sozinha na mensagem. Comeca por '#': nunca se
|
|
29
|
+
# confunde com as outras duas formas, e tag no meio de uma frase continua indo para o assistente.
|
|
30
|
+
PULSE_CADENCIA = re.compile(r'^[ \t]*#OrkPulse(?:On(?:-(?:15|30|60)m)?|Off)[ \t]*[.!]?[ \t]*$', re.IGNORECASE)
|
|
31
|
+
# O endereco do pulse no corpo assinado: nem thread nem pedido. Quem traduz e o nucleo.
|
|
32
|
+
PULSE_ALVO, PULSE_ENDERECO = 'pulse', 'resposta'
|
|
33
|
+
CORE_TIMEOUT = 10
|
|
34
|
+
CONFIRM_TIMEOUT = 3
|
|
35
|
+
MAX_ENTRIES = 1024
|
|
36
|
+
MAX_PENDING = 16
|
|
37
|
+
CONFIRM_TEXT = {
|
|
38
|
+
'gate': 'Orkastery: resposta ao gate registrada pelo núcleo.',
|
|
39
|
+
'session': 'Orkastery: resposta entregue à sessão pelo núcleo.',
|
|
40
|
+
}
|
|
41
|
+
logger = logging.getLogger(__name__)
|
|
42
|
+
# Só metadados: nunca guardar resposta, chave, stdout ou exceção nestes registros.
|
|
43
|
+
_entries = {}
|
|
44
|
+
_tasks = set()
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _allowed(name, value):
|
|
48
|
+
return value in {s.strip() for s in os.environ.get(name, '').split(',') if s.strip()}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _status(entry=None, replay=False):
|
|
52
|
+
effect = entry['effect'] if entry else 'unconfirmed'
|
|
53
|
+
confirmation = entry['confirmation'] if entry else 'not_attempted'
|
|
54
|
+
return {'action': 'skip', 'reason': f'ork.hitl: effect={effect}; confirmation={confirmation}',
|
|
55
|
+
'effect': effect, 'confirmation': confirmation, 'replay': replay}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _is_pulse(text):
|
|
59
|
+
return '\x00' not in text and bool(PULSE_CONSENT.fullmatch(text) or PULSE_LOTE.fullmatch(text)
|
|
60
|
+
or PULSE_CADENCIA.fullmatch(text))
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _receipt_ok(receipt, operation, pedido):
|
|
64
|
+
if operation == 'pulse':
|
|
65
|
+
# O texto que volta ao dono e montado pelo nucleo; este adaptador so o transporta.
|
|
66
|
+
return (isinstance(receipt, dict) and receipt.get('ok') is True
|
|
67
|
+
and receipt.get('contrato') == 'ork.pulse-resposta/v1'
|
|
68
|
+
and isinstance(receipt.get('mensagem'), str) and bool(receipt['mensagem'])
|
|
69
|
+
and type(receipt.get('repetida')) is bool)
|
|
70
|
+
states = {'aprovado', 'recusado', 'aguardando'} if operation == 'gate' else {'entregue'}
|
|
71
|
+
return (isinstance(receipt, dict) and receipt.get('ok') is True
|
|
72
|
+
and receipt.get('pedidoId') == pedido and receipt.get('estado') in states
|
|
73
|
+
and type(receipt.get('repetida')) is bool
|
|
74
|
+
and (operation == 'gate' or isinstance(receipt.get('sessionId'), str)
|
|
75
|
+
and bool(receipt['sessionId'])))
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def _call_core(argv, envelope, root, env):
|
|
79
|
+
return subprocess.run(argv, input=json.dumps(envelope, ensure_ascii=False), cwd=root,
|
|
80
|
+
env=env, text=True, encoding='utf-8', capture_output=True,
|
|
81
|
+
timeout=CORE_TIMEOUT, check=False)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
async def _deliver(entry, msg, operation, pedido, argv, envelope, root, env):
|
|
85
|
+
try:
|
|
86
|
+
# A espera no executor também tem orçamento. Cancelamento não prova rollback.
|
|
87
|
+
entry['effect'] = 'unknown'
|
|
88
|
+
result = await asyncio.wait_for(
|
|
89
|
+
asyncio.to_thread(_call_core, argv, envelope, root, env), CORE_TIMEOUT + 1)
|
|
90
|
+
receipt = json.loads(result.stdout) if result.returncode == 0 else None
|
|
91
|
+
if not _receipt_ok(receipt, operation, pedido):
|
|
92
|
+
return
|
|
93
|
+
entry['effect'] = 'confirmed'
|
|
94
|
+
if receipt['repetida']:
|
|
95
|
+
# Após restart não sabemos se o canal já recebeu. Nunca reenviar às cegas.
|
|
96
|
+
entry['confirmation'] = 'unknown'
|
|
97
|
+
return
|
|
98
|
+
entry['confirmation'] = 'unknown'
|
|
99
|
+
reply = receipt['mensagem'] if operation == 'pulse' else CONFIRM_TEXT[operation]
|
|
100
|
+
sent = await asyncio.wait_for(msg.reply_text(
|
|
101
|
+
reply, do_quote=False, parse_mode=None,
|
|
102
|
+
read_timeout=CONFIRM_TIMEOUT, write_timeout=CONFIRM_TIMEOUT,
|
|
103
|
+
connect_timeout=CONFIRM_TIMEOUT, pool_timeout=CONFIRM_TIMEOUT), CONFIRM_TIMEOUT)
|
|
104
|
+
from telegram import Message
|
|
105
|
+
if (isinstance(sent, Message) and type(sent.message_id) is int and sent.message_id > 0
|
|
106
|
+
and sent.chat.id == msg.chat.id and sent.from_user.id == msg.get_bot().id
|
|
107
|
+
and sent.from_user.is_bot is True and sent.text == reply):
|
|
108
|
+
entry['confirmation'] = 'sent'
|
|
109
|
+
except (Exception, asyncio.CancelledError):
|
|
110
|
+
# Timeout/exceção/recibo inválido: efeito pode existir; canal pode ter recebido.
|
|
111
|
+
# Nunca ecoar detalhes do CLI/PTB, nem transformar incerteza em sucesso.
|
|
112
|
+
pass
|
|
113
|
+
finally:
|
|
114
|
+
entry['done'] = True
|
|
115
|
+
logger.info('ork.hitl: ref=%s effect=%s confirmation=%s',
|
|
116
|
+
entry['ref'], entry['effect'], entry['confirmation'])
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _ingress(*, event, gateway, **_kwargs):
|
|
120
|
+
text = getattr(event, 'text', '')
|
|
121
|
+
if not isinstance(text, str):
|
|
122
|
+
return None
|
|
123
|
+
pulse = not text.startswith('/ork ') and _is_pulse(text)
|
|
124
|
+
if not text.startswith('/ork ') and not pulse:
|
|
125
|
+
return None
|
|
126
|
+
if getattr(getattr(event, 'source', None), 'platform', None) and event.source.platform.value != 'telegram':
|
|
127
|
+
# O resumo do pulse sai pelo Telegram; a resposta a ele so e reconhecida la.
|
|
128
|
+
return None if pulse else _native_ingress(event, gateway, _kwargs.get('session_store'))
|
|
129
|
+
# invoke_hook instalado NÃO resolve coroutine. Todo caminho /ork, e a resposta ao pulse, retorna skip.
|
|
130
|
+
try:
|
|
131
|
+
from gateway.platforms.base import MessageEvent
|
|
132
|
+
from telegram import Message
|
|
133
|
+
if not isinstance(event, MessageEvent) or getattr(event, 'internal', False):
|
|
134
|
+
return _status()
|
|
135
|
+
msg, source = event.raw_message, event.source
|
|
136
|
+
if (not isinstance(msg, Message) or source.platform.value != 'telegram'
|
|
137
|
+
or gateway._is_user_authorized(source) is not True):
|
|
138
|
+
return _status()
|
|
139
|
+
user, chat = str(msg.from_user.id), str(msg.chat.id)
|
|
140
|
+
bot = os.environ.get('ORK_HITL_TELEGRAM_BOT_ID', '')
|
|
141
|
+
if (msg.from_user.is_bot is not False or not bot or str(msg.get_bot().id) != bot
|
|
142
|
+
or user == bot or getattr(msg, 'forward_origin', None) or getattr(msg, 'sender_chat', None)
|
|
143
|
+
or str(source.user_id) != user or str(source.chat_id) != chat
|
|
144
|
+
or type(msg.message_id) is not int or msg.message_id <= 0
|
|
145
|
+
or str(event.message_id) != str(msg.message_id) or msg.text != text
|
|
146
|
+
or not _allowed('ORK_HITL_TELEGRAM_USERS', user)
|
|
147
|
+
or not _allowed('ORK_HITL_TELEGRAM_CHATS', chat)):
|
|
148
|
+
return _status()
|
|
149
|
+
if pulse:
|
|
150
|
+
# O dono respondeu ao resumo ou ao lote. O texto inteiro e a resposta, e o endereco
|
|
151
|
+
# assinado e o do pulse: este adaptador nao conhece pedido e nao escolhe nada.
|
|
152
|
+
operation, thread, pedido, answer = 'pulse', PULSE_ALVO, PULSE_ENDERECO, text
|
|
153
|
+
else:
|
|
154
|
+
match = COMMAND.fullmatch(text)
|
|
155
|
+
if not match or '\x00' in text:
|
|
156
|
+
return _status()
|
|
157
|
+
operation, thread, pedido, answer = match.groups()
|
|
158
|
+
now = datetime.now(timezone.utc)
|
|
159
|
+
age = (now - msg.date).total_seconds()
|
|
160
|
+
# FX1: este canal assina com a SUA chave. ORK_HITL_INGRESS_KEY fica so para o
|
|
161
|
+
# envelope v1 legado, que este adaptador nao produz mais.
|
|
162
|
+
key = os.environ.get('ORK_HITL_INGRESS_KEY_HERMES', '')
|
|
163
|
+
root = os.environ.get('ORK_HITL_ROOT', '')
|
|
164
|
+
if not 0 <= age <= 60 or len(key.encode()) < 32 or not os.path.isabs(root):
|
|
165
|
+
return _status()
|
|
166
|
+
envelope = {'resposta': answer, 'canal': CANAL, 'origem': 'telegram', 'por': f'telegram:{user}',
|
|
167
|
+
'mensagem': f'telegram:{chat}:{msg.message_id}',
|
|
168
|
+
'recebidoEm': msg.date.astimezone(timezone.utc).isoformat(timespec='milliseconds').replace('+00:00', 'Z')}
|
|
169
|
+
# FX5: `conta` tem posicao fixa no corpo v2, e e `None` neste canal: a allowlist do
|
|
170
|
+
# Hermes e de usuario e chat, nao de conta. Declarar conta aqui seria recusado pelo
|
|
171
|
+
# nucleo, e omitir a posicao mudaria a aridade do corpo assinado.
|
|
172
|
+
body = ['ork.hitl-answer/v2', thread, pedido, CANAL, None, envelope['origem'], envelope['por'],
|
|
173
|
+
envelope['mensagem'], envelope['recebidoEm'], answer]
|
|
174
|
+
envelope['prova'] = hmac.new(key.encode(), json.dumps(body, ensure_ascii=False, separators=(',', ':')).encode(), hashlib.sha256).hexdigest()
|
|
175
|
+
# Cada subcomando tem a sua flag de stdin: `gate answer` exige --resposta-stdin e
|
|
176
|
+
# `sessions answer` exige --stdin. Mandar a mesma flag para os dois fazia toda
|
|
177
|
+
# resposta de sessao ser recusada pelo proprio CLI, antes de qualquer conferencia.
|
|
178
|
+
if operation == 'pulse':
|
|
179
|
+
argv = [os.environ.get('ORK_BIN', 'ork'), 'pulse', 'responder', '--resposta-stdin',
|
|
180
|
+
'--origem', 'telegram', '--canal', CANAL,
|
|
181
|
+
'--por', envelope['por'], '--mensagem', envelope['mensagem']]
|
|
182
|
+
else:
|
|
183
|
+
argv = [os.environ.get('ORK_BIN', 'ork'), 'gate' if operation == 'gate' else 'sessions',
|
|
184
|
+
'answer', thread, pedido,
|
|
185
|
+
'--resposta-stdin' if operation == 'gate' else '--stdin',
|
|
186
|
+
'--origem', 'telegram', '--canal', CANAL,
|
|
187
|
+
'--por', envelope['por'], '--mensagem', envelope['mensagem']]
|
|
188
|
+
# Usar o loop que possui o bot HTTP; nunca asyncio.run/thread com esse bot.
|
|
189
|
+
loop = asyncio.get_running_loop()
|
|
190
|
+
identity = (root, bot, chat, str(msg.message_id))
|
|
191
|
+
fingerprint = operation + ':' + envelope['prova']
|
|
192
|
+
previous = _entries.get(identity)
|
|
193
|
+
if previous:
|
|
194
|
+
return _status(previous, replay=True) if hmac.compare_digest(previous['fingerprint'], fingerprint) else _status()
|
|
195
|
+
for identity_old, old in list(_entries.items()):
|
|
196
|
+
if old['done'] and time.monotonic() >= old['expires']:
|
|
197
|
+
del _entries[identity_old]
|
|
198
|
+
if len(_entries) >= MAX_ENTRIES or len(_tasks) >= MAX_PENDING:
|
|
199
|
+
return _status()
|
|
200
|
+
entry = {'fingerprint': fingerprint, 'effect': 'pending', 'confirmation': 'not_attempted',
|
|
201
|
+
'done': False, 'expires': time.monotonic() + 61,
|
|
202
|
+
'ref': hashlib.sha256(json.dumps(identity).encode()).hexdigest()[:16]}
|
|
203
|
+
_entries[identity] = entry # Reserva antes de agendar, sem await no callback.
|
|
204
|
+
work = _deliver(entry, msg, operation, pedido, argv, envelope, root, dict(os.environ))
|
|
205
|
+
try:
|
|
206
|
+
task = loop.create_task(work)
|
|
207
|
+
except Exception:
|
|
208
|
+
work.close()
|
|
209
|
+
entry.update(effect='unconfirmed', done=True)
|
|
210
|
+
return _status(entry)
|
|
211
|
+
_tasks.add(task)
|
|
212
|
+
task.add_done_callback(_tasks.discard)
|
|
213
|
+
return _status(entry)
|
|
214
|
+
except Exception:
|
|
215
|
+
pass
|
|
216
|
+
return _status()
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def register(ctx):
|
|
220
|
+
ctx.register_hook('pre_gateway_dispatch', _ingress)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
async def _native_core(argv, root, envelope=None):
|
|
224
|
+
# Subprocesso assíncrono limitado: não prende executor thread no shutdown.
|
|
225
|
+
proc = await asyncio.create_subprocess_exec(*argv, cwd=root,
|
|
226
|
+
stdin=asyncio.subprocess.PIPE, stdout=asyncio.subprocess.PIPE, stderr=asyncio.subprocess.DEVNULL,
|
|
227
|
+
limit=65536)
|
|
228
|
+
try:
|
|
229
|
+
data = None if envelope is None else json.dumps(envelope, ensure_ascii=False).encode()
|
|
230
|
+
out, _ = await asyncio.wait_for(proc.communicate(data), CORE_TIMEOUT)
|
|
231
|
+
if proc.returncode or len(out) > 65536:
|
|
232
|
+
raise ValueError('native.core.unavailable')
|
|
233
|
+
return json.loads(out)
|
|
234
|
+
except BaseException:
|
|
235
|
+
if proc.returncode is None:
|
|
236
|
+
proc.kill()
|
|
237
|
+
await proc.wait()
|
|
238
|
+
raise
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
async def _native_deliver(entry, msg, match, binding, key, root):
|
|
242
|
+
operation, thread, pedido, answer = match.groups()
|
|
243
|
+
cli = os.environ.get('ORK_BIN', 'ork')
|
|
244
|
+
try:
|
|
245
|
+
compact = lambda v: json.dumps(v, ensure_ascii=False, separators=(',', ':'))
|
|
246
|
+
offer = dict(native=dict(binding, messageId=str(msg.id)),
|
|
247
|
+
recebidoEm=msg.created_at.astimezone(timezone.utc).isoformat(timespec='milliseconds').replace('+00:00', 'Z'))
|
|
248
|
+
body = ['ork.hitl-native-offer/v1', thread, pedido] + [offer['native'][k] for k in
|
|
249
|
+
['host', 'installationId', 'connectionId', 'sessionId', 'accountId', 'channelId', 'conversationId', 'personId', 'messageId']] + [offer['recebidoEm']]
|
|
250
|
+
offer['prova'] = hmac.new(key.encode(), compact(body).encode(), hashlib.sha256).hexdigest()
|
|
251
|
+
view = await _native_core([cli, 'gate', 'context', thread, pedido, '--native-offer-stdin'], root, offer)
|
|
252
|
+
p = view.get('pedido', {})
|
|
253
|
+
if (p.get('id') != pedido or p.get('thread') != thread or (operation != 'offer' and p.get('alvo', {}).get('tipo') != operation)
|
|
254
|
+
or not any(c.get('canal') == 'hermes' and c.get('transporte') == 'native' and c.get('estado') == 'disponivel' for c in view.get('canais', []))
|
|
255
|
+
or not re.fullmatch('[a-f0-9]{64}', view.get('contexto', ''))
|
|
256
|
+
or not re.fullmatch('[a-f0-9]{64}', view.get('pedidoSha256', ''))):
|
|
257
|
+
return
|
|
258
|
+
if operation == 'offer':
|
|
259
|
+
text = view.get('apresentacao', {}).get('mensagem')
|
|
260
|
+
if not isinstance(text, str):
|
|
261
|
+
return
|
|
262
|
+
# I-35: o prazo chega ao dono no fuso dele; nucleo de outra versao pode nao
|
|
263
|
+
# ter posto o prazo local na mensagem, entao o campo local vai junto.
|
|
264
|
+
prazo_local = view.get('prazoLocal')
|
|
265
|
+
if isinstance(prazo_local, str) and prazo_local and prazo_local not in text:
|
|
266
|
+
text += '\nPrazo (fuso do dono): ' + prazo_local
|
|
267
|
+
import discord
|
|
268
|
+
entry['effect'] = 'presented'
|
|
269
|
+
entry['confirmation'] = 'unknown'
|
|
270
|
+
text += '\nCanal nativo Hermes disponível nesta conversa.'
|
|
271
|
+
sent = await asyncio.wait_for(msg.reply(text, mention_author=False,
|
|
272
|
+
allowed_mentions=discord.AllowedMentions.none()), CONFIRM_TIMEOUT)
|
|
273
|
+
if isinstance(sent, discord.Message) and sent.channel.id == msg.channel.id and sent.content == text:
|
|
274
|
+
entry['confirmation'] = 'sent'
|
|
275
|
+
return
|
|
276
|
+
n = dict(binding, messageId=str(msg.id), context=view['contexto'],
|
|
277
|
+
pedidoSha256=view['pedidoSha256'], expiresAt=p['prazo'])
|
|
278
|
+
identity = [n[k] for k in ['host', 'installationId', 'connectionId', 'sessionId', 'channelId', 'accountId', 'conversationId', 'messageId']]
|
|
279
|
+
envelope = dict(origem='native', canal='hermes', conta=n['accountId'], native=n,
|
|
280
|
+
por='native:hermes:' + n['personId'], mensagem='native:' + hashlib.sha256(compact(identity).encode()).hexdigest(),
|
|
281
|
+
recebidoEm=msg.created_at.astimezone(timezone.utc).isoformat(timespec='milliseconds').replace('+00:00', 'Z'), resposta=answer)
|
|
282
|
+
body = ['ork.hitl-native/v1', thread, pedido] + [n[k] for k in ['host', 'installationId', 'connectionId', 'sessionId',
|
|
283
|
+
'accountId', 'channelId', 'conversationId', 'personId', 'messageId', 'context', 'pedidoSha256', 'expiresAt']] + [envelope[k] for k in ['origem', 'canal', 'conta', 'por', 'mensagem', 'recebidoEm', 'resposta']]
|
|
284
|
+
envelope['prova'] = hmac.new(key.encode(), compact(body).encode(), hashlib.sha256).hexdigest()
|
|
285
|
+
entry['effect'] = 'unknown'
|
|
286
|
+
receipt = await _native_core([cli, 'gate' if operation == 'gate' else 'sessions', 'answer', thread, pedido,
|
|
287
|
+
'--resposta-stdin' if operation == 'gate' else '--stdin', '--origem', 'native', '--canal', 'hermes',
|
|
288
|
+
'--conta', n['accountId'], '--por', envelope['por'], '--mensagem', envelope['mensagem']], root, envelope)
|
|
289
|
+
if not _receipt_ok(receipt, operation, pedido):
|
|
290
|
+
return
|
|
291
|
+
entry['effect'] = 'confirmed'
|
|
292
|
+
if receipt['repetida']:
|
|
293
|
+
entry['confirmation'] = 'unknown'
|
|
294
|
+
return
|
|
295
|
+
entry['confirmation'] = 'unknown'
|
|
296
|
+
import discord
|
|
297
|
+
sent = await asyncio.wait_for(msg.reply(CONFIRM_TEXT[operation], mention_author=False,
|
|
298
|
+
allowed_mentions=discord.AllowedMentions.none()), CONFIRM_TIMEOUT)
|
|
299
|
+
if isinstance(sent, discord.Message) and sent.channel.id == msg.channel.id and sent.content == CONFIRM_TEXT[operation]:
|
|
300
|
+
entry['confirmation'] = 'sent'
|
|
301
|
+
except (Exception, asyncio.CancelledError):
|
|
302
|
+
pass
|
|
303
|
+
finally:
|
|
304
|
+
entry['done'] = True
|
|
305
|
+
logger.info('ork.hitl.native: ref=%s effect=%s confirmation=%s', entry['ref'], entry['effect'], entry['confirmation'])
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _native_ingress(event, gateway, session_store):
|
|
309
|
+
"""Callback Discord do gateway instalado; terminal/ACP sem atestado fica indisponível."""
|
|
310
|
+
try:
|
|
311
|
+
from gateway.platforms.base import MessageEvent
|
|
312
|
+
import discord
|
|
313
|
+
if not isinstance(event, MessageEvent) or getattr(event, 'internal', False):
|
|
314
|
+
return _status()
|
|
315
|
+
msg, source = event.raw_message, event.source
|
|
316
|
+
binding = json.loads(os.environ.get('ORK_HITL_NATIVE_BINDING_HERMES', 'null'))
|
|
317
|
+
key, root = os.environ.get('ORK_HITL_NATIVE_KEY_HERMES', ''), os.environ.get('ORK_HITL_ROOT', '')
|
|
318
|
+
fields = {'host', 'installationId', 'connectionId', 'sessionId', 'accountId', 'channelId', 'conversationId', 'personId'}
|
|
319
|
+
if (not isinstance(binding, dict) or set(binding) != fields or any(not isinstance(v, str) or not v for v in binding.values())
|
|
320
|
+
or binding['host'] != 'hermes' or binding['channelId'] != 'discord' or source.platform.value != 'discord'
|
|
321
|
+
or not isinstance(msg, discord.Message) or source.is_bot is not False or msg.author.bot is not False
|
|
322
|
+
or msg.webhook_id is not None or msg.application_id is not None
|
|
323
|
+
or gateway._is_user_authorized(source) is not True or len(key.encode()) < 32 or not os.path.isabs(root)
|
|
324
|
+
or str(msg.author.id) != source.user_id or source.user_id != binding['personId']
|
|
325
|
+
or str(msg.channel.id) != source.chat_id or source.chat_id != binding['conversationId']
|
|
326
|
+
or str(getattr(msg.guild, 'id', '')) != binding['accountId']
|
|
327
|
+
or str(event.message_id) != str(msg.id) or msg.content != event.text):
|
|
328
|
+
return _status()
|
|
329
|
+
age = (datetime.now(timezone.utc) - msg.created_at).total_seconds()
|
|
330
|
+
if not 0 <= age <= 60:
|
|
331
|
+
return _status()
|
|
332
|
+
# API observada no SessionStore instalado; não cria nem reseta sessão por consulta.
|
|
333
|
+
entries = [s for s in session_store.list_sessions() if s.session_id == binding['sessionId']
|
|
334
|
+
and s.session_key == session_store._generate_session_key(source) and not s.suspended]
|
|
335
|
+
if len(entries) != 1:
|
|
336
|
+
return _status()
|
|
337
|
+
match = NATIVE_COMMAND.fullmatch(event.text)
|
|
338
|
+
if not match or '\x00' in event.text or (match[1] == 'offer' and match[4] is not None) or (match[1] != 'offer' and not match[4]):
|
|
339
|
+
return _status()
|
|
340
|
+
identity = (root, binding['connectionId'], binding['sessionId'], source.chat_id, str(msg.id))
|
|
341
|
+
if identity in _entries:
|
|
342
|
+
return _status(_entries[identity], replay=True)
|
|
343
|
+
for old_id, old in list(_entries.items()):
|
|
344
|
+
if old['done'] and time.monotonic() >= old['expires']:
|
|
345
|
+
del _entries[old_id]
|
|
346
|
+
if len(_entries) >= MAX_ENTRIES or len(_tasks) >= MAX_PENDING:
|
|
347
|
+
return _status()
|
|
348
|
+
entry = dict(effect='pending', confirmation='not_attempted', done=False, expires=time.monotonic() + 61,
|
|
349
|
+
ref=hashlib.sha256(json.dumps(identity).encode()).hexdigest()[:16])
|
|
350
|
+
_entries[identity] = entry
|
|
351
|
+
task = asyncio.get_running_loop().create_task(_native_deliver(entry, msg, match, binding, key, root))
|
|
352
|
+
_tasks.add(task)
|
|
353
|
+
task.add_done_callback(_tasks.discard)
|
|
354
|
+
return _status(entry)
|
|
355
|
+
except Exception:
|
|
356
|
+
return _status()
|