fruxon 2.0.2__tar.gz → 2.2.0__tar.gz
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.
- {fruxon-2.0.2 → fruxon-2.2.0}/HISTORY.md +60 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/PKG-INFO +9 -2
- {fruxon-2.0.2 → fruxon-2.2.0}/README.md +8 -1
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/__init__.py +30 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/_version.py +2 -2
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/checks/ledger.py +97 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/__init__.py +1 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/_shared.py +86 -0
- fruxon-2.2.0/src/fruxon/cli/agents_voice_calls.py +379 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/auth.py +82 -14
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/examples.py +15 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/knowledge_notices.py +4 -17
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/llm_providers.py +261 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/triggers.py +14 -1
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/triggers_ledger.py +388 -9
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/triggers_shape.py +207 -4
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/exceptions.py +15 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/fruxon.py +478 -20
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/models.py +495 -5
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/skills/fruxon-build-agent/SKILL.md +6 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/skills/fruxon-debug-trace/SKILL.md +17 -5
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/skills/fruxon-meet/SKILL.md +2 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/ui.py +45 -2
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_checks_ledger.py +97 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_cli.py +148 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_client.py +118 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_llm_provider_configs_cli.py +255 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_triggers.py +64 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_triggers_ledger.py +645 -2
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_triggers_shape.py +190 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_ui.py +175 -0
- fruxon-2.2.0/tests/test_voice_calls.py +288 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/.gitignore +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/LICENSE +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/pyproject.toml +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/__main__.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/_ssl.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/checks/__init__.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/checks/agent.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/checks/references.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/_crash.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/_schema.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/_stream.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/agents.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/agents_approvals.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/agents_budget.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/agents_channels.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/agents_draft.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/agents_evaluations.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/agents_executions.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/agents_improvements.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/agents_lifecycle.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/agents_memory.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/agents_reply_style.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/agents_revisions.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/agents_sandbox.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/agents_sandbox_test.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/agents_slots.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/agents_system_blocks.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/agents_tests.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/agents_topics.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/applications.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/applications_entry_points.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/assets.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/capabilities.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/completion.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/config.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/consult_pins.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/conversation_imports.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/dash.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/describe.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/doctor.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/environments.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/escalations.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/guides.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/integrations.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/keys.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/knowledge_bases.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/knowledge_documents.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/knowledge_sources.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/messages.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/metrics.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/participants.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/pipelines.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/secrets.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/skills.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/storage.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/tools.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/triggers_budget.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/triggers_check.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/triggers_ledger_stage_tests.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/triggers_questions.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/cli/workspaces.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/credentials.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/device_auth.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/doctor.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/flow_validation.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/output.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/params.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/sandbox_scenario.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/skills/__init__.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/skills/fruxon-agent-mode/SKILL.md +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/skills/fruxon-create-integration/SKILL.md +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/skills/fruxon-use-integrations/SKILL.md +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/telemetry.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/tui/__init__.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/tui/app.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/tui/demo.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/tui/links.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/tui/screens/__init__.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/tui/screens/agent.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/tui/screens/base.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/tui/screens/home.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/tui/screens/ledger.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/tui/screens/loader.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/tui/screens/pickers.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/tui/screens/trace.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/tui/source.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/tui/theme.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/tui/views.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/tui/widgets.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/update_check.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/src/fruxon/validation.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/.test_durations.json +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/__init__.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/conftest.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_actor.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_agent_lifecycle.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_agent_revisions_list.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_agent_slots.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_agents_check_cli.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_agents_revisions_cli.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_agents_sandbox.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_applications.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_approvals.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_assets.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_budgets.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_capabilities.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_channels.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_checks_agent.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_cli_base_url_routing.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_cli_crash.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_cli_root_flags.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_connect_nudge.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_consult.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_conversation_imports.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_credentials.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_dash_app.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_dash_cli.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_dash_client.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_dash_links.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_dash_source.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_describe.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_device_auth.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_docs_commands_exist.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_doctor.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_draft_bootstrap.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_draft_evaluate_cli.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_draft_pull_shape.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_draft_run_replay.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_draft_validate_cli.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_drafts.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_entry_points.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_environments.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_escalations.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_evaluations.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_execution_records.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_flow_validation.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_fruxon.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_guides.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_improvements.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_integration_configs_write_cli.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_integration_triggers_cli.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_integrations_authorize_cli.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_integrations_connect_cli.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_kb_trial_cli.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_knowledge_bases.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_knowledge_documents.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_knowledge_notices.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_knowledge_sources.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_llm_providers_connect_cli.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_managed_by.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_memory.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_messages.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_metrics.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_online_llm_config_refs.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_online_prefetch_refs.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_online_secret_refs.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_output.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_params.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_participants.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_participants_write.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_pipelines.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_reply_style.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_sandbox_scenario.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_schema.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_schema_fetch.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_secrets.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_skills.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_ssl.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_storage.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_system_blocks.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_system_blocks_cli.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_telemetry.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_test_chats.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_topics.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_trigger_budgets.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_triggers_check_cli.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_triggers_ledger_batch_views.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_triggers_ledger_stage_tests.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_triggers_questions.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_triggers_write.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_tui_boundary.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_update_check.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_validation.py +0 -0
- {fruxon-2.0.2 → fruxon-2.2.0}/tests/test_workspaces_cli.py +0 -0
|
@@ -1,5 +1,65 @@
|
|
|
1
1
|
# History
|
|
2
2
|
|
|
3
|
+
## Unreleased — whoami and the 403 hint read the role cap off the key
|
|
4
|
+
|
|
5
|
+
- On a server with fruxon-backend #1571, `fruxon whoami` and the 403
|
|
6
|
+
`insufficient_scope` hint take your role and the scopes in effect from
|
|
7
|
+
`/tokens/current` (`ownerRole`, `effectiveScopes`) instead of also reading
|
|
8
|
+
your membership, which needs `workspace:read`. A key without that scope now
|
|
9
|
+
gets its role named instead of "couldn't be read". Older servers keep the
|
|
10
|
+
membership read.
|
|
11
|
+
- **`fruxon whoami`** lists a service-account key's grant (`N scopes from its
|
|
12
|
+
grant`, and `effectiveScopes` under `current_key` in `-o json`) instead of
|
|
13
|
+
calling it "dead weight" for carrying no scopes of its own.
|
|
14
|
+
|
|
15
|
+
## Unreleased — say when your workspace role, not the key, refuses a call
|
|
16
|
+
|
|
17
|
+
- A 403 `insufficient_scope` for a scope the key already carries no longer
|
|
18
|
+
says to re-run `fruxon login --preset …`. Since fruxon-backend `f5aaf1250`
|
|
19
|
+
the server caps a personal key to its owner's current workspace role, so a
|
|
20
|
+
Viewer's key minted with `agents:write` is refused it, and a new key would be
|
|
21
|
+
refused the same way. The CLI now asks the refused key what it carries
|
|
22
|
+
(`/tokens/current`, then the owner's membership) and, when the key has the
|
|
23
|
+
scope, says the role is the limit and to ask a workspace Admin to raise it or
|
|
24
|
+
make the change. A key that lacks the scope, a service-account key, or a
|
|
25
|
+
lookup that fails keeps the preset hint.
|
|
26
|
+
- **`fruxon whoami`** labels its scope list as the key's own and adds a line
|
|
27
|
+
with your role in the workspace and how many of those scopes it leaves in
|
|
28
|
+
effect. `-o json` adds `ownerType`, `ownerRole` and `effectiveScopes` under
|
|
29
|
+
`current_key` (`null` when the role couldn't be read).
|
|
30
|
+
- `InsufficientScopeError.client` is the client whose request was refused, and
|
|
31
|
+
`FruxonClient.get_user(user)` reads one workspace member, including `role`
|
|
32
|
+
and `effectiveScopes`.
|
|
33
|
+
|
|
34
|
+
## Unreleased — read voice calls back
|
|
35
|
+
|
|
36
|
+
- **`fruxon agents voice-calls list <agent>`** lists the agent's Talk calls
|
|
37
|
+
from the Test lab, newest first. Each row has the status, language, why the
|
|
38
|
+
call ended, its turns and its length. Output is `table`, `json` or `id`.
|
|
39
|
+
- **`fruxon agents voice-calls get <agent> <call-id>`** shows one call turn by
|
|
40
|
+
turn: what the caller said, the whole reply, and what was heard of it. When
|
|
41
|
+
the caller cut in, `delivery` is `INTERRUPTED` and `spokenText` is the part
|
|
42
|
+
they heard. Each turn also carries `generation` and the run behind its reply
|
|
43
|
+
(`executionRecordId`, for `agents executions trace`). Output is `text`,
|
|
44
|
+
`table` or `json`.
|
|
45
|
+
- Client methods `list_voice_calls` and `get_voice_call` return the new
|
|
46
|
+
`VoiceCall` and `VoiceCallTurn`. Both need fruxon-backend #1537; an older
|
|
47
|
+
server answers 404.
|
|
48
|
+
|
|
49
|
+
## Unreleased — one completion on an LLM config, with no agent
|
|
50
|
+
|
|
51
|
+
- **`fruxon llm-providers configs complete <provider> <config> --model M`**
|
|
52
|
+
sends a system prompt and a short conversation to a model through the
|
|
53
|
+
config's published revision and prints the reply, with no agent, session or
|
|
54
|
+
execution record (scope `llm_providers:test`; fruxon-backend `:complete`).
|
|
55
|
+
It answers "does this model follow that instruction?" — pass a platform
|
|
56
|
+
system block's text as `--system` and read the reply — without a test turn
|
|
57
|
+
that writes to a live agent. `--system` (text, `@file`, `-`; repeatable),
|
|
58
|
+
`-m [user:|assistant:]TEXT` (repeatable) or `--messages chat.json`,
|
|
59
|
+
`--temperature`, `--max-tokens` (server cap 8192), `--param k=v`. The reply
|
|
60
|
+
goes to stdout, model, finish reason, tokens and latency to stderr; `-o json`
|
|
61
|
+
passes the server's result through. A Fruxon-hosted provider is refused.
|
|
62
|
+
|
|
3
63
|
## Unreleased — `fruxon messages list --reason EMAIL_THREAD_UNAVAILABLE`
|
|
4
64
|
|
|
5
65
|
- `--reason` takes the server's new `EMAIL_THREAD_UNAVAILABLE` (fruxon-backend
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: fruxon
|
|
3
|
-
Version: 2.0
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: The Fruxon SDK is a lightweight Python client for integrating with the Fruxon platform.
|
|
5
5
|
Project-URL: bugs, https://github.com/fruxon-ai/fruxon-sdk/issues
|
|
6
6
|
Project-URL: changelog, https://github.com/fruxon-ai/fruxon-sdk/blob/main/HISTORY.md
|
|
@@ -104,6 +104,7 @@ Execution:
|
|
|
104
104
|
| `fruxon escalations list/history/unhandled/policy/ack` | What happened when an agent handed a conversation to a person. `list` is the queue (live takeovers + degraded escalations nobody was reachable for); `unhandled` is the append-only ledger under it. **`history` is the record of closed console-routed takeovers** — those leave the queue, so `list --status ALL` answers `[]` about a conversation a human worked for half an hour. Read `humanObservedAt` before believing a `RESOLVED`: null means nobody was ever seen picking it up. `policy <agent>` resolves who the *next* escalation would reach — an empty ladder under `NATIVE_HANDOFF` is correct, the console supplies the human. |
|
|
105
105
|
| `fruxon messages list` | The channel ledger — including inbound an agent was never shown. A **withheld** message (a human owns the chat in the source console, or admission refused the sender) never becomes a conversation turn, so it is in no transcript and the thread reads as a customer being ignored. `--delivery WITHHELD_PENDING` lists the ones nothing has ever acted on; nothing drains them. |
|
|
106
106
|
| `fruxon agents sandbox open/turn/fire/resolve-input/stream/answer/close <session>` | Drive an agent-network sandbox end-to-end without real channels — send turns as participants, test-fire triggers, and watch the run (`stream`, NDJSON in agent mode). Sandbox **captures the reply and hard-blocks every real send**; `--await` makes consult/approval gates suspend so they can be resolved (`answer` for consults, `approvals respond` for approvals). `--draft` runs your unpublished draft instead of the deployed revision (`sandbox draft <session>` says which); `--confirm-writes` holds every create/update/delete tool call for approval, because sandbox does **not** block integration writes; a write that did not run (rejected, expired, or one the run could not pause for) shows on `stream` as `refused`. |
|
|
107
|
+
| `fruxon agents voice-calls list/get <agent>` | Read back the Test lab's Talk calls. `list` gives calls newest first (status, end reason, turns, length). `get <agent> <call-id>` gives the call turn by turn: the caller's transcript, the reply, what was heard of it (`delivery`; `spokenText` when the caller cut in), and each reply's `executionRecordId` for `agents executions trace`. Read-only; needs `executions:read`. |
|
|
107
108
|
| `fruxon agents sandbox fork <agent> <message-id>` | Fork a real conversation at a transcript row (an `id` from `agents topics messages`) into a sandbox session already holding the history the agent had when it produced that reply. `--send` re-sends the customer's message against it — with `--draft` to test an edit before publishing — and holds writes for approval unless `--no-confirm-writes`. |
|
|
108
109
|
| `fruxon agents sandbox test <file-or-dir>` | Run scenario-based e2e tests of an agent's network behavior. A scenario (YAML/JSON) declares `steps` (turns / trigger fires), `responders` (scripted consult/approval answers), and `expect` assertions (reply / routing / tools called or refused / no-leak / outcome); the runner drives a sandbox session and returns one verdict — exit 0 (pass) / 1 (fail) / 12 (bad file), with `--junit` for CI. An optional `setup` block provisions real resources first — an `asset` (uploaded + ingestion-waited), `participant`, or `agent` — templated as `${asset.catalog.id}` and **torn down afterwards even on failure** (`--keep` to inspect). Assertions are deterministic by default; `reply_judge: {metric, min_score}` adds an opt-in **semantic** check that scores the reply against a tenant eval metric. The thing an agent runs to prove the agent it built works. |
|
|
109
110
|
|
|
@@ -151,6 +152,7 @@ Integrations + tools:
|
|
|
151
152
|
| `fruxon llm-providers list/get/models` | Browse LLM providers and models supported at the tenant level. |
|
|
152
153
|
| `fruxon llm-providers connect <provider>` / `link-status` | Create a config for an LLM provider and print a **credential link**: a human opens it, signs in to Fruxon, types the API key (or access keys, or service account key), and the page tests it with the provider and publishes the config an agent pins as `provider.tenantConfigId`. The key never passes through the terminal or an agent driving the CLI. `--config <id>` fills an existing config, `--param name=value` sets a non-secret setting such as Bedrock's `region`, `--no-sign-in` mints a link anyone holding it can fill (Admin or Owner). `link-status <provider> <config> <request> [--wait]` says when it was filled. |
|
|
153
154
|
| `fruxon llm-providers configs list/get/test/publish` | The workspace's saved LLM credentials — the `provider.tenantConfigId` an agent pins. `get` shows the draft (secrets masked), the published revision and last known health. `test` probes the draft on every provider surface that shares the credential (or just one, given that surface's provider id), one line per failing surface, exit 12 on failure (`llm_providers:test`). `publish` ships the draft as a new revision that every agent on the config switches to — it confirms, needs `--yes` in agent mode, and does not wait on a passing test (`llm_providers:write`). |
|
|
155
|
+
| `fruxon llm-providers configs complete <provider> <config> --model M` | One completion on a saved config's published revision, with no agent, session or execution — to see whether a model follows a prompt or a system block's instruction. `--system` (text, `@file`, `-`; repeatable), `-m [user:\|assistant:]TEXT` (repeatable) or `--messages chat.json`, `--temperature`, `--max-tokens` (server cap 8192), `--param k=v`. The reply goes to stdout, the model, finish reason, tokens and latency to stderr (`llm_providers:test`). Fruxon-hosted providers are refused. |
|
|
154
156
|
| `fruxon assets create/list/get/wait/operations/delete` | Manage knowledge-base (RAG) assets an agent can query — upload local files, wait for async ingestion, inspect operations, and use the ids for `assetConfig.assetIds`. `delete` confirms + refuses without `--yes` in agent mode. |
|
|
155
157
|
| `fruxon knowledge-bases list/get/create/update/publish/archive/delete` | The authored, reviewable corpus behind a `manual_knowledge` asset — **not** the asset itself, which is what gets searched (`get` prints the asset id as `backingAssetId`). **`publish` is the step that makes any of it findable**: only `PUBLISHED` documents travel in a snapshot, and only publishing the *base* moves one, so a base full of finished articles answers nothing until it runs. It returns on *acceptance*, not searchability — RAG ingests afterwards, so a pending publish state is normal. `list` hides archived bases, which keep answering for agents already attached to them; `delete` is refused while an agent revision still attaches one, and `archive` is the reversible path. |
|
|
156
158
|
| `fruxon knowledge-bases usage <kb>` | Whether agents actually use the base: searches per hour, day or week (`--interval`), the passage tokens each search handed the agent, how many found nothing (split from searches that **could not fully search** it — index missing or still building, a phrasing failed), and which agents searched (up to 20 you can open, the rest counted together). `--origin test` counts test runs instead of production. Retrieved tokens are counted by the search index, so what the model read can be less when a large result is trimmed or offloaded. Searches are recorded from 15 October 2026 and kept 90 days at most, so a window reaching further back says where counting began rather than showing empty days. A 404 is a wrong id or a backend that predates the read. |
|
|
@@ -170,6 +172,8 @@ Integrations + tools:
|
|
|
170
172
|
| `fruxon triggers check <trigger>` | Audit whether a work-shape automation can move its items, part by part in the order an item meets them: on and taking items in, the last pass read its source, each PROCESS stage's agent exists with a deployed revision and working credentials and models (a rejected credential's row says how many items failed there since), each ASK stage's contact is declared and bound on the asking agent and its oldest question has not waited past a fifth of its answer window, the outcome note's contact reaches a person, this week's spend against the weekly limit, and how many items died in the last 7 days. `{trigger, overall, checks[]}` like `agents check`; exits 12 only on a hard fail, warnings stay 0. Read-only. |
|
|
171
173
|
| `fruxon triggers budget list/get/set/delete` | Spend limits on an automation **as a whole** (`--period DAILY/WEEKLY/MONTHLY`, UTC calendar windows), counting every production run it caused whichever agent made it. Once one is enforced they govern its runs, which then meet only these limits and not the agents' own; with none enforced, each stage stops at its agent's own limit. None by default and every one removable. An enforced limit refuses new work at the limit (stage runs, passes, a question's opening, the report) and holds the stages until the window resets, while turns of conversations it already opened still run (up to 110%); `--no-enforce` only alerts the Application's admins. Needs Editor. |
|
|
172
174
|
| `fruxon triggers ledger redrive/ignore/settle/cancel-batch` | The operator verbs; all confirm and refuse without `--yes` in agent mode. `redrive` returns a DEAD or FAILED item to the queue with its attempts reset — settle any `UNKNOWN` effect first or the next attempt hits the same refusal. `ignore` excludes an item for a **required** reason, which is the only thing separating IGNORED from DEAD later. `settle` answers an effect the platform could not confirm (`--landed` / `--did-not-land` + `--reason`); wrong in the second direction duplicates the write. `cancel-batch` calls off a question — the items go back to READY and a later pass re-batches them. |
|
|
175
|
+
| `fruxon triggers ledger wake` | End an item's wait at a WAIT stage now, as if its due time had come: an event the stage waits for that already arrived takes `onEvent` with what it carries, otherwise the `onDue` exit applies (on to the next or named stage, or FAIL / DEAD); the history records `OPERATOR_WAKE`. 409 for an item waiting on a question (answer it or `cancel-batch`). `ignore` stops a wait without continuing. `items --waiting-for TIME --order-by wakeAt` lists what waits on time, soonest first; the Detail column says when each wakes and the footer flags any 15 minutes overdue. `items --admitted-via API\|AGENT` lists what was started by hand (`admit`, or an agent's `start_work_item`). |
|
|
176
|
+
| `fruxon triggers ledger admit` | Start one item by hand on an automation whose work shape declares `onDemand`. `--set path=value` / `path:=<json>` (dotted paths nest) or `--data-file`; the server checks the declared inputs and refuses credential-named keys. The key — `--key`, or composed from the shape's `keyPaths` — is the whole idempotency: the same key again returns the existing item **unchanged** (`created: false`), whatever its status. Needs `triggers:fire`. |
|
|
173
177
|
| `fruxon triggers ledger set-data` | Correct a parked (DEAD / FAILED / IGNORED) item's `data` before re-driving it — the value a stage recorded wrong, such as an ASK answer written as `SKIP`, which a plain re-drive would route on again. `--set path=value` (or `path:=<json>`, repeatable) on a top-level key the item carries or a path the shape declares (`produces`, `answeredWhenPath`); a misspelled key is refused with the settable list rather than added. `--reason` is required and lands on the item's history beside every old → new value. `--redrive` re-drives in the same transaction; `redrive` and `redrive-many` take `--set` too. |
|
|
174
178
|
| `fruxon triggers ledger redrive-many/ignore-many` | The same two verbs over a **selection** — `--id` (repeatable), `--stage`, `--status`, `--before`/`--after` (Unix ms). The CLI resolves the selection to a count under the verb's own eligibility, shows it, and the server acts on exactly that count (409 if the set changed). The count is the server's own whenever the endpoint's filters say exactly what the selection says (a `--status` or an `--ignore-reason`, with no `--id` and no `--before`/`--after`) — one request instead of walking every page of a 4,000-item ledger, and one fewer place for the two sides to disagree. `ignore-many --stage X` is how you drain a stage. When the selection is `--id` values, the result names every id it did **not** act on and why — not a work item, another automation's, or a status the verb cannot touch (usually a pass holding the claim). |
|
|
175
179
|
| `fruxon triggers ledger exclusions` | Every reason items were set aside, with how many each holds — the list `--ignore-reason` is picked from, and usually the fastest way to see a prompt excluding a whole class of work it should not. `funnel` stops at `1,204 IGNORED`, which is a status and not something anyone can act on; an exclusion is a judgement a stage made under an instruction, so what you hold after revising it is a *reason*. Each count is exactly what a reason-scoped re-drive resolves, so the diagnosis and the fix use the same string. Reasons print whole and are never truncated — the filter matches the entire string, case-sensitively, so a shortened one selects nothing. A `(none recorded)` row predates the reason being kept on the item: those items are real, no reason filter can reach them, and their way back is `--id`. At most 200 distinct reasons come back. |
|
|
@@ -179,7 +183,7 @@ Integrations + tools:
|
|
|
179
183
|
| `fruxon triggers ledger walk/walk-steps` | `walk --item <id> --yes` chains stage tests: the item's stage (or `--from`), then each stage it advances into, on the data it advanced with, until a real pass would stop too. Each ASK stage it reaches takes `--reply <stage>=<text>` or `--answer <stage>=<value>`; `--agent-revision <agent>=<N>` runs every stage of that agent on an undeployed revision. `stopped.code` is `COMPLETED`, `REACHED_UNTIL` (`--until`), `NOT_ADVANCED` (a `WAIT`, too, and an ADVANCE a pass would undo: a duplicate, or fields the next stage requires), `REPLY_NEEDED` (an ASK stage nobody answered), `STAGE_NOT_TESTABLE`, `MAX_STEPS` (default 10), `TIMEOUT` or `CANCELLED`. Every step is a paid run, so it confirms first and needs `--yes` in agent mode; it exits 0 whenever it ran. `walk-steps <trigger> <walk>` reads a walk back in step order. |
|
|
180
184
|
| `fruxon triggers ledger reset-cursor/purge` | `reset-cursor` moves where the next scan starts, as a compare-and-swap on the version it just read (`--value` omitted = the door's own horizon); it never re-runs admitted work. It is the *after* to `workShape.door.startFrom`'s *before* — a horizon set at creation seeds the cursor before anything sweeps, and is ignored once a pass has read the source. `purge` starts the ledger over — cancels open questions, deletes items and effects, bumps the cursor version — and confirms the exact item count first. The items' transitions stay, as the ledger's history. Destructive; the effects' external targets are not touched. |
|
|
181
185
|
| `fruxon triggers questions list/answer` | The human half of an ASK stage, tenant-wide. `ledger batches <trigger>` answers *what is this automation waiting on*; this answers *what is waiting on anyone* — soonest deadline first, which is how an undelivered question gets found at all. Read **`delivery`** first: `UNDELIVERABLE` (nobody reachable in the slot) and `FAILED` (a target resolved, the send did not land) both mean nobody was asked and nobody will answer unless you do — `QUEUE_ONLY` is healthy. `--undelivered-only` narrows to that pair; `--trigger` filters client-side (the endpoint has no such filter). `answer` settles one on the assignee's behalf and is `--yes`-gated: the reply interpreter **fails closed**, so an answer it cannot place advances nothing and the question stays open, and each item advances only once the stage's `answeredWhenPath` is filled — answering two of three advances two. |
|
|
182
|
-
| `fruxon triggers set-stages <id> --file` | Replace an automation's stages without rewriting the rest of its work shape (a PATCH replaces the whole shape, so this reads it, splices, and posts). Takes a bare stages array or any document carrying one. Pre-flights the two refusals the server only reports after the write: live items at a stage your list drops, and a `door` that will re-expand `source` on save. `--dry-run` prints the merged shape and the warnings; `preview-shape <id> --file` takes the same file and renders what each stage would hand its agent. `--help` documents every stage field the server reads but the CLI passes through untouched — `routes`, `ignoreReasons`, a FORM stage's `answerField`, `formClosing` and `formJoinNotice
|
|
186
|
+
| `fruxon triggers set-stages <id> --file` | Replace an automation's stages without rewriting the rest of its work shape (a PATCH replaces the whole shape, so this reads it, splices, and posts). Takes a bare stages array or any document carrying one. Pre-flights the two refusals the server only reports after the write: live items at a stage your list drops, and a `door` that will re-expand `source` on save. `--dry-run` prints the merged shape and the warnings; `preview-shape <id> --file` takes the same file and renders what each stage would hand its agent. `--help` documents every stage field the server reads but the CLI passes through untouched — `routes`, `ignoreReasons`, a FORM stage's `answerField`, `formClosing` and `formJoinNotice`, and a WAIT stage's `wait` (`minutes` or `untilPath`, then `onDue`; an `event` with `match`, `capture` and `onEvent` to end it early — the invoice example and the save rules are there). It also warns before a save that changes a stage's kind, or a WAIT stage's event correlation, under CLAIMED or WAITING items, which the server refuses, and prints the save's own warnings — such as a WAIT matching its event on a value nothing keeps unique. |
|
|
183
187
|
| `fruxon triggers revisions list/get/restore` | A trigger's audit history — every mutation writes a snapshot, and `changeReason` separates a config edit from a binding reconcile. `get -o json | jq .workShape` is the shape as it stood, which is how you undo an edit by hand. `restore` replays one and is **not** a universal undo: it runs the same validation a save does, so a snapshot that drops a stage still holding live items is refused. |
|
|
184
188
|
| `fruxon secrets list/get/grants` | Discover tenant secrets an agent can reference — the ids for `allowedSecretIds` and the `{{secret.KEY}}` names, with publish-state and per-agent grants. Metadata only; values are never returned. |
|
|
185
189
|
| `fruxon participants list/get/create/update/delete/enable/disable` | Manage agent-network participants (people / groups / agents the network routes to). `list` takes `--query` (matches names **and channel addresses and account ids**, so a phone number or handle resolves to an id, and searches both directory tiers by default), `--kind`, `--tier`, `--agent`, `--environment-id`, and pages up to `--limit` / `--all`. `create`/`update` take `--file` (+`--schema`); `enable`/`disable` toggle routing; `delete` confirms + refuses without `--yes` in agent mode. |
|
|
@@ -340,6 +344,9 @@ spend = client.get_ledger_spend(trigger_id) # this calendar week, by agent and
|
|
|
340
344
|
print(spend.cost, [(a.agent_id, a.cost) for a in spend.agents])
|
|
341
345
|
for b in client.list_ledger_batches(trigger_id, open_only=True):
|
|
342
346
|
print(b.delivery, b.question) # UNDELIVERABLE ⇒ nobody was asked
|
|
347
|
+
for i in client.list_ledger_items(trigger_id, waiting_for="TIME", order_by="wakeAt"):
|
|
348
|
+
print(i.item_key, i.wake_at) # parked at a WAIT stage, soonest first
|
|
349
|
+
client.wake_ledger_item(trigger_id, item_id, reason="indexed early") # its onDue exit applies now
|
|
343
350
|
```
|
|
344
351
|
|
|
345
352
|
The client picks up `FRUXON_TOKEN`, `FRUXON_WORKSPACE`, and `FRUXON_BASE_URL` only if you read them yourself — the constructor takes explicit values. The CLI resolves them automatically (command flags → root flags → env → stored config).
|
|
@@ -69,6 +69,7 @@ Execution:
|
|
|
69
69
|
| `fruxon escalations list/history/unhandled/policy/ack` | What happened when an agent handed a conversation to a person. `list` is the queue (live takeovers + degraded escalations nobody was reachable for); `unhandled` is the append-only ledger under it. **`history` is the record of closed console-routed takeovers** — those leave the queue, so `list --status ALL` answers `[]` about a conversation a human worked for half an hour. Read `humanObservedAt` before believing a `RESOLVED`: null means nobody was ever seen picking it up. `policy <agent>` resolves who the *next* escalation would reach — an empty ladder under `NATIVE_HANDOFF` is correct, the console supplies the human. |
|
|
70
70
|
| `fruxon messages list` | The channel ledger — including inbound an agent was never shown. A **withheld** message (a human owns the chat in the source console, or admission refused the sender) never becomes a conversation turn, so it is in no transcript and the thread reads as a customer being ignored. `--delivery WITHHELD_PENDING` lists the ones nothing has ever acted on; nothing drains them. |
|
|
71
71
|
| `fruxon agents sandbox open/turn/fire/resolve-input/stream/answer/close <session>` | Drive an agent-network sandbox end-to-end without real channels — send turns as participants, test-fire triggers, and watch the run (`stream`, NDJSON in agent mode). Sandbox **captures the reply and hard-blocks every real send**; `--await` makes consult/approval gates suspend so they can be resolved (`answer` for consults, `approvals respond` for approvals). `--draft` runs your unpublished draft instead of the deployed revision (`sandbox draft <session>` says which); `--confirm-writes` holds every create/update/delete tool call for approval, because sandbox does **not** block integration writes; a write that did not run (rejected, expired, or one the run could not pause for) shows on `stream` as `refused`. |
|
|
72
|
+
| `fruxon agents voice-calls list/get <agent>` | Read back the Test lab's Talk calls. `list` gives calls newest first (status, end reason, turns, length). `get <agent> <call-id>` gives the call turn by turn: the caller's transcript, the reply, what was heard of it (`delivery`; `spokenText` when the caller cut in), and each reply's `executionRecordId` for `agents executions trace`. Read-only; needs `executions:read`. |
|
|
72
73
|
| `fruxon agents sandbox fork <agent> <message-id>` | Fork a real conversation at a transcript row (an `id` from `agents topics messages`) into a sandbox session already holding the history the agent had when it produced that reply. `--send` re-sends the customer's message against it — with `--draft` to test an edit before publishing — and holds writes for approval unless `--no-confirm-writes`. |
|
|
73
74
|
| `fruxon agents sandbox test <file-or-dir>` | Run scenario-based e2e tests of an agent's network behavior. A scenario (YAML/JSON) declares `steps` (turns / trigger fires), `responders` (scripted consult/approval answers), and `expect` assertions (reply / routing / tools called or refused / no-leak / outcome); the runner drives a sandbox session and returns one verdict — exit 0 (pass) / 1 (fail) / 12 (bad file), with `--junit` for CI. An optional `setup` block provisions real resources first — an `asset` (uploaded + ingestion-waited), `participant`, or `agent` — templated as `${asset.catalog.id}` and **torn down afterwards even on failure** (`--keep` to inspect). Assertions are deterministic by default; `reply_judge: {metric, min_score}` adds an opt-in **semantic** check that scores the reply against a tenant eval metric. The thing an agent runs to prove the agent it built works. |
|
|
74
75
|
|
|
@@ -116,6 +117,7 @@ Integrations + tools:
|
|
|
116
117
|
| `fruxon llm-providers list/get/models` | Browse LLM providers and models supported at the tenant level. |
|
|
117
118
|
| `fruxon llm-providers connect <provider>` / `link-status` | Create a config for an LLM provider and print a **credential link**: a human opens it, signs in to Fruxon, types the API key (or access keys, or service account key), and the page tests it with the provider and publishes the config an agent pins as `provider.tenantConfigId`. The key never passes through the terminal or an agent driving the CLI. `--config <id>` fills an existing config, `--param name=value` sets a non-secret setting such as Bedrock's `region`, `--no-sign-in` mints a link anyone holding it can fill (Admin or Owner). `link-status <provider> <config> <request> [--wait]` says when it was filled. |
|
|
118
119
|
| `fruxon llm-providers configs list/get/test/publish` | The workspace's saved LLM credentials — the `provider.tenantConfigId` an agent pins. `get` shows the draft (secrets masked), the published revision and last known health. `test` probes the draft on every provider surface that shares the credential (or just one, given that surface's provider id), one line per failing surface, exit 12 on failure (`llm_providers:test`). `publish` ships the draft as a new revision that every agent on the config switches to — it confirms, needs `--yes` in agent mode, and does not wait on a passing test (`llm_providers:write`). |
|
|
120
|
+
| `fruxon llm-providers configs complete <provider> <config> --model M` | One completion on a saved config's published revision, with no agent, session or execution — to see whether a model follows a prompt or a system block's instruction. `--system` (text, `@file`, `-`; repeatable), `-m [user:\|assistant:]TEXT` (repeatable) or `--messages chat.json`, `--temperature`, `--max-tokens` (server cap 8192), `--param k=v`. The reply goes to stdout, the model, finish reason, tokens and latency to stderr (`llm_providers:test`). Fruxon-hosted providers are refused. |
|
|
119
121
|
| `fruxon assets create/list/get/wait/operations/delete` | Manage knowledge-base (RAG) assets an agent can query — upload local files, wait for async ingestion, inspect operations, and use the ids for `assetConfig.assetIds`. `delete` confirms + refuses without `--yes` in agent mode. |
|
|
120
122
|
| `fruxon knowledge-bases list/get/create/update/publish/archive/delete` | The authored, reviewable corpus behind a `manual_knowledge` asset — **not** the asset itself, which is what gets searched (`get` prints the asset id as `backingAssetId`). **`publish` is the step that makes any of it findable**: only `PUBLISHED` documents travel in a snapshot, and only publishing the *base* moves one, so a base full of finished articles answers nothing until it runs. It returns on *acceptance*, not searchability — RAG ingests afterwards, so a pending publish state is normal. `list` hides archived bases, which keep answering for agents already attached to them; `delete` is refused while an agent revision still attaches one, and `archive` is the reversible path. |
|
|
121
123
|
| `fruxon knowledge-bases usage <kb>` | Whether agents actually use the base: searches per hour, day or week (`--interval`), the passage tokens each search handed the agent, how many found nothing (split from searches that **could not fully search** it — index missing or still building, a phrasing failed), and which agents searched (up to 20 you can open, the rest counted together). `--origin test` counts test runs instead of production. Retrieved tokens are counted by the search index, so what the model read can be less when a large result is trimmed or offloaded. Searches are recorded from 15 October 2026 and kept 90 days at most, so a window reaching further back says where counting began rather than showing empty days. A 404 is a wrong id or a backend that predates the read. |
|
|
@@ -135,6 +137,8 @@ Integrations + tools:
|
|
|
135
137
|
| `fruxon triggers check <trigger>` | Audit whether a work-shape automation can move its items, part by part in the order an item meets them: on and taking items in, the last pass read its source, each PROCESS stage's agent exists with a deployed revision and working credentials and models (a rejected credential's row says how many items failed there since), each ASK stage's contact is declared and bound on the asking agent and its oldest question has not waited past a fifth of its answer window, the outcome note's contact reaches a person, this week's spend against the weekly limit, and how many items died in the last 7 days. `{trigger, overall, checks[]}` like `agents check`; exits 12 only on a hard fail, warnings stay 0. Read-only. |
|
|
136
138
|
| `fruxon triggers budget list/get/set/delete` | Spend limits on an automation **as a whole** (`--period DAILY/WEEKLY/MONTHLY`, UTC calendar windows), counting every production run it caused whichever agent made it. Once one is enforced they govern its runs, which then meet only these limits and not the agents' own; with none enforced, each stage stops at its agent's own limit. None by default and every one removable. An enforced limit refuses new work at the limit (stage runs, passes, a question's opening, the report) and holds the stages until the window resets, while turns of conversations it already opened still run (up to 110%); `--no-enforce` only alerts the Application's admins. Needs Editor. |
|
|
137
139
|
| `fruxon triggers ledger redrive/ignore/settle/cancel-batch` | The operator verbs; all confirm and refuse without `--yes` in agent mode. `redrive` returns a DEAD or FAILED item to the queue with its attempts reset — settle any `UNKNOWN` effect first or the next attempt hits the same refusal. `ignore` excludes an item for a **required** reason, which is the only thing separating IGNORED from DEAD later. `settle` answers an effect the platform could not confirm (`--landed` / `--did-not-land` + `--reason`); wrong in the second direction duplicates the write. `cancel-batch` calls off a question — the items go back to READY and a later pass re-batches them. |
|
|
140
|
+
| `fruxon triggers ledger wake` | End an item's wait at a WAIT stage now, as if its due time had come: an event the stage waits for that already arrived takes `onEvent` with what it carries, otherwise the `onDue` exit applies (on to the next or named stage, or FAIL / DEAD); the history records `OPERATOR_WAKE`. 409 for an item waiting on a question (answer it or `cancel-batch`). `ignore` stops a wait without continuing. `items --waiting-for TIME --order-by wakeAt` lists what waits on time, soonest first; the Detail column says when each wakes and the footer flags any 15 minutes overdue. `items --admitted-via API\|AGENT` lists what was started by hand (`admit`, or an agent's `start_work_item`). |
|
|
141
|
+
| `fruxon triggers ledger admit` | Start one item by hand on an automation whose work shape declares `onDemand`. `--set path=value` / `path:=<json>` (dotted paths nest) or `--data-file`; the server checks the declared inputs and refuses credential-named keys. The key — `--key`, or composed from the shape's `keyPaths` — is the whole idempotency: the same key again returns the existing item **unchanged** (`created: false`), whatever its status. Needs `triggers:fire`. |
|
|
138
142
|
| `fruxon triggers ledger set-data` | Correct a parked (DEAD / FAILED / IGNORED) item's `data` before re-driving it — the value a stage recorded wrong, such as an ASK answer written as `SKIP`, which a plain re-drive would route on again. `--set path=value` (or `path:=<json>`, repeatable) on a top-level key the item carries or a path the shape declares (`produces`, `answeredWhenPath`); a misspelled key is refused with the settable list rather than added. `--reason` is required and lands on the item's history beside every old → new value. `--redrive` re-drives in the same transaction; `redrive` and `redrive-many` take `--set` too. |
|
|
139
143
|
| `fruxon triggers ledger redrive-many/ignore-many` | The same two verbs over a **selection** — `--id` (repeatable), `--stage`, `--status`, `--before`/`--after` (Unix ms). The CLI resolves the selection to a count under the verb's own eligibility, shows it, and the server acts on exactly that count (409 if the set changed). The count is the server's own whenever the endpoint's filters say exactly what the selection says (a `--status` or an `--ignore-reason`, with no `--id` and no `--before`/`--after`) — one request instead of walking every page of a 4,000-item ledger, and one fewer place for the two sides to disagree. `ignore-many --stage X` is how you drain a stage. When the selection is `--id` values, the result names every id it did **not** act on and why — not a work item, another automation's, or a status the verb cannot touch (usually a pass holding the claim). |
|
|
140
144
|
| `fruxon triggers ledger exclusions` | Every reason items were set aside, with how many each holds — the list `--ignore-reason` is picked from, and usually the fastest way to see a prompt excluding a whole class of work it should not. `funnel` stops at `1,204 IGNORED`, which is a status and not something anyone can act on; an exclusion is a judgement a stage made under an instruction, so what you hold after revising it is a *reason*. Each count is exactly what a reason-scoped re-drive resolves, so the diagnosis and the fix use the same string. Reasons print whole and are never truncated — the filter matches the entire string, case-sensitively, so a shortened one selects nothing. A `(none recorded)` row predates the reason being kept on the item: those items are real, no reason filter can reach them, and their way back is `--id`. At most 200 distinct reasons come back. |
|
|
@@ -144,7 +148,7 @@ Integrations + tools:
|
|
|
144
148
|
| `fruxon triggers ledger walk/walk-steps` | `walk --item <id> --yes` chains stage tests: the item's stage (or `--from`), then each stage it advances into, on the data it advanced with, until a real pass would stop too. Each ASK stage it reaches takes `--reply <stage>=<text>` or `--answer <stage>=<value>`; `--agent-revision <agent>=<N>` runs every stage of that agent on an undeployed revision. `stopped.code` is `COMPLETED`, `REACHED_UNTIL` (`--until`), `NOT_ADVANCED` (a `WAIT`, too, and an ADVANCE a pass would undo: a duplicate, or fields the next stage requires), `REPLY_NEEDED` (an ASK stage nobody answered), `STAGE_NOT_TESTABLE`, `MAX_STEPS` (default 10), `TIMEOUT` or `CANCELLED`. Every step is a paid run, so it confirms first and needs `--yes` in agent mode; it exits 0 whenever it ran. `walk-steps <trigger> <walk>` reads a walk back in step order. |
|
|
145
149
|
| `fruxon triggers ledger reset-cursor/purge` | `reset-cursor` moves where the next scan starts, as a compare-and-swap on the version it just read (`--value` omitted = the door's own horizon); it never re-runs admitted work. It is the *after* to `workShape.door.startFrom`'s *before* — a horizon set at creation seeds the cursor before anything sweeps, and is ignored once a pass has read the source. `purge` starts the ledger over — cancels open questions, deletes items and effects, bumps the cursor version — and confirms the exact item count first. The items' transitions stay, as the ledger's history. Destructive; the effects' external targets are not touched. |
|
|
146
150
|
| `fruxon triggers questions list/answer` | The human half of an ASK stage, tenant-wide. `ledger batches <trigger>` answers *what is this automation waiting on*; this answers *what is waiting on anyone* — soonest deadline first, which is how an undelivered question gets found at all. Read **`delivery`** first: `UNDELIVERABLE` (nobody reachable in the slot) and `FAILED` (a target resolved, the send did not land) both mean nobody was asked and nobody will answer unless you do — `QUEUE_ONLY` is healthy. `--undelivered-only` narrows to that pair; `--trigger` filters client-side (the endpoint has no such filter). `answer` settles one on the assignee's behalf and is `--yes`-gated: the reply interpreter **fails closed**, so an answer it cannot place advances nothing and the question stays open, and each item advances only once the stage's `answeredWhenPath` is filled — answering two of three advances two. |
|
|
147
|
-
| `fruxon triggers set-stages <id> --file` | Replace an automation's stages without rewriting the rest of its work shape (a PATCH replaces the whole shape, so this reads it, splices, and posts). Takes a bare stages array or any document carrying one. Pre-flights the two refusals the server only reports after the write: live items at a stage your list drops, and a `door` that will re-expand `source` on save. `--dry-run` prints the merged shape and the warnings; `preview-shape <id> --file` takes the same file and renders what each stage would hand its agent. `--help` documents every stage field the server reads but the CLI passes through untouched — `routes`, `ignoreReasons`, a FORM stage's `answerField`, `formClosing` and `formJoinNotice
|
|
151
|
+
| `fruxon triggers set-stages <id> --file` | Replace an automation's stages without rewriting the rest of its work shape (a PATCH replaces the whole shape, so this reads it, splices, and posts). Takes a bare stages array or any document carrying one. Pre-flights the two refusals the server only reports after the write: live items at a stage your list drops, and a `door` that will re-expand `source` on save. `--dry-run` prints the merged shape and the warnings; `preview-shape <id> --file` takes the same file and renders what each stage would hand its agent. `--help` documents every stage field the server reads but the CLI passes through untouched — `routes`, `ignoreReasons`, a FORM stage's `answerField`, `formClosing` and `formJoinNotice`, and a WAIT stage's `wait` (`minutes` or `untilPath`, then `onDue`; an `event` with `match`, `capture` and `onEvent` to end it early — the invoice example and the save rules are there). It also warns before a save that changes a stage's kind, or a WAIT stage's event correlation, under CLAIMED or WAITING items, which the server refuses, and prints the save's own warnings — such as a WAIT matching its event on a value nothing keeps unique. |
|
|
148
152
|
| `fruxon triggers revisions list/get/restore` | A trigger's audit history — every mutation writes a snapshot, and `changeReason` separates a config edit from a binding reconcile. `get -o json | jq .workShape` is the shape as it stood, which is how you undo an edit by hand. `restore` replays one and is **not** a universal undo: it runs the same validation a save does, so a snapshot that drops a stage still holding live items is refused. |
|
|
149
153
|
| `fruxon secrets list/get/grants` | Discover tenant secrets an agent can reference — the ids for `allowedSecretIds` and the `{{secret.KEY}}` names, with publish-state and per-agent grants. Metadata only; values are never returned. |
|
|
150
154
|
| `fruxon participants list/get/create/update/delete/enable/disable` | Manage agent-network participants (people / groups / agents the network routes to). `list` takes `--query` (matches names **and channel addresses and account ids**, so a phone number or handle resolves to an id, and searches both directory tiers by default), `--kind`, `--tier`, `--agent`, `--environment-id`, and pages up to `--limit` / `--all`. `create`/`update` take `--file` (+`--schema`); `enable`/`disable` toggle routing; `delete` confirms + refuses without `--yes` in agent mode. |
|
|
@@ -305,6 +309,9 @@ spend = client.get_ledger_spend(trigger_id) # this calendar week, by agent and
|
|
|
305
309
|
print(spend.cost, [(a.agent_id, a.cost) for a in spend.agents])
|
|
306
310
|
for b in client.list_ledger_batches(trigger_id, open_only=True):
|
|
307
311
|
print(b.delivery, b.question) # UNDELIVERABLE ⇒ nobody was asked
|
|
312
|
+
for i in client.list_ledger_items(trigger_id, waiting_for="TIME", order_by="wakeAt"):
|
|
313
|
+
print(i.item_key, i.wake_at) # parked at a WAIT stage, soonest first
|
|
314
|
+
client.wake_ledger_item(trigger_id, item_id, reason="indexed early") # its onDue exit applies now
|
|
308
315
|
```
|
|
309
316
|
|
|
310
317
|
The client picks up `FRUXON_TOKEN`, `FRUXON_WORKSPACE`, and `FRUXON_BASE_URL` only if you read them yourself — the constructor takes explicit values. The CLI resolves them automatically (command flags → root flags → env → stored config).
|
|
@@ -80,7 +80,15 @@ from fruxon.models import (
|
|
|
80
80
|
LedgerFunnelCell,
|
|
81
81
|
LedgerGatheringGroup,
|
|
82
82
|
LedgerItem,
|
|
83
|
+
LedgerItemAdmission,
|
|
84
|
+
LedgerItemAttempt,
|
|
85
|
+
LedgerItemAttemptAsk,
|
|
86
|
+
LedgerItemAttemptBatch,
|
|
87
|
+
LedgerItemAttemptOutcome,
|
|
88
|
+
LedgerItemAttemptWait,
|
|
83
89
|
LedgerItemPoint,
|
|
90
|
+
LedgerItemRuns,
|
|
91
|
+
LedgerItemRunTotals,
|
|
84
92
|
LedgerItemTimeseries,
|
|
85
93
|
LedgerItemTransition,
|
|
86
94
|
LedgerPass,
|
|
@@ -97,6 +105,11 @@ from fruxon.models import (
|
|
|
97
105
|
LedgerStageTestSimulatedCall,
|
|
98
106
|
LedgerStageTestSink,
|
|
99
107
|
LedgerStageTestVerdict,
|
|
108
|
+
LedgerWaitCapture,
|
|
109
|
+
LedgerWaitConfig,
|
|
110
|
+
LedgerWaitEventConfig,
|
|
111
|
+
LedgerWaitExit,
|
|
112
|
+
LedgerWaitMatch,
|
|
100
113
|
ManagedBy,
|
|
101
114
|
ManagedResourceLink,
|
|
102
115
|
Memory,
|
|
@@ -123,6 +136,8 @@ from fruxon.models import (
|
|
|
123
136
|
TriggerParameterMapping,
|
|
124
137
|
TriggerRevision,
|
|
125
138
|
UnhandledEscalation,
|
|
139
|
+
VoiceCall,
|
|
140
|
+
VoiceCallTurn,
|
|
126
141
|
WorkShapeDoor,
|
|
127
142
|
WorkShapeDoorConversation,
|
|
128
143
|
WorkShapeDoorField,
|
|
@@ -212,6 +227,14 @@ __all__ = [
|
|
|
212
227
|
"LedgerFunnelCell",
|
|
213
228
|
"LedgerGatheringGroup",
|
|
214
229
|
"LedgerItem",
|
|
230
|
+
"LedgerItemAdmission",
|
|
231
|
+
"LedgerItemAttempt",
|
|
232
|
+
"LedgerItemAttemptAsk",
|
|
233
|
+
"LedgerItemAttemptBatch",
|
|
234
|
+
"LedgerItemAttemptOutcome",
|
|
235
|
+
"LedgerItemAttemptWait",
|
|
236
|
+
"LedgerItemRuns",
|
|
237
|
+
"LedgerItemRunTotals",
|
|
215
238
|
"LedgerItemTransition",
|
|
216
239
|
"LedgerPass",
|
|
217
240
|
"LedgerPurgeResult",
|
|
@@ -221,6 +244,11 @@ __all__ = [
|
|
|
221
244
|
"LedgerStageTestSimulatedCall",
|
|
222
245
|
"LedgerStageTestSink",
|
|
223
246
|
"LedgerStageTestVerdict",
|
|
247
|
+
"LedgerWaitCapture",
|
|
248
|
+
"LedgerWaitConfig",
|
|
249
|
+
"LedgerWaitEventConfig",
|
|
250
|
+
"LedgerWaitExit",
|
|
251
|
+
"LedgerWaitMatch",
|
|
224
252
|
"LedgerSkippedItem",
|
|
225
253
|
"ManagedBy",
|
|
226
254
|
"Memory",
|
|
@@ -250,6 +278,8 @@ __all__ = [
|
|
|
250
278
|
"TriggerRevision",
|
|
251
279
|
"UnhandledEscalation",
|
|
252
280
|
"ValidationError",
|
|
281
|
+
"VoiceCall",
|
|
282
|
+
"VoiceCallTurn",
|
|
253
283
|
"WorkShapeDoor",
|
|
254
284
|
"WorkShapeDoorConversation",
|
|
255
285
|
"WorkShapeDoorField",
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '2.0
|
|
22
|
-
__version_tuple__ = version_tuple = (2,
|
|
21
|
+
__version__ = version = '2.2.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (2, 2, 0)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -21,6 +21,7 @@ from datetime import datetime
|
|
|
21
21
|
|
|
22
22
|
from fruxon.checks import CheckReport, CheckResult, shape_get
|
|
23
23
|
from fruxon.exceptions import FruxonError, NotFoundError
|
|
24
|
+
from fruxon.models import LedgerWaitConfig, LedgerWaitEventConfig, LedgerWaitExit
|
|
24
25
|
|
|
25
26
|
DAY_MS = 86_400_000
|
|
26
27
|
_HOUR_MS = 3_600_000
|
|
@@ -137,6 +138,7 @@ def check_ledger(client, trigger: str, *, detail=None, now_ms: int | None = None
|
|
|
137
138
|
cells: dict[str, dict[str, int]] = {}
|
|
138
139
|
for cell in funnel or []:
|
|
139
140
|
cells.setdefault(cell.stage_id, {})[cell.status.upper()] = cell.count
|
|
141
|
+
waiting_cells = {c.stage_id: c for c in funnel or [] if c.status.upper() == "WAITING"}
|
|
140
142
|
today = int(datetime.fromtimestamp(now / 1000).replace(hour=0, minute=0, second=0, microsecond=0).timestamp())
|
|
141
143
|
series, _ = _try(
|
|
142
144
|
client.get_ledger_item_timeseries, trigger, interval="DAY", since=today * 1000 - 6 * DAY_MS, until=now
|
|
@@ -207,6 +209,9 @@ def check_ledger(client, trigger: str, *, detail=None, now_ms: int | None = None
|
|
|
207
209
|
if kind == "ASK":
|
|
208
210
|
rows.append(_ask_row(trigger, stage, stage_id, asker, slots_of, batches, batches_ok, now))
|
|
209
211
|
continue
|
|
212
|
+
if kind == "WAIT":
|
|
213
|
+
rows.append(_wait_row(trigger, stage, stage_id, waiting_cells.get(stage_id), now))
|
|
214
|
+
continue
|
|
210
215
|
agent = str(shape_get(stage, "agentId") or "")
|
|
211
216
|
if not agent:
|
|
212
217
|
add(stage_id, "skip", "no agent — items pass through", explain="", fix="trigger-edit")
|
|
@@ -432,10 +437,102 @@ def _ask_row(
|
|
|
432
437
|
)
|
|
433
438
|
|
|
434
439
|
|
|
440
|
+
def _wait_row(trigger: str, stage: dict, stage_id: str, cell, now: int) -> CheckResult:
|
|
441
|
+
"""A WAIT stage: nothing runs there, so the one thing that can be wrong is a
|
|
442
|
+
wait left well past its due time — the sweeper that ends them is behind."""
|
|
443
|
+
wait = LedgerWaitConfig.from_dict(shape_get(stage, "wait")) or LedgerWaitConfig()
|
|
444
|
+
what = f"waits {describe_wait(wait)}"
|
|
445
|
+
overdue = getattr(cell, "overdue", 0) if cell is not None else 0
|
|
446
|
+
if overdue:
|
|
447
|
+
return CheckResult(
|
|
448
|
+
stage_id,
|
|
449
|
+
"warn",
|
|
450
|
+
f"{what} · {overdue} overdue",
|
|
451
|
+
hint=f"fruxon triggers ledger items {trigger} --waiting-for TIME --order-by wakeAt",
|
|
452
|
+
explain="A wait ends within a minute or two of its due time. Items more than 15 minutes past it mean "
|
|
453
|
+
"the wait sweeper is down or behind; `fruxon triggers ledger wake` ends one by hand.",
|
|
454
|
+
fix=f"work:WAITING:{stage_id}",
|
|
455
|
+
)
|
|
456
|
+
waiting = cell.count if cell is not None else 0
|
|
457
|
+
next_wake = getattr(cell, "next_wake_at", None) if cell is not None else None
|
|
458
|
+
if waiting:
|
|
459
|
+
what += f" · {waiting:,} waiting" + (f", next wakes {_clock(next_wake, now)}" if next_wake else "")
|
|
460
|
+
return CheckResult(
|
|
461
|
+
stage_id,
|
|
462
|
+
"ok",
|
|
463
|
+
what,
|
|
464
|
+
explain="Nothing runs here: each item waits until its due time"
|
|
465
|
+
+ (", or until its event arrives," if wait.event is not None else "")
|
|
466
|
+
+ " then moves on.",
|
|
467
|
+
fix=f"work:WAITING:{stage_id}",
|
|
468
|
+
)
|
|
469
|
+
|
|
470
|
+
|
|
471
|
+
def _span(minutes: int) -> str:
|
|
472
|
+
days, rest = divmod(minutes, 1_440)
|
|
473
|
+
hours, mins = divmod(rest, 60)
|
|
474
|
+
return " ".join(f"{n}{unit}" for n, unit in ((days, "d"), (hours, "h"), (mins, "m")) if n) or "0m"
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def _exit_text(exit_: LedgerWaitExit) -> str:
|
|
478
|
+
if exit_.action == "ADVANCE_TO":
|
|
479
|
+
return exit_.stage_id or "?"
|
|
480
|
+
if exit_.action in {"FAIL", "DEAD"}:
|
|
481
|
+
return exit_.action
|
|
482
|
+
return "next"
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def describe_wait(wait: LedgerWaitConfig) -> str:
|
|
486
|
+
"""A WAIT stage in a few words: ``3d``, ``until due_at, then DEAD``, ``1h 30m → paid``, or with an
|
|
487
|
+
event ``until due_at or stripe.payment_intent.succeeded: event → receipt, due → next``."""
|
|
488
|
+
span = _span(wait.minutes) if wait.minutes is not None else f"until {wait.until_path or '?'}"
|
|
489
|
+
if wait.event is not None:
|
|
490
|
+
return (
|
|
491
|
+
f"{span} or {wait.event.event_type or '?'}: event → {_exit_text(wait.effective_on_event)}, "
|
|
492
|
+
f"due → {_exit_text(wait.effective_on_due)}"
|
|
493
|
+
)
|
|
494
|
+
exit_ = wait.effective_on_due
|
|
495
|
+
if exit_.action == "ADVANCE_TO":
|
|
496
|
+
return f"{span} → {exit_.stage_id or '?'}"
|
|
497
|
+
if exit_.action in {"FAIL", "DEAD"}:
|
|
498
|
+
return f"{span}, then {exit_.action}"
|
|
499
|
+
return span
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def describe_wait_event(event: LedgerWaitEventConfig) -> list[tuple[str, str]]:
|
|
503
|
+
"""``(label, text)`` lines for what a WAIT stage's event is matched on and carries into the item."""
|
|
504
|
+
lines: list[tuple[str, str]] = []
|
|
505
|
+
if event.integration_config_id:
|
|
506
|
+
lines.append(("connection", event.integration_config_id))
|
|
507
|
+
lines.extend(("match", f"{m.event_path} = item {m.item_path}") for m in event.match)
|
|
508
|
+
lines.extend(("capture", f"{c.event_path} → {c.item_path} ({c.type})") for c in event.capture)
|
|
509
|
+
if event.filter:
|
|
510
|
+
lines.append(("only when", _filter_text(event.filter)))
|
|
511
|
+
return lines
|
|
512
|
+
|
|
513
|
+
|
|
514
|
+
def _filter_text(flt: dict) -> str:
|
|
515
|
+
"""An event filter as one line: its conditions, joined the way its ``match`` combines them."""
|
|
516
|
+
conditions = shape_get(flt, "conditions")
|
|
517
|
+
parts = []
|
|
518
|
+
for c in conditions if isinstance(conditions, list) else []:
|
|
519
|
+
if not isinstance(c, dict):
|
|
520
|
+
continue
|
|
521
|
+
values = shape_get(c, "values")
|
|
522
|
+
value = f"[{', '.join(str(v) for v in values)}]" if isinstance(values, list) else shape_get(c, "value")
|
|
523
|
+
parts.append(" ".join(str(p) for p in (shape_get(c, "path"), shape_get(c, "operator"), value) if p is not None))
|
|
524
|
+
joiner = " or " if str(shape_get(flt, "match") or "ALL").upper() == "ANY" else " and "
|
|
525
|
+
return joiner.join(parts) or "—"
|
|
526
|
+
|
|
527
|
+
|
|
435
528
|
def describe_stage(stage: dict) -> tuple[str, str, str, str]:
|
|
436
529
|
"""``(name, kind, who, what it does)`` for the console's shape table."""
|
|
437
530
|
stage_id = str(shape_get(stage, "id") or "?")
|
|
438
531
|
kind = str(shape_get(stage, "kind") or "").upper()
|
|
532
|
+
if kind == "WAIT":
|
|
533
|
+
wait = LedgerWaitConfig.from_dict(shape_get(stage, "wait")) or LedgerWaitConfig()
|
|
534
|
+
who = wait.event.event_type if wait.event is not None and wait.event.event_type else "clock"
|
|
535
|
+
return stage_id, "wait", who, describe_wait(wait)
|
|
439
536
|
if kind == "ASK":
|
|
440
537
|
ask = shape_get(stage, "ask")
|
|
441
538
|
ask = ask if isinstance(ask, dict) else {}
|
|
@@ -710,6 +710,7 @@ from fruxon.cli import agents_slots as _agents_slots # noqa: E402, F401
|
|
|
710
710
|
from fruxon.cli import agents_system_blocks as _agents_system_blocks # noqa: E402, F401
|
|
711
711
|
from fruxon.cli import agents_tests as _agents_tests # noqa: E402, F401
|
|
712
712
|
from fruxon.cli import agents_topics as _agents_topics # noqa: E402, F401
|
|
713
|
+
from fruxon.cli import agents_voice_calls as _agents_voice_calls # noqa: E402, F401
|
|
713
714
|
from fruxon.cli import applications as _applications # noqa: E402, F401
|
|
714
715
|
from fruxon.cli import applications_entry_points as _applications_entry_points # noqa: E402, F401
|
|
715
716
|
from fruxon.cli import assets as _assets # noqa: E402, F401
|
|
@@ -12,6 +12,7 @@ from __future__ import annotations
|
|
|
12
12
|
|
|
13
13
|
import json
|
|
14
14
|
import sys
|
|
15
|
+
from dataclasses import dataclass
|
|
15
16
|
from datetime import datetime, timezone
|
|
16
17
|
|
|
17
18
|
import typer
|
|
@@ -69,6 +70,24 @@ def format_ts(ms: int | None) -> str:
|
|
|
69
70
|
return f"{ms} (raw)"
|
|
70
71
|
|
|
71
72
|
|
|
73
|
+
def format_relative(ms: int, now: int) -> str:
|
|
74
|
+
"""How far ``ms`` is from ``now``, both epoch-ms: ``in 2d 3h`` ahead, ``40m ago`` behind.
|
|
75
|
+
|
|
76
|
+
Two units at most and never less than a minute — a due time is read as
|
|
77
|
+
"soon" or "a while", not to the second.
|
|
78
|
+
"""
|
|
79
|
+
delta = abs(ms - now) // 60_000
|
|
80
|
+
days, rest = divmod(delta, 1_440)
|
|
81
|
+
hours, minutes = divmod(rest, 60)
|
|
82
|
+
if days:
|
|
83
|
+
span = f"{days}d {hours}h" if hours else f"{days}d"
|
|
84
|
+
elif hours:
|
|
85
|
+
span = f"{hours}h {minutes}m" if minutes else f"{hours}h"
|
|
86
|
+
else:
|
|
87
|
+
span = f"{max(minutes, 1)}m"
|
|
88
|
+
return f"in {span}" if ms >= now else f"{span} ago"
|
|
89
|
+
|
|
90
|
+
|
|
72
91
|
def build_dict(required: dict, *, truthy: dict | None = None, keep_zero: dict | None = None) -> dict:
|
|
73
92
|
"""Assemble a wire dict from three buckets, so each call site only
|
|
74
93
|
has to say which bucket a field belongs in rather than re-deriving
|
|
@@ -305,6 +324,73 @@ def build_client(workspace: str | None, base_url: str | None) -> FruxonClient:
|
|
|
305
324
|
return FruxonClient(token=token, workspace=workspace, base_url=resolved_base)
|
|
306
325
|
|
|
307
326
|
|
|
327
|
+
@dataclass(frozen=True)
|
|
328
|
+
class KeyAccess:
|
|
329
|
+
"""What the calling key carries, and what the server lets it do in this workspace.
|
|
330
|
+
|
|
331
|
+
``scopes`` is the key's own list, as minted (``/tokens/current``). A
|
|
332
|
+
personal key acts as its owner, and the server cuts it down on every
|
|
333
|
+
request to what the owner's current workspace role allows
|
|
334
|
+
(fruxon-backend ``RoleScopeMap.CapToRole``), so ``scopes`` can name a
|
|
335
|
+
scope the key is refused. ``role`` and ``effective_scopes`` are that
|
|
336
|
+
cut. A service-account key carries no scopes of its own and no role
|
|
337
|
+
caps it: its ``effective_scopes`` is the account's grant and ``role``
|
|
338
|
+
is ``None``. Both are ``None`` when the server didn't say: the owner's
|
|
339
|
+
membership could not be read, or a service-account key on a server
|
|
340
|
+
older than fruxon-backend #1571.
|
|
341
|
+
"""
|
|
342
|
+
|
|
343
|
+
key: dict
|
|
344
|
+
scopes: list[str]
|
|
345
|
+
personal: bool
|
|
346
|
+
role: str | None
|
|
347
|
+
effective_scopes: list[str] | None
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def read_key_access(client: FruxonClient) -> KeyAccess | None:
|
|
351
|
+
"""Ask the server what ``client``'s key carries and what it may do in this workspace.
|
|
352
|
+
|
|
353
|
+
A server with fruxon-backend #1571 says it all on ``/tokens/current``
|
|
354
|
+
(``effectiveScopes``, and ``ownerRole`` for a personal key). On an
|
|
355
|
+
older one a personal key's cut is worked out from its owner's
|
|
356
|
+
membership (``GET /users/{ownerId}``), a read that needs
|
|
357
|
+
``workspace:read``.
|
|
358
|
+
|
|
359
|
+
Best-effort: ``None`` when ``/tokens/current`` can't answer (signed in
|
|
360
|
+
by federation, network down, …), and a :class:`KeyAccess` without a
|
|
361
|
+
role when only the membership read fails.
|
|
362
|
+
"""
|
|
363
|
+
try:
|
|
364
|
+
key = client.get_current_token()
|
|
365
|
+
except Exception:
|
|
366
|
+
return None
|
|
367
|
+
if not key:
|
|
368
|
+
return None
|
|
369
|
+
scopes = [str(s) for s in key.get("scopes") or []]
|
|
370
|
+
personal = key.get("ownerType") == "USER"
|
|
371
|
+
role = effective = None
|
|
372
|
+
owner = key.get("ownerId")
|
|
373
|
+
reported, owner_role = key.get("effectiveScopes"), key.get("ownerRole")
|
|
374
|
+
if isinstance(reported, list) and (owner_role or not personal):
|
|
375
|
+
# The server's own answer: what this request was authorised with
|
|
376
|
+
# (CapToRole's cut, or a service account's grant). One call, with
|
|
377
|
+
# no workspace:read, which a narrow key may not carry.
|
|
378
|
+
effective = [str(s) for s in reported]
|
|
379
|
+
role = str(owner_role) if owner_role else None
|
|
380
|
+
elif personal and owner:
|
|
381
|
+
try:
|
|
382
|
+
member = client.get_user(str(owner))
|
|
383
|
+
except Exception:
|
|
384
|
+
member = {}
|
|
385
|
+
ceiling = member.get("effectiveScopes")
|
|
386
|
+
if member.get("role") and isinstance(ceiling, list):
|
|
387
|
+
role = str(member["role"])
|
|
388
|
+
allowed = {str(s) for s in ceiling}
|
|
389
|
+
# CapToRole's order: the key's own, keeping what the role carries.
|
|
390
|
+
effective = [s for s in dict.fromkeys(scopes) if s in allowed]
|
|
391
|
+
return KeyAccess(key=key, scopes=scopes, personal=personal, role=role, effective_scopes=effective)
|
|
392
|
+
|
|
393
|
+
|
|
308
394
|
def load_json_body(file: str, *, what: str) -> dict:
|
|
309
395
|
"""Read and parse a ``--file`` JSON request body into a dict.
|
|
310
396
|
|