fruxon 0.21.2__tar.gz → 1.0.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-0.21.2 → fruxon-1.0.0}/HISTORY.md +23 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/PKG-INFO +2 -2
- {fruxon-0.21.2 → fruxon-1.0.0}/README.md +1 -1
- {fruxon-0.21.2 → fruxon-1.0.0}/pyproject.toml +7 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/_version.py +2 -2
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/_crash.py +19 -16
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/examples.py +9 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/integrations.py +273 -2
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/fruxon.py +69 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/skills/fruxon-agent-mode/SKILL.md +1 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/skills/fruxon-meet/SKILL.md +1 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/skills/fruxon-use-integrations/SKILL.md +8 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_docs_commands_exist.py +3 -0
- fruxon-1.0.0/tests/test_integration_configs_write_cli.py +414 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_kb_trial_cli.py +3 -1
- {fruxon-0.21.2 → fruxon-1.0.0}/.gitignore +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/LICENSE +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/__init__.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/__main__.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/_ssl.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/__init__.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/_schema.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/_shared.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/_stream.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/agents.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/agents_approvals.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/agents_budget.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/agents_channels.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/agents_draft.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/agents_evaluations.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/agents_executions.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/agents_improvements.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/agents_lifecycle.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/agents_memory.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/agents_reply_style.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/agents_revisions.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/agents_sandbox.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/agents_sandbox_test.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/agents_slots.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/agents_system_blocks.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/agents_tests.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/agents_topics.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/applications.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/applications_entry_points.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/assets.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/auth.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/capabilities.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/completion.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/config.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/consult_pins.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/conversation_imports.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/describe.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/doctor.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/environments.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/escalations.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/guides.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/keys.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/knowledge_bases.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/knowledge_documents.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/knowledge_notices.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/knowledge_sources.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/llm_providers.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/messages.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/metrics.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/participants.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/pipelines.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/secrets.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/skills.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/storage.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/tools.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/triggers.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/triggers_budget.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/triggers_ledger.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/triggers_ledger_stage_tests.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/triggers_questions.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/triggers_shape.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/cli/workspaces.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/credentials.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/device_auth.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/doctor.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/exceptions.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/flow_validation.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/models.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/output.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/params.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/sandbox_scenario.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/skills/__init__.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/skills/fruxon-build-agent/SKILL.md +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/skills/fruxon-create-integration/SKILL.md +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/skills/fruxon-debug-trace/SKILL.md +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/telemetry.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/ui.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/update_check.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/src/fruxon/validation.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/.test_durations.json +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/__init__.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/conftest.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_actor.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_agent_lifecycle.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_agent_revisions_list.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_agent_slots.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_agents_check_cli.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_agents_revisions_cli.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_agents_sandbox.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_applications.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_approvals.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_assets.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_budgets.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_capabilities.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_channels.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_cli.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_cli_base_url_routing.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_cli_crash.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_cli_root_flags.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_client.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_connect_nudge.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_consult.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_conversation_imports.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_credentials.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_describe.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_device_auth.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_doctor.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_draft_bootstrap.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_draft_evaluate_cli.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_draft_pull_shape.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_draft_run_replay.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_draft_validate_cli.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_drafts.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_entry_points.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_environments.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_escalations.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_evaluations.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_execution_records.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_flow_validation.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_fruxon.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_guides.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_improvements.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_integration_triggers_cli.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_integrations_authorize_cli.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_knowledge_bases.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_knowledge_documents.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_knowledge_notices.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_knowledge_sources.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_llm_provider_configs_cli.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_managed_by.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_memory.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_messages.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_metrics.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_online_llm_config_refs.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_online_prefetch_refs.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_online_secret_refs.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_output.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_params.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_participants.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_participants_write.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_pipelines.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_reply_style.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_sandbox_scenario.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_schema.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_schema_fetch.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_secrets.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_skills.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_ssl.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_storage.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_system_blocks.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_system_blocks_cli.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_telemetry.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_test_chats.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_topics.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_trigger_budgets.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_triggers.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_triggers_ledger.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_triggers_ledger_batch_views.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_triggers_ledger_stage_tests.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_triggers_questions.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_triggers_shape.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_triggers_write.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_ui.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_update_check.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_validation.py +0 -0
- {fruxon-0.21.2 → fruxon-1.0.0}/tests/test_workspaces_cli.py +0 -0
|
@@ -1,5 +1,28 @@
|
|
|
1
1
|
# History
|
|
2
2
|
|
|
3
|
+
## Unreleased — edit and publish an integration config from the CLI
|
|
4
|
+
|
|
5
|
+
`integrations configs` could only `list` and `get`, so changing a config
|
|
6
|
+
(clearing Agentit's BAFI egress pin, 2026-10-19) meant the dashboard. Uses the
|
|
7
|
+
backend's existing `PATCH …/configs/{config}` and `POST …/configs/{config}:publish`.
|
|
8
|
+
|
|
9
|
+
- **`fruxon integrations configs update <integration> <config> --file <patch.json>`**
|
|
10
|
+
patches the config's draft. The file holds only the fields to change
|
|
11
|
+
(`{"egressCountry": ""}` clears an egress pin); `--schema` prints the shape.
|
|
12
|
+
The server ignores fields it doesn't know and reads `null` as "unchanged",
|
|
13
|
+
so an empty patch, an unknown field or a `null` exits 12 before anything is
|
|
14
|
+
sent. Prints the config with secrets masked by the server, never the values
|
|
15
|
+
sent.
|
|
16
|
+
- **`fruxon integrations configs publish <integration> <config> [--note T]`**
|
|
17
|
+
makes the draft the revision tool calls resolve. Shows what is live now and
|
|
18
|
+
asks; `--yes` skips it and is required in agent mode (exit 16 without).
|
|
19
|
+
Prints the new revision's `id` and `version`.
|
|
20
|
+
- `FruxonClient.update_integration_config(integration, config_id, body)` and
|
|
21
|
+
`publish_integration_config(integration, config_id, note=None)`. The
|
|
22
|
+
publish response is the snapshot with its credentials decrypted; the client
|
|
23
|
+
returns only `id`, `configId`, `version`, `note`, `sandboxMode`,
|
|
24
|
+
`egressCountry`, `createdBy` and `createdAt`.
|
|
25
|
+
|
|
3
26
|
## Unreleased — telemetry names the command, the person and the workspace
|
|
4
27
|
|
|
5
28
|
CLI usage in Mixpanel had three gaps. The command name stopped at two
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: fruxon
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 1.0.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
|
|
@@ -139,7 +139,7 @@ Integrations + tools:
|
|
|
139
139
|
| Command | What it does |
|
|
140
140
|
| --- | --- |
|
|
141
141
|
| `fruxon integrations list/get/create/update/verify/open` | Manage external integrations — the connections agents draw tools from. `create`/`update`/`verify` take a `--file` JSON body; `open` opens the dashboard. |
|
|
142
|
-
| `fruxon integrations configs list/get` |
|
|
142
|
+
| `fruxon integrations configs list/get/update/publish` | The per-integration auth/config records. `update --file` patches a config's draft (only the fields sent; `{"egressCountry": ""}` clears an egress pin) and prints it with secrets masked; `publish` makes the draft the revision tool calls resolve, asks first (`--yes` in agent mode), and prints the new revision's `id` and `version`, never the credentials. |
|
|
143
143
|
| `fruxon integrations authorize <integration>` | Mint an application-level OAuth authorization URL to **connect** an integration — auto-detects its OAuth2 method (`--auth-method` to pick, `--scope`/`--config-param` to tune). OAuth needs a browser consent step, so the CLI hands you the link; a human clicks it, and the connection is saved as a tenant config a slot can pin. |
|
|
144
144
|
| `fruxon integrations triggers <integration>` | List the event types an integration can fire an agent on — each descriptor's `id` is the `eventType` a trigger listens for, plus the `payloadFields` (dotted paths) a binding's `parameterMappings` can read. Discovery for wiring an inbound-event trigger without guessing. |
|
|
145
145
|
| `fruxon integrations mcp …` | Inspect and enable the MCP server for an integration. |
|
|
@@ -106,7 +106,7 @@ Integrations + tools:
|
|
|
106
106
|
| Command | What it does |
|
|
107
107
|
| --- | --- |
|
|
108
108
|
| `fruxon integrations list/get/create/update/verify/open` | Manage external integrations — the connections agents draw tools from. `create`/`update`/`verify` take a `--file` JSON body; `open` opens the dashboard. |
|
|
109
|
-
| `fruxon integrations configs list/get` |
|
|
109
|
+
| `fruxon integrations configs list/get/update/publish` | The per-integration auth/config records. `update --file` patches a config's draft (only the fields sent; `{"egressCountry": ""}` clears an egress pin) and prints it with secrets masked; `publish` makes the draft the revision tool calls resolve, asks first (`--yes` in agent mode), and prints the new revision's `id` and `version`, never the credentials. |
|
|
110
110
|
| `fruxon integrations authorize <integration>` | Mint an application-level OAuth authorization URL to **connect** an integration — auto-detects its OAuth2 method (`--auth-method` to pick, `--scope`/`--config-param` to tune). OAuth needs a browser consent step, so the CLI hands you the link; a human clicks it, and the connection is saved as a tenant config a slot can pin. |
|
|
111
111
|
| `fruxon integrations triggers <integration>` | List the event types an integration can fire an agent on — each descriptor's `id` is the `eventType` a trigger listens for, plus the `payloadFields` (dotted paths) a binding's `parameterMappings` can read. Discovery for wiring an inbound-event trigger without guessing. |
|
|
112
112
|
| `fruxon integrations mcp …` | Inspect and enable the MCP server for an integration. |
|
|
@@ -117,6 +117,13 @@ fruxon = "fruxon.cli:app"
|
|
|
117
117
|
[tool.ruff]
|
|
118
118
|
line-length = 120
|
|
119
119
|
|
|
120
|
+
[tool.ruff.format]
|
|
121
|
+
# Ruff 0.17 formats the Python code blocks inside Markdown too. Ours are
|
|
122
|
+
# illustrations, not code: hand-aligned comments, one-line calls and
|
|
123
|
+
# pseudo-signatures (``def f(...): ... # POST /route``) that the formatter
|
|
124
|
+
# would explode. Leave the docs as written.
|
|
125
|
+
exclude = ["*.md"]
|
|
126
|
+
|
|
120
127
|
[tool.ruff.lint]
|
|
121
128
|
select = [
|
|
122
129
|
"E", # pycodestyle errors
|
|
@@ -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 = '0.
|
|
22
|
-
__version_tuple__ = version_tuple = (
|
|
21
|
+
__version__ = version = '1.0.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 0, 0)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -72,8 +72,9 @@ def is_success_exit(exc: BaseException) -> bool:
|
|
|
72
72
|
"""Whether Click/Typer stopped successfully after help or version.
|
|
73
73
|
|
|
74
74
|
``--help`` and ``--version`` use a zero-code ``Exit`` instead of returning
|
|
75
|
-
normally.
|
|
76
|
-
|
|
75
|
+
normally. Which class that is depends on the Typer release: the top-level
|
|
76
|
+
Click one, the vendored ``typer._click`` copy (0.26), or ``typer.exceptions``
|
|
77
|
+
(0.27, which moved ``Exit`` and ``Abort`` out of the vendored Click).
|
|
77
78
|
"""
|
|
78
79
|
return _is_click_exception(exc, class_name="Exit") and getattr(exc, "exit_code", 1) == 0
|
|
79
80
|
|
|
@@ -84,22 +85,24 @@ def is_no_args_help(exc: BaseException) -> bool:
|
|
|
84
85
|
|
|
85
86
|
|
|
86
87
|
def _is_click_exception(exc: BaseException, *, class_name: str = "ClickException") -> bool:
|
|
87
|
-
"""Match Click's public exceptions and Typer's vendored equivalents.
|
|
88
|
-
import click
|
|
88
|
+
"""Match Click's public exceptions and Typer's vendored equivalents.
|
|
89
89
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
try:
|
|
96
|
-
from typer._click import exceptions as typer_click_exceptions
|
|
90
|
+
Typer 0.27 keeps ``Exit`` and ``Abort`` in :mod:`typer.exceptions` rather
|
|
91
|
+
than in its vendored Click, so a ``--help`` raised there matched neither
|
|
92
|
+
Click namespace and was reported as a failed command.
|
|
93
|
+
"""
|
|
94
|
+
import importlib
|
|
97
95
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
96
|
+
classes: list[type[BaseException]] = []
|
|
97
|
+
# Older Typer releases have neither of the last two and use top-level Click.
|
|
98
|
+
for module_name in ("click.exceptions", "typer._click.exceptions", "typer.exceptions"):
|
|
99
|
+
try:
|
|
100
|
+
module = importlib.import_module(module_name)
|
|
101
|
+
except ImportError:
|
|
102
|
+
continue
|
|
103
|
+
found = getattr(module, class_name, None)
|
|
104
|
+
if isinstance(found, type) and issubclass(found, BaseException):
|
|
105
|
+
classes.append(found)
|
|
103
106
|
|
|
104
107
|
return bool(classes) and isinstance(exc, tuple(classes))
|
|
105
108
|
|
|
@@ -700,6 +700,15 @@ _EXAMPLES: dict[str, list[tuple[str, str]]] = {
|
|
|
700
700
|
("fruxon integrations create -f config.json", "Wire up a new external service"),
|
|
701
701
|
("fruxon integrations open my-integration", "Open the dashboard config page"),
|
|
702
702
|
("fruxon integrations verify my-integration -f auth.json", "Test credentials"),
|
|
703
|
+
("fruxon integrations configs list my-integration", "Saved credentials — the tenantConfigId a slot pins"),
|
|
704
|
+
(
|
|
705
|
+
'echo \'{"egressCountry": ""}\' | fruxon integrations configs update my-integration e71e… -f -',
|
|
706
|
+
"Patch a config's draft (only the fields you send)",
|
|
707
|
+
),
|
|
708
|
+
(
|
|
709
|
+
"fruxon integrations configs publish my-integration e71e… --note '…'",
|
|
710
|
+
"Make the draft live for every tool call",
|
|
711
|
+
),
|
|
703
712
|
],
|
|
704
713
|
"tools": [
|
|
705
714
|
("fruxon tools list my-integration", "List tools under an integration"),
|
|
@@ -11,6 +11,10 @@ Read commands (``list`` / ``get``) take simple flags; write commands
|
|
|
11
11
|
because the payloads (``CreateIntegration``, ``VerifyAuthConfigRequest``)
|
|
12
12
|
carry structured ``configMetadata`` / ``authConfig`` objects, not
|
|
13
13
|
key=value pairs — same pattern as ``fruxon agents test``.
|
|
14
|
+
|
|
15
|
+
``configs`` covers a tenant's saved credentials for an integration:
|
|
16
|
+
``configs update`` patches a config's draft from a ``--file`` JSON body,
|
|
17
|
+
and ``configs publish`` ships the draft as the revision tool calls resolve.
|
|
14
18
|
"""
|
|
15
19
|
|
|
16
20
|
from __future__ import annotations
|
|
@@ -19,9 +23,10 @@ import json
|
|
|
19
23
|
from typing import Annotated
|
|
20
24
|
|
|
21
25
|
import typer
|
|
26
|
+
from rich.markup import escape
|
|
22
27
|
|
|
23
28
|
from fruxon.cli import app
|
|
24
|
-
from fruxon.cli._shared import build_client, load_json_body, print_connect_nudge
|
|
29
|
+
from fruxon.cli._shared import build_client, confirm_or_die, load_json_body, print_connect_nudge
|
|
25
30
|
from fruxon.exceptions import FruxonError
|
|
26
31
|
from fruxon.fruxon import Integration
|
|
27
32
|
from fruxon.ui import (
|
|
@@ -30,10 +35,12 @@ from fruxon.ui import (
|
|
|
30
35
|
fail_from_api_error,
|
|
31
36
|
integration_connect_url,
|
|
32
37
|
print_banner,
|
|
38
|
+
require_yes_in_agent_mode,
|
|
33
39
|
resolve_output_format,
|
|
34
40
|
say_err,
|
|
35
41
|
say_info,
|
|
36
42
|
say_ok,
|
|
43
|
+
say_warn,
|
|
37
44
|
stderr,
|
|
38
45
|
)
|
|
39
46
|
|
|
@@ -674,7 +681,7 @@ def integrations_authorize(
|
|
|
674
681
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
675
682
|
|
|
676
683
|
configs_app = typer.Typer(
|
|
677
|
-
help="List and
|
|
684
|
+
help="List, inspect, update and publish tenant-level integration configs (reusable auth + connection settings).",
|
|
678
685
|
no_args_is_help=True,
|
|
679
686
|
)
|
|
680
687
|
integrations_app.add_typer(configs_app, name="configs")
|
|
@@ -781,6 +788,270 @@ def integration_configs_get(
|
|
|
781
788
|
print(json.dumps(body, indent=2))
|
|
782
789
|
|
|
783
790
|
|
|
791
|
+
# The top-level fields of ``UpdateTenantIntegrationConfigRequest``. The server
|
|
792
|
+
# drops a key it doesn't know without a word, so a typo (``egress_country``)
|
|
793
|
+
# would "succeed" and change nothing; checking here turns that into an error.
|
|
794
|
+
# It binds case-insensitively, so this matches that way too.
|
|
795
|
+
_CONFIG_UPDATE_FIELDS = (
|
|
796
|
+
"displayName",
|
|
797
|
+
"parameters",
|
|
798
|
+
"auth",
|
|
799
|
+
"authMap",
|
|
800
|
+
"sandboxMode",
|
|
801
|
+
"sandboxVariant",
|
|
802
|
+
"egressCountry",
|
|
803
|
+
"oAuthCredentialKey",
|
|
804
|
+
)
|
|
805
|
+
_PUBLISH_CONFIG_ACTION = "publish an integration config"
|
|
806
|
+
|
|
807
|
+
|
|
808
|
+
@configs_app.command("update")
|
|
809
|
+
def integration_configs_update(
|
|
810
|
+
integration: Annotated[str, typer.Argument(help="Integration the config belongs to.")],
|
|
811
|
+
config_id: Annotated[str, typer.Argument(help="Config UUID to update.")],
|
|
812
|
+
file: Annotated[
|
|
813
|
+
str | None,
|
|
814
|
+
typer.Option(
|
|
815
|
+
"--file",
|
|
816
|
+
"-f",
|
|
817
|
+
help="UpdateTenantIntegrationConfigRequest JSON body: only the fields to change. `-` reads stdin.",
|
|
818
|
+
),
|
|
819
|
+
] = None,
|
|
820
|
+
schema: Annotated[
|
|
821
|
+
bool,
|
|
822
|
+
typer.Option(
|
|
823
|
+
"--schema", help="Print the JSON schema for --file (UpdateTenantIntegrationConfigRequest) and exit."
|
|
824
|
+
),
|
|
825
|
+
] = False,
|
|
826
|
+
workspace: Annotated[str | None, typer.Option("--workspace", help="Workspace identifier override.")] = None,
|
|
827
|
+
base_url: Annotated[str | None, typer.Option("--base-url", help="API base URL override.")] = None,
|
|
828
|
+
output: Annotated[
|
|
829
|
+
str | None,
|
|
830
|
+
typer.Option("--output", "-o", help="Output format: text (default for humans), json (default in agent mode)."),
|
|
831
|
+
] = None,
|
|
832
|
+
):
|
|
833
|
+
"""Update a config's draft from a JSON patch. Nothing changes at run time until `publish`.
|
|
834
|
+
|
|
835
|
+
`--file` holds only the fields to change. Fields you leave out keep their
|
|
836
|
+
saved values; `parameters`, `auth` and `authMap` replace the saved value
|
|
837
|
+
whole when present, so copy them from `configs get` and send a secret's
|
|
838
|
+
masked value back unchanged to keep it. `"egressCountry": ""` clears the
|
|
839
|
+
egress pin; `null` means "leave unchanged", so it is refused here rather
|
|
840
|
+
than silently doing nothing.
|
|
841
|
+
|
|
842
|
+
Tool calls resolve the config's *published* revision, not its draft:
|
|
843
|
+
follow with `configs publish` to make the change live.
|
|
844
|
+
|
|
845
|
+
Prints the updated config with secrets masked by the server, the same
|
|
846
|
+
shape as `configs get` — never the values you sent. Prefer the dashboard
|
|
847
|
+
for entering new credentials: a secret in `--file` sits on disk.
|
|
848
|
+
|
|
849
|
+
Needs the `integrations:write` scope.
|
|
850
|
+
|
|
851
|
+
Examples:
|
|
852
|
+
echo '{"egressCountry": ""}' | fruxon integrations configs update bafi <config-id> --file -
|
|
853
|
+
fruxon integrations configs update github <config-id> --file ./patch.json
|
|
854
|
+
fruxon integrations configs update github <config-id> --schema > schema.json
|
|
855
|
+
"""
|
|
856
|
+
from fruxon.cli._schema import print_schema
|
|
857
|
+
|
|
858
|
+
if schema:
|
|
859
|
+
print_schema("UpdateTenantIntegrationConfigRequest", workspace=workspace, base_url=base_url)
|
|
860
|
+
return
|
|
861
|
+
if not file:
|
|
862
|
+
fail(
|
|
863
|
+
"Missing required option [bold]--file[/bold].",
|
|
864
|
+
hint="Pass a JSON file with the fields to change, or [bold]--schema[/bold] to see the shape.",
|
|
865
|
+
code=EXIT_VALIDATION,
|
|
866
|
+
)
|
|
867
|
+
|
|
868
|
+
output = _resolve_write_output(output)
|
|
869
|
+
body = load_json_body(file, what="an UpdateTenantIntegrationConfigRequest object (the fields to change)")
|
|
870
|
+
_check_config_patch(body)
|
|
871
|
+
|
|
872
|
+
client = build_client(workspace, base_url)
|
|
873
|
+
try:
|
|
874
|
+
config = client.update_integration_config(integration, config_id, body)
|
|
875
|
+
except FruxonError as e:
|
|
876
|
+
fail_from_api_error(e, hint=_hint_for_config_error(e, integration))
|
|
877
|
+
|
|
878
|
+
publish_cmd = f"fruxon integrations configs publish {integration} {config_id}"
|
|
879
|
+
published = config.get("publishedRevisionId")
|
|
880
|
+
not_live = (
|
|
881
|
+
f"Not live yet: tool calls still resolve published revision {published}."
|
|
882
|
+
if published
|
|
883
|
+
else "This config has never been published, so no tool call resolves it yet."
|
|
884
|
+
)
|
|
885
|
+
if output == "json":
|
|
886
|
+
print(json.dumps(config, indent=2))
|
|
887
|
+
stderr.print(f"[dim]Draft updated. {not_live} Publish with: {publish_cmd}[/dim]")
|
|
888
|
+
return
|
|
889
|
+
|
|
890
|
+
name = config.get("displayName") or config_id
|
|
891
|
+
if stderr.is_terminal:
|
|
892
|
+
print_banner(subtitle=f"config · {integration}")
|
|
893
|
+
stderr.print()
|
|
894
|
+
say_ok(
|
|
895
|
+
f"Updated the draft of [bold]{escape(name)}[/bold] ({config_id}) · changed: {', '.join(body)}",
|
|
896
|
+
hint=f"{not_live} Publish with [bold]{publish_cmd}[/bold].",
|
|
897
|
+
)
|
|
898
|
+
|
|
899
|
+
|
|
900
|
+
@configs_app.command("publish")
|
|
901
|
+
def integration_configs_publish(
|
|
902
|
+
integration: Annotated[str, typer.Argument(help="Integration the config belongs to.")],
|
|
903
|
+
config_id: Annotated[str, typer.Argument(help="Config UUID to publish.")],
|
|
904
|
+
note: Annotated[
|
|
905
|
+
str | None,
|
|
906
|
+
typer.Option("--note", help="Publish note stored on the new revision (e.g. 'cleared egress pin')."),
|
|
907
|
+
] = None,
|
|
908
|
+
yes: Annotated[
|
|
909
|
+
bool,
|
|
910
|
+
typer.Option("--yes", "-y", help="Skip the confirmation prompt. REQUIRED under agent mode (CLAUDECODE / CI)."),
|
|
911
|
+
] = False,
|
|
912
|
+
workspace: Annotated[str | None, typer.Option("--workspace", help="Workspace identifier override.")] = None,
|
|
913
|
+
base_url: Annotated[str | None, typer.Option("--base-url", help="API base URL override.")] = None,
|
|
914
|
+
output: Annotated[
|
|
915
|
+
str | None,
|
|
916
|
+
typer.Option("--output", "-o", help="Output format: text (default for humans), json (default in agent mode)."),
|
|
917
|
+
] = None,
|
|
918
|
+
):
|
|
919
|
+
"""Publish a config's draft as a new revision. Tool calls switch to it immediately.
|
|
920
|
+
|
|
921
|
+
Snapshots the draft (parameters, auth, authMap, sandbox routing, egress
|
|
922
|
+
country) into a new, immutable revision and points the config's
|
|
923
|
+
`publishedRevisionId` at it. Every agent and trigger resolving this config
|
|
924
|
+
uses the new revision from its next tool call. To revert, roll back to an
|
|
925
|
+
earlier revision on the config's dashboard page.
|
|
926
|
+
|
|
927
|
+
Prints the new revision's `id` and `version` (plus `configId`, `note`,
|
|
928
|
+
`sandboxMode`, `egressCountry`, `createdBy`, `createdAt`). The server's
|
|
929
|
+
response also carries the snapshotted credentials; they are dropped, never
|
|
930
|
+
printed.
|
|
931
|
+
|
|
932
|
+
Asks for confirmation; `--yes` skips it and is required in agent mode.
|
|
933
|
+
Needs the `integrations:write` scope.
|
|
934
|
+
|
|
935
|
+
Examples:
|
|
936
|
+
fruxon integrations configs publish bafi <config-id> --note 'cleared egress pin'
|
|
937
|
+
fruxon integrations configs publish github <config-id> --yes -o json
|
|
938
|
+
"""
|
|
939
|
+
output = _resolve_write_output(output)
|
|
940
|
+
# Before any network call: an agent without --yes can't be asked.
|
|
941
|
+
require_yes_in_agent_mode(yes, action=_PUBLISH_CONFIG_ACTION)
|
|
942
|
+
client = build_client(workspace, base_url)
|
|
943
|
+
# Read it first, so the prompt names the config and what is live now —
|
|
944
|
+
# and so a wrong id fails before anyone is asked to confirm anything.
|
|
945
|
+
try:
|
|
946
|
+
config = client.get_integration_config(integration, config_id)
|
|
947
|
+
except FruxonError as e:
|
|
948
|
+
fail_from_api_error(e, hint=_hint_for_config_error(e, integration))
|
|
949
|
+
|
|
950
|
+
name = config.get("displayName") or config_id
|
|
951
|
+
if output == "text" or not yes:
|
|
952
|
+
if output == "text" and stderr.is_terminal:
|
|
953
|
+
print_banner(subtitle=f"config publish · {integration}")
|
|
954
|
+
stderr.print()
|
|
955
|
+
_render_config_publish_preview(config, integration, config_id)
|
|
956
|
+
|
|
957
|
+
confirm_or_die(
|
|
958
|
+
f"Publish the draft of [bold]{escape(name)}[/bold] as a new revision? "
|
|
959
|
+
"Every tool call using this config switches to it immediately.",
|
|
960
|
+
yes=yes,
|
|
961
|
+
action=_PUBLISH_CONFIG_ACTION,
|
|
962
|
+
)
|
|
963
|
+
|
|
964
|
+
try:
|
|
965
|
+
if stderr.is_terminal and output == "text":
|
|
966
|
+
with stderr.status(f"[bold]Publishing [cyan]{escape(name)}[/cyan]…[/bold]"):
|
|
967
|
+
revision = client.publish_integration_config(integration, config_id, note=note)
|
|
968
|
+
else:
|
|
969
|
+
revision = client.publish_integration_config(integration, config_id, note=note)
|
|
970
|
+
except FruxonError as e:
|
|
971
|
+
fail_from_api_error(e, hint=_hint_for_config_error(e, integration))
|
|
972
|
+
|
|
973
|
+
if output == "json":
|
|
974
|
+
print(json.dumps(revision, indent=2))
|
|
975
|
+
return
|
|
976
|
+
|
|
977
|
+
version = revision.get("version")
|
|
978
|
+
stderr.print()
|
|
979
|
+
say_ok(
|
|
980
|
+
f"Published [bold]{escape(name)}[/bold] ({config_id}) as revision "
|
|
981
|
+
f"[bold]v{version if version is not None else '?'}[/bold] [dim]{revision.get('id') or '—'}[/dim]",
|
|
982
|
+
hint=(
|
|
983
|
+
"Tool calls resolving this config use it from their next call. "
|
|
984
|
+
"To revert, roll back to an earlier revision on the config's dashboard page."
|
|
985
|
+
),
|
|
986
|
+
)
|
|
987
|
+
|
|
988
|
+
|
|
989
|
+
def _check_config_patch(body: dict) -> None:
|
|
990
|
+
"""Refuse a patch the server would accept and then ignore, before sending it."""
|
|
991
|
+
known = {field.lower() for field in _CONFIG_UPDATE_FIELDS}
|
|
992
|
+
fields = ", ".join(_CONFIG_UPDATE_FIELDS)
|
|
993
|
+
if not body:
|
|
994
|
+
fail(
|
|
995
|
+
"Nothing to update: the patch is an empty object.",
|
|
996
|
+
hint=f'Send only the fields to change, e.g. {{"egressCountry": ""}}. Fields: {fields}.',
|
|
997
|
+
code=EXIT_VALIDATION,
|
|
998
|
+
)
|
|
999
|
+
unknown = [key for key in body if key.lower() not in known]
|
|
1000
|
+
if unknown:
|
|
1001
|
+
them = "them" if len(unknown) != 1 else "it"
|
|
1002
|
+
fail(
|
|
1003
|
+
f"Unknown field(s) in the patch: {escape(', '.join(unknown))}.",
|
|
1004
|
+
hint=(
|
|
1005
|
+
f"The server would ignore {them} and change nothing. Fields: {fields}. "
|
|
1006
|
+
"Read-only fields from `configs get` (id, hasAuth, …) don't belong here."
|
|
1007
|
+
),
|
|
1008
|
+
code=EXIT_VALIDATION,
|
|
1009
|
+
)
|
|
1010
|
+
nulls = [key for key, value in body.items() if value is None]
|
|
1011
|
+
if nulls:
|
|
1012
|
+
them = "them" if len(nulls) != 1 else "it"
|
|
1013
|
+
clear = ' Send "" to clear egressCountry.' if "egresscountry" in {key.lower() for key in nulls} else ""
|
|
1014
|
+
fail(
|
|
1015
|
+
f"null leaves a field unchanged, so this would do nothing for: {escape(', '.join(nulls))}.",
|
|
1016
|
+
hint=f"Drop {them} from the patch.{clear}",
|
|
1017
|
+
code=EXIT_VALIDATION,
|
|
1018
|
+
)
|
|
1019
|
+
|
|
1020
|
+
|
|
1021
|
+
def _render_config_publish_preview(config: dict, integration: str, config_id: str) -> None:
|
|
1022
|
+
"""What is about to be published, and what is live now."""
|
|
1023
|
+
say_info(f"[bold]{escape(config.get('displayName') or config_id)}[/bold] [dim]{config_id} · {integration}[/dim]")
|
|
1024
|
+
published = config.get("publishedRevisionId")
|
|
1025
|
+
_say_kv(
|
|
1026
|
+
"live now",
|
|
1027
|
+
f"revision [bold]{published}[/bold]"
|
|
1028
|
+
if published
|
|
1029
|
+
else "[yellow]nothing — never published, so no tool call resolves it yet[/yellow]",
|
|
1030
|
+
)
|
|
1031
|
+
if config.get("egressCountry"):
|
|
1032
|
+
_say_kv("egress", f"{escape(str(config['egressCountry']))} [dim](draft)[/dim]")
|
|
1033
|
+
health = config.get("credentialHealth")
|
|
1034
|
+
if isinstance(health, str) and health and health.upper() != "HEALTHY":
|
|
1035
|
+
detail = config.get("credentialHealthDetail")
|
|
1036
|
+
say_warn(f"Credential health: {escape(health)}", hint=escape(str(detail)) if detail else None)
|
|
1037
|
+
|
|
1038
|
+
|
|
1039
|
+
def _hint_for_config_error(error: FruxonError, integration: str) -> str | None:
|
|
1040
|
+
from fruxon.exceptions import NotFoundError
|
|
1041
|
+
from fruxon.ui import hint_for_forbidden
|
|
1042
|
+
|
|
1043
|
+
if isinstance(error, NotFoundError):
|
|
1044
|
+
return (
|
|
1045
|
+
f"No such config under [bold]{integration}[/bold] — list them with "
|
|
1046
|
+
f"[bold]fruxon integrations configs list {integration}[/bold]."
|
|
1047
|
+
)
|
|
1048
|
+
# Before _hint_for_error, whose generic 403 line would hide the missing scope.
|
|
1049
|
+
forbidden_hint = hint_for_forbidden(error)
|
|
1050
|
+
if forbidden_hint is not None:
|
|
1051
|
+
return forbidden_hint
|
|
1052
|
+
return _hint_for_error(error)
|
|
1053
|
+
|
|
1054
|
+
|
|
784
1055
|
def _build_configs_table(configs: list[dict]):
|
|
785
1056
|
from rich.table import Table
|
|
786
1057
|
|
|
@@ -217,6 +217,21 @@ _MAX_PAGE_SIZE = 100
|
|
|
217
217
|
# :meth:`FruxonClient.list_participants`.
|
|
218
218
|
_VALID_PARTICIPANT_TIER = frozenset({"CURATED", "AMBIENT"})
|
|
219
219
|
|
|
220
|
+
# What :meth:`FruxonClient.publish_integration_config` keeps of the revision
|
|
221
|
+
# the server returns. That response is the snapshot itself, credentials
|
|
222
|
+
# decrypted, so this lists what may leave rather than what may not: a field
|
|
223
|
+
# the server adds later stays out until someone decides it is safe.
|
|
224
|
+
_PUBLISHED_INTEGRATION_REVISION_FIELDS = (
|
|
225
|
+
"id",
|
|
226
|
+
"configId",
|
|
227
|
+
"version",
|
|
228
|
+
"note",
|
|
229
|
+
"sandboxMode",
|
|
230
|
+
"egressCountry",
|
|
231
|
+
"createdBy",
|
|
232
|
+
"createdAt",
|
|
233
|
+
)
|
|
234
|
+
|
|
220
235
|
_STATUS_EXCEPTIONS: dict[int, type[FruxonAPIError]] = {
|
|
221
236
|
400: ValidationError,
|
|
222
237
|
401: AuthenticationError,
|
|
@@ -2565,6 +2580,60 @@ class FruxonClient:
|
|
|
2565
2580
|
)
|
|
2566
2581
|
return raw
|
|
2567
2582
|
|
|
2583
|
+
def update_integration_config(self, integration: str, config_id: str, body: dict[str, object]) -> dict:
|
|
2584
|
+
"""Patch a tenant-level integration config's draft.
|
|
2585
|
+
|
|
2586
|
+
Hits ``PATCH /v1/tenants/{tenant}/integrations/{integration}/configs/{configId}``
|
|
2587
|
+
with an ``UpdateTenantIntegrationConfigRequest`` (scope
|
|
2588
|
+
``integrations:write``). Only the draft changes: tool calls keep
|
|
2589
|
+
resolving the published revision until :meth:`publish_integration_config`.
|
|
2590
|
+
|
|
2591
|
+
Top-level fields left out of ``body`` keep their saved values.
|
|
2592
|
+
``parameters``, ``auth`` and ``authMap`` replace the saved value whole
|
|
2593
|
+
when present; send a secret's masked value from
|
|
2594
|
+
:meth:`get_integration_config` back unchanged to keep it.
|
|
2595
|
+
``egressCountry: ""`` clears the egress pin.
|
|
2596
|
+
|
|
2597
|
+
Returns the updated config in the shape :meth:`get_integration_config`
|
|
2598
|
+
returns, secrets masked by the server.
|
|
2599
|
+
"""
|
|
2600
|
+
url = self._integration_url(integration, f"/configs/{config_id}")
|
|
2601
|
+
raw = self._put_or_patch(url, body, method="PATCH")
|
|
2602
|
+
if not isinstance(raw, dict):
|
|
2603
|
+
raise FruxonAPIError(
|
|
2604
|
+
status=200,
|
|
2605
|
+
title="Unexpected response",
|
|
2606
|
+
detail="Expected an integration config object.",
|
|
2607
|
+
)
|
|
2608
|
+
return raw
|
|
2609
|
+
|
|
2610
|
+
def publish_integration_config(self, integration: str, config_id: str, *, note: str | None = None) -> dict:
|
|
2611
|
+
"""Publish a config's current draft as a new revision.
|
|
2612
|
+
|
|
2613
|
+
Hits ``POST /v1/tenants/{tenant}/integrations/{integration}/configs/{configId}:publish``
|
|
2614
|
+
(scope ``integrations:write``). Snapshots the draft into a new,
|
|
2615
|
+
immutable revision and points the config's ``publishedRevisionId`` at
|
|
2616
|
+
it; tool calls resolving this config use it from their next call.
|
|
2617
|
+
|
|
2618
|
+
The server echoes the whole snapshot back, credentials included and
|
|
2619
|
+
unmasked. This returns only the revision's identity and non-secret
|
|
2620
|
+
fields: ``id``, ``configId``, ``version``, ``note``, ``sandboxMode``,
|
|
2621
|
+
``egressCountry``, ``createdBy`` and ``createdAt``, each when the
|
|
2622
|
+
server sent it.
|
|
2623
|
+
"""
|
|
2624
|
+
url = self._integration_url(integration, f"/configs/{config_id}:publish")
|
|
2625
|
+
body: dict[str, object] = {}
|
|
2626
|
+
if note:
|
|
2627
|
+
body["note"] = note
|
|
2628
|
+
result = self._post_json(url, body)
|
|
2629
|
+
if not isinstance(result, dict):
|
|
2630
|
+
raise FruxonAPIError(
|
|
2631
|
+
status=200,
|
|
2632
|
+
title="Unexpected response",
|
|
2633
|
+
detail="Expected an integration config revision object.",
|
|
2634
|
+
)
|
|
2635
|
+
return {key: result[key] for key in _PUBLISHED_INTEGRATION_REVISION_FIELDS if key in result}
|
|
2636
|
+
|
|
2568
2637
|
# -------------------------------------------------------------------- mcp
|
|
2569
2638
|
|
|
2570
2639
|
def get_mcp_config(self, integration: str) -> dict:
|
|
@@ -103,6 +103,7 @@ the bypass flag.
|
|
|
103
103
|
| `triggers fire` (no --yes) | Pass `--yes` — it runs the bound agent(s) now (cost + side effects). |
|
|
104
104
|
| `triggers ledger walk` (no --yes) | Pass `--yes` — every step is a paid model run on the agent's test budget (up to `--max-steps`, default 10). It writes nothing; read `stopped.code` in the JSON, since it exits 0 whenever it ran. |
|
|
105
105
|
| `triggers delete` / `unbind` (no --yes) | Pass `--yes` — removes the trigger / its agent wiring. |
|
|
106
|
+
| `integrations configs publish` (no --yes) | Pass `--yes` — **but only on the human's say-so.** Every tool call that resolves the config switches to the new revision immediately. |
|
|
106
107
|
| `participants delete` (no --yes) | Pass `--yes` — removes the participant from the network. |
|
|
107
108
|
| `capabilities delete` (no --yes) | Pass `--yes` — removes the capability from the routing vocabulary. |
|
|
108
109
|
| `participants unbind` (no --yes) | Pass `--yes` — removes the participant from the agent's consult roster. |
|
|
@@ -64,6 +64,7 @@ fruxon agents sandbox test <file|dir> # run scenario e2e tests of an agent (exi
|
|
|
64
64
|
fruxon integrations list / create
|
|
65
65
|
fruxon integrations authorize <integration> # mint an OAuth connect link for a human to click (auto-detects the method)
|
|
66
66
|
fruxon integrations triggers <integration> # event types (+ payload paths) an integration can fire an agent on
|
|
67
|
+
fruxon integrations configs list / get / update / publish <integration> # saved credentials; update edits the draft, publish makes it live
|
|
67
68
|
fruxon tools list <integration> / create
|
|
68
69
|
fruxon assets create/list/get/wait/delete # the indexed RAG assets an agent's assetConfig attaches
|
|
69
70
|
fruxon assets search/documents/chunks <a> # what the INDEX holds — distinct from the authored corpus below
|
|
@@ -27,6 +27,14 @@ You are now operating as a Fruxon integrations specialist.
|
|
|
27
27
|
needs. `fruxon integrations configs get <integration> <config>`
|
|
28
28
|
prints one config's full payload (auth shape, `publishedRevisionId`)
|
|
29
29
|
— use it to confirm a config is published before pointing a slot at it.
|
|
30
|
+
- `fruxon integrations configs update <integration> <config> --file <patch.json>`
|
|
31
|
+
changes a config's **draft**: the file holds only the fields to change
|
|
32
|
+
(e.g. `{"egressCountry": ""}` clears an egress pin). Tool calls keep
|
|
33
|
+
resolving the **published** revision, so follow with
|
|
34
|
+
`fruxon integrations configs publish <integration> <config> --yes`, which
|
|
35
|
+
every agent using the config picks up on its next call — ask the human
|
|
36
|
+
first. Don't collect secrets in chat to put in a patch: credentials are
|
|
37
|
+
entered in the dashboard.
|
|
30
38
|
- `fruxon integrations get <id>` — catalog metadata for one
|
|
31
39
|
integration (description, tags). (It does not report auth status —
|
|
32
40
|
use `configs list` for that.)
|
|
@@ -194,6 +194,9 @@ def test_the_doc_documents_something():
|
|
|
194
194
|
# expanded — the blind spot that hid `agents test`.
|
|
195
195
|
assert "knowledge-bases source preview" in documented
|
|
196
196
|
assert "agents executions trace" in documented
|
|
197
|
+
# Expanded from the meet guide's `list / get / update / publish`.
|
|
198
|
+
assert "integrations configs update" in documented
|
|
199
|
+
assert "integrations configs publish" in documented
|
|
197
200
|
# The three removed commands must not come back into the page.
|
|
198
201
|
assert not any(d.split()[0] in {"run", "chat"} for d in documented), documented
|
|
199
202
|
assert not any(d == "agents test" or d.startswith("agents test ") for d in documented), documented
|