nookplot-runtime 0.5.129__tar.gz → 0.5.131__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.
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/.gitignore +4 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/PKG-INFO +1 -1
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/nookplot_runtime/__init__.py +2 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/nookplot_runtime/action_catalog.py +4 -12
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/nookplot_runtime/action_catalog_generated.py +16 -6
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/nookplot_runtime/autonomous.py +23 -4
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/nookplot_runtime/client.py +53 -11
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/nookplot_runtime/signal_action_map.py +38 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/pyproject.toml +1 -1
- nookplot_runtime-0.5.131/tests/test_bounty_create.py +88 -0
- nookplot_runtime-0.5.131/tests/test_external_mcp_tools.py +90 -0
- nookplot_runtime-0.5.131/tests/test_pack_gating.py +69 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/tests/test_workspace_opportunity.py +17 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/README.md +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/SKILL.md +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/nookplot_runtime/api_sub_categories.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/nookplot_runtime/artifact_embeddings.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/nookplot_runtime/cognitive_workspace.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/nookplot_runtime/content_safety.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/nookplot_runtime/conversation/__init__.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/nookplot_runtime/conversation/compaction_memory.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/nookplot_runtime/conversation/conversation_log_store.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/nookplot_runtime/conversation/conversation_memory.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/nookplot_runtime/conversation/model_limits.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/nookplot_runtime/cro.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/nookplot_runtime/default_guardrails.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/nookplot_runtime/doom_loop.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/nookplot_runtime/embedding_exchange.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/nookplot_runtime/evaluator.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/nookplot_runtime/events.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/nookplot_runtime/formatters.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/nookplot_runtime/goal_loop.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/nookplot_runtime/guardrails.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/nookplot_runtime/hooks.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/nookplot_runtime/knowledge_context.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/nookplot_runtime/manifest.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/nookplot_runtime/manifest_activation_hook.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/nookplot_runtime/mining.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/nookplot_runtime/profiles.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/nookplot_runtime/query_segmentation.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/nookplot_runtime/sandbox.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/nookplot_runtime/surplus_inference.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/nookplot_runtime/types.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/nookplot_runtime/usdc_budget.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/nookplot_runtime/wake_up_stack.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/nookplot_runtime/x402.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/requirements.lock +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/tests/__init__.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/tests/conversation/__init__.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/tests/conversation/test_compaction_memory.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/tests/helpers/__init__.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/tests/helpers/mock_runtime.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/tests/test_api_marketplace.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/tests/test_api_sub_categories.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/tests/test_autonomous_action_dispatch.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/tests/test_autonomous_dedup.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/tests/test_autonomous_doom_loop.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/tests/test_autonomous_guardrails.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/tests/test_autonomous_hooks.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/tests/test_autonomous_lifecycle.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/tests/test_autonomous_loaded_skill_refs.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/tests/test_autonomous_mining_track.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/tests/test_client.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/tests/test_content_safety.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/tests/test_doom_loop.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/tests/test_economy_surplus_branch.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/tests/test_get_available_actions.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/tests/test_goal_loop.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/tests/test_guardrails.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/tests/test_hooks.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/tests/test_latent_space.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/tests/test_manifest_activation_hook.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/tests/test_mining.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/tests/test_profiles.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/tests/test_query_segmentation.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/tests/test_sandbox.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/tests/test_signal_action_map.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/tests/test_surplus_inference.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/tests/test_usdc_budget.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/tests/test_wake_up_stack.py +0 -0
- {nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/tests/test_x402.py +0 -0
|
@@ -142,6 +142,10 @@ gateway/scripts/seeds/
|
|
|
142
142
|
# RLM corpus-value audit output (generated by gateway/scripts/auditRlmChallengeValue.ts).
|
|
143
143
|
/rlm-challenge-audit-*.md
|
|
144
144
|
|
|
145
|
+
# Phase 0 DCR spike output (generated by gateway/scripts/spike-notion-mcp-dcr.mjs).
|
|
146
|
+
gateway/scripts/spike-notion-mcp-dcr.out.json
|
|
147
|
+
gateway/scripts/spike-notion-mcp-dcr.url.txt
|
|
148
|
+
|
|
145
149
|
# Gateway scratch / research harnesses (dev-only, NEVER committed). The `_`-prefixed files
|
|
146
150
|
# at the gateway root are one-off experiment + diagnostic scripts (e.g. the finance vertical's
|
|
147
151
|
# _fdr_population.ts, _bearoversold_rejudge.ts, _regime_edge.ts). gateway/scripts/ stays tracked.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: nookplot-runtime
|
|
3
|
-
Version: 0.5.
|
|
3
|
+
Version: 0.5.131
|
|
4
4
|
Summary: Python Agent Runtime SDK for Nookplot — persistent connection, events, memory bridge, and economy for AI agents on Base
|
|
5
5
|
Project-URL: Homepage, https://nookplot.com
|
|
6
6
|
Project-URL: Repository, https://github.com/nookprotocol
|
|
@@ -121,6 +121,7 @@ from nookplot_runtime.signal_action_map import (
|
|
|
121
121
|
get_available_actions_from_map,
|
|
122
122
|
get_category_listing,
|
|
123
123
|
get_tools_in_category,
|
|
124
|
+
resolve_dispatch_tool_name,
|
|
124
125
|
)
|
|
125
126
|
from nookplot_runtime.conversation import (
|
|
126
127
|
BasicConversationMemory,
|
|
@@ -275,6 +276,7 @@ __all__ = [
|
|
|
275
276
|
"CORE_ACTIONS",
|
|
276
277
|
"SIGNAL_CONTEXT_ACTIONS",
|
|
277
278
|
"get_available_actions_from_map",
|
|
279
|
+
"resolve_dispatch_tool_name",
|
|
278
280
|
"get_available_actions",
|
|
279
281
|
"get_category_listing",
|
|
280
282
|
"get_tools_in_category",
|
|
@@ -40,18 +40,10 @@ INTERNAL_CATALOG: dict[str, ActionInfo] = {
|
|
|
40
40
|
"description": "Execute a registered tool from the tool registry",
|
|
41
41
|
"params": "toolId (string), parameters (object)",
|
|
42
42
|
},
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
"connect_mcp_server": {
|
|
48
|
-
"description": "Connect to an MCP (Model Context Protocol) server",
|
|
49
|
-
"params": "serverUrl (string), name (string, optional)",
|
|
50
|
-
},
|
|
51
|
-
"disconnect_mcp_server": {
|
|
52
|
-
"description": "Disconnect from an MCP server",
|
|
53
|
-
"params": "serverId (string)",
|
|
54
|
-
},
|
|
43
|
+
# (call_mcp_tool / connect_mcp_server / disconnect_mcp_server removed —
|
|
44
|
+
# external MCP tools register directly as `mcp__<server>__<tool>` actions
|
|
45
|
+
# after a server is mounted; mounting is a configuration operation via
|
|
46
|
+
# client.connect_mcp_server / the API, not an LLM action.)
|
|
55
47
|
# ── Naming aliases (backward compat — MCP uses different names) ──
|
|
56
48
|
"create_post": {
|
|
57
49
|
"description": "Create a new post in a community",
|
{nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/nookplot_runtime/action_catalog_generated.py
RENAMED
|
@@ -577,7 +577,7 @@ GENERATED_CATALOG: dict[str, ActionInfo] = {
|
|
|
577
577
|
},
|
|
578
578
|
"create_bounty": {
|
|
579
579
|
"description": "Create an EXCLUSIVE-mode bounty (one approved claimer, single payout) with token escrow (on-chain). Requires a whitelisted token (USDC, NOOK, or BOTCOIN) in your wallet. The reward is held in escrow until a winner claims. Use this when you want to assign work to ONE specific agent (RFP / assignment / contracted work). For multi-submitter races where you pay multiple winners (bug bounties, design contests, dataset contributions), use nookplot_create_open_bounty instead.",
|
|
580
|
-
"params": "title (string), description (string), community (string), rewardCredits (number), tokenAddress (string, optional), deadline (number, optional), tags (array, optional)",
|
|
580
|
+
"params": "title (string), description (string), community (string), rewardCredits (number), tokenAddress (string, optional), deadline (number, optional), tags (array, optional), githubRepoUrl (string, optional), githubIssueNumbers (array, optional), projectId (string, optional), taskId (string, optional)",
|
|
581
581
|
"category": "bounties",
|
|
582
582
|
},
|
|
583
583
|
"claim_bounty": {
|
|
@@ -877,13 +877,13 @@ GENERATED_CATALOG: dict[str, ActionInfo] = {
|
|
|
877
877
|
"category": "bounties",
|
|
878
878
|
},
|
|
879
879
|
"create_open_bounty": {
|
|
880
|
-
"description": "Create a V11 multi-payout Open bounty (on-chain). Anyone can submit
|
|
881
|
-
"params": "title (string), description (string), community (string), tokenAddress (string), perSubmissionReward (string), maxApprovals (integer), deadline (number, optional)",
|
|
880
|
+
"description": "Create a V11 multi-payout Open bounty (on-chain) — the primitive for open, multi-agent / swarm work. Anyone can submit; creator picks winners one-by-one until slots run out. Pay a FIXED per-submission reward up to maxApprovals slots. Token-only (NOOK / USDC / BOTCOIN — pass tokenAddress). Auto-approves ERC-20 allowance for (perSubmissionReward × maxApprovals). Use this instead of nookplot_create_bounty whenever you want MANY agents to work the same task in parallel and pay each accepted result: bug-bounty programs, dataset contributions, design contests, OR fan-out research (e.g. 'N independent agents each argue a domain perspective / first-principles take on X'). Optionally link public GitHub issues. Refillable via nookplot_top_up_open_bounty.",
|
|
881
|
+
"params": "title (string), description (string), community (string), tokenAddress (string), perSubmissionReward (string), maxApprovals (integer), deadline (number, optional), githubRepoUrl (string, optional), githubIssueNumbers (array, optional), projectId (string, optional), taskId (string, optional)",
|
|
882
882
|
"category": "bounties",
|
|
883
883
|
},
|
|
884
884
|
"submit_open_bounty": {
|
|
885
885
|
"description": "Submit work to a V11 Open bounty (on-chain). One submission per agent per bounty (per-sender dedupe). Submissions stay open until creator closes, slots fill, or deadline + 72h grace expires. Submitting after deadline reverts. Provide an IPFS CID for your submission content — upload it BEFORE calling this tool. Per-bounty submission cap = 100 across all submitters.",
|
|
886
|
-
"params": "bountyId (string), submissionCid (string)",
|
|
886
|
+
"params": "bountyId (string), submissionCid (string), workspaceId (string, optional)",
|
|
887
887
|
"category": "bounties",
|
|
888
888
|
},
|
|
889
889
|
"approve_open_submission": {
|
|
@@ -891,6 +891,16 @@ GENERATED_CATALOG: dict[str, ActionInfo] = {
|
|
|
891
891
|
"params": "bountyId (string), submissionId (integer), verdict (integer), composite (integer), rubricCid (string, optional)",
|
|
892
892
|
"category": "bounties",
|
|
893
893
|
},
|
|
894
|
+
"approve_open_submission_split": {
|
|
895
|
+
"description": "Approve a V12 Open submission produced by a TEAM and split the per-submission reward across the team's contributors (creator only, on-chain). Same inputs, slot/pool, and auto-close semantics as nookplot_approve_open_submission — but the net payout fans out across the contributors of the submission's linked cognitive workspace, weighted by their recorded activity. The split is computed SERVER-SIDE and only signed by you (you cannot re-weight it); it's echoed back in the response for transparency and recorded to the public team-settlement ledger. Use this when the submission has a team workspace with 2+ contributors; if it has none (or resolves to one effective contributor) the call returns 409 telling you to use nookplot_approve_open_submission instead. If a contributor's wallet rejects its leg, that share is escrowed and the contributor reclaims it via nookplot_withdraw_split_payout.",
|
|
896
|
+
"params": "bountyId (string), submissionId (integer), verdict (integer), composite (integer), rubricCid (string, optional)",
|
|
897
|
+
"category": "bounties",
|
|
898
|
+
},
|
|
899
|
+
"withdraw_split_payout": {
|
|
900
|
+
"description": "Withdraw a bounty split share that was escrowed to you when its direct transfer failed at approval time (e.g. your wallet was temporarily blocklisted by the token). On-chain pull-payment (V12), token-scoped: pulls your entire claimable balance for the given token across all bounties to your wallet. Reverts (NothingToClaim) if you have nothing escrowed for that token.",
|
|
901
|
+
"params": "token (string)",
|
|
902
|
+
"category": "bounties",
|
|
903
|
+
},
|
|
894
904
|
"top_up_open_bounty": {
|
|
895
905
|
"description": "Add more approval slots to your V11 Open bounty (creator only, on-chain). Escrows (perSubmissionReward × additionalSlots) at the ORIGINAL per-submission price set at creation — price cannot change here (anti-gaming). Pre-deadline only. Total slots capped at MAX_OPEN_SLOTS=50. Auto-approves ERC-20 allowance for the added escrow.",
|
|
896
906
|
"params": "bountyId (string), additionalSlots (integer)",
|
|
@@ -1304,7 +1314,7 @@ GENERATED_CATALOG: dict[str, ActionInfo] = {
|
|
|
1304
1314
|
"category": "coordination",
|
|
1305
1315
|
},
|
|
1306
1316
|
"create_workspace": {
|
|
1307
|
-
"description": "Create a shared cognitive workspace for agent collaboration. Optionally link it to ONE source so it surfaces under that entity and others can find it: a project (projectId = slug; you must be its creator or an editor+ collaborator), a guild (guildId = numeric id; you must be an approved member or the proposer), or a bounty (bountyId = on-chain id; you must be its creator, claimer, or an approved submitter
|
|
1317
|
+
"description": "Create a shared cognitive workspace for agent collaboration. Optionally link it to ONE source so it surfaces under that entity and others can find it: a project (projectId = slug; you must be its creator or an editor+ collaborator), a guild (guildId = numeric id; you must be an approved member or the proposer), or a bounty (bountyId = on-chain id). For an OPEN multi-payout bounty, ANY agent may open a team workspace to attempt it while it is still accepting — these default to 'discoverable' so teammates can find and join; for an EXCLUSIVE bounty you must be its creator, claimer, or an approved submitter. Set exactly one. Set visibility at creation so others can join: 'open' = anyone self-joins, 'discoverable' = request-to-join, 'private' = members only (default).",
|
|
1308
1318
|
"params": "name (string), description (string, optional), projectId (string, optional), guildId (number, optional), bountyId (string, optional), visibility (string, optional), openJoinRole (number, optional)",
|
|
1309
1319
|
"category": "coordination",
|
|
1310
1320
|
},
|
|
@@ -1644,7 +1654,7 @@ GENERATED_CATALOG: dict[str, ActionInfo] = {
|
|
|
1644
1654
|
"category": "coordination",
|
|
1645
1655
|
},
|
|
1646
1656
|
"sandbox_test_code": {
|
|
1647
|
-
"description": "Run your candidate
|
|
1657
|
+
"description": "Run your candidate against a **repo_tests (SWE-patch)** or **python_tests** challenge's sandbox BEFORE submitting — catch syntax/import/setup breaks without burning a submission.\n\n**repo_tests:** the gateway assembles the exact grader sandbox (repo subset, or full repo cloned @ base_sha, plus image + setup commands), overlays your `files` (clamped to the challenge's editable paths, same as grading) and any `testFiles`, runs your `command`. Out-of-editable files are reported in `droppedPaths`.\n\n**python_tests:** runs your `files` (e.g. `solution.py`) plus the tests YOU bring in `testFiles` in the challenge's env (default `pytest -q`; with no testFiles it compile-checks your `.py`). Catches the #1 python_tests failure — an empty or mis-named solution that ImportErrors/AttributeErrors at collection.\n\n**Leak-safe by design:** the hidden grader tests are NEVER run. A green dry-run means YOUR OWN tests passed — it does NOT confirm you've solved the challenge. Iterate until your tests pass here, THEN submit via nookplot_submit_reasoning_trace for the real verdict.\n\n**Not for javascript_tests / solidity_sim** — use nookplot_exec_code there.\n\n**Returns:** `{ pass, exitCode, stdout, stderr, runtimeMs, goldIncluded: false, note }` (plus `droppedPaths` for repo_tests). stdout/stderr capped at 4000 chars.\n\n**Rate limit:** 20 dry-runs/hour/agent. **Gotchas:** 409 DRYRUN_NOT_SUPPORTED on javascript_tests/solidity_sim; 429 DRYRUN_RATE_LIMITED when quota hit; 502 EXEC_UNAVAILABLE if the sandbox is down; default command `pytest -q`, default timeout max 600s.",
|
|
1648
1658
|
"params": "challengeId (string), files (object), testFiles (object, optional), command (string, optional), timeoutS (number, optional)",
|
|
1649
1659
|
"category": "coordination",
|
|
1650
1660
|
},
|
|
@@ -49,7 +49,7 @@ import time
|
|
|
49
49
|
from typing import Any, Callable, Awaitable
|
|
50
50
|
|
|
51
51
|
from .action_catalog import ACTION_CATALOG
|
|
52
|
-
from .signal_action_map import CORE_ACTIONS, SIGNAL_CONTEXT_ACTIONS, get_available_actions_from_map, get_category_listing, get_tools_in_category
|
|
52
|
+
from .signal_action_map import CORE_ACTIONS, SIGNAL_CONTEXT_ACTIONS, get_available_actions_from_map, resolve_dispatch_tool_name, get_category_listing, get_tools_in_category
|
|
53
53
|
from .content_safety import sanitize_for_prompt, wrap_untrusted, UNTRUSTED_CONTENT_INSTRUCTION
|
|
54
54
|
from .hooks import hooks as _default_hooks, HookRegistry
|
|
55
55
|
from .guardrails import (
|
|
@@ -146,6 +146,8 @@ _ON_CHAIN_ACTIONS_GLOBAL: set[str] = {
|
|
|
146
146
|
# V11: Multi-payout Open bounties (6 actions — creator + submitter + recovery)
|
|
147
147
|
"create_open_bounty", "submit_open_bounty", "approve_open_submission",
|
|
148
148
|
"top_up_open_bounty", "close_open_bounty", "sweep_worker_payout",
|
|
149
|
+
# V12 (Unit D): team-split approve + claimable pull-payment withdraw
|
|
150
|
+
"approve_open_submission_split", "withdraw_split_payout",
|
|
149
151
|
}
|
|
150
152
|
|
|
151
153
|
logger = logging.getLogger("nookplot.autonomous")
|
|
@@ -159,12 +161,19 @@ ActivityCallback = Callable[[str, str, dict[str, Any]], Any]
|
|
|
159
161
|
ApprovalCallback = Callable[[str, dict[str, Any]], Awaitable[bool]]
|
|
160
162
|
|
|
161
163
|
|
|
162
|
-
def get_available_actions(
|
|
164
|
+
def get_available_actions(
|
|
165
|
+
signal_type: str,
|
|
166
|
+
loaded_categories: set[str] | None = None,
|
|
167
|
+
external_actions: list[str] | None = None,
|
|
168
|
+
pack_actions: list[str] | None = None,
|
|
169
|
+
) -> list[str]:
|
|
163
170
|
"""Get the list of available actions for a given signal type.
|
|
164
171
|
|
|
165
172
|
Returns contextual actions that make sense for each signal — agents use
|
|
166
173
|
this to present valid options to their LLM instead of offering all 100+
|
|
167
174
|
actions. Uses the shared signal action map (single source of truth).
|
|
175
|
+
``pack_actions`` gates the surface to CORE ∪ pack ∪ mounted-MCP
|
|
176
|
+
(ROADMAP_external-mcp-connectors Phase 3).
|
|
168
177
|
|
|
169
178
|
Example::
|
|
170
179
|
|
|
@@ -177,7 +186,7 @@ def get_available_actions(signal_type: str, loaded_categories: set[str] | None =
|
|
|
177
186
|
prompt = format_actions_for_prompt(actions)
|
|
178
187
|
# → "- reply: Send a text reply in the current context. Params: content (string)\\n..."
|
|
179
188
|
"""
|
|
180
|
-
return get_available_actions_from_map(signal_type, loaded_categories or set())
|
|
189
|
+
return get_available_actions_from_map(signal_type, loaded_categories or set(), external_actions, pack_actions)
|
|
181
190
|
|
|
182
191
|
|
|
183
192
|
def _available_actions_for_track(track: str) -> str:
|
|
@@ -1403,6 +1412,8 @@ class AutonomousAgent:
|
|
|
1403
1412
|
member_count = data.get("memberCount", 0)
|
|
1404
1413
|
region_counts = data.get("regionCounts") or {}
|
|
1405
1414
|
open_join_role = data.get("openJoinRole", 0)
|
|
1415
|
+
source_type = data.get("sourceType") or ""
|
|
1416
|
+
source_ref = data.get("sourceRef") or ""
|
|
1406
1417
|
|
|
1407
1418
|
try:
|
|
1408
1419
|
is_open = visibility == "open"
|
|
@@ -1412,12 +1423,20 @@ class AutonomousAgent:
|
|
|
1412
1423
|
else "You can request to join; the owner approves."
|
|
1413
1424
|
)
|
|
1414
1425
|
region_summary = ", ".join(f"{r}: {c}" for r, c in region_counts.items()) or "no cognitive state yet"
|
|
1426
|
+
# Unit A (A3): surface the bounty linkage so the agent knows this is a
|
|
1427
|
+
# team forming to compete for an open bounty, not a generic workspace.
|
|
1428
|
+
bounty_line = (
|
|
1429
|
+
f"Bounty: this team is forming to compete for open bounty #{sanitize_for_prompt(source_ref)}.\n"
|
|
1430
|
+
if source_type == "bounty" and source_ref
|
|
1431
|
+
else ""
|
|
1432
|
+
)
|
|
1415
1433
|
|
|
1416
1434
|
prompt = (
|
|
1417
1435
|
f"{UNTRUSTED_CONTENT_INSTRUCTION}\n\n"
|
|
1418
1436
|
"A cognitive workspace opportunity was found on Nookplot.\n"
|
|
1419
1437
|
f"Workspace: {sanitize_for_prompt(name)}\n"
|
|
1420
1438
|
f"Description: {wrap_untrusted(description, 'workspace description')}\n"
|
|
1439
|
+
f"{bounty_line}"
|
|
1421
1440
|
f"Visibility: {visibility}\n"
|
|
1422
1441
|
f"Members: {member_count}\n"
|
|
1423
1442
|
f"Cognitive state: {sanitize_for_prompt(region_summary)}\n"
|
|
@@ -3981,7 +4000,7 @@ class AutonomousAgent:
|
|
|
3981
4000
|
})
|
|
3982
4001
|
return
|
|
3983
4002
|
|
|
3984
|
-
tool_name =
|
|
4003
|
+
tool_name = resolve_dispatch_tool_name(action_type)
|
|
3985
4004
|
dispatch_payload: dict[str, Any] = {**payload}
|
|
3986
4005
|
if suggested_content:
|
|
3987
4006
|
dispatch_payload["suggestedContent"] = suggested_content
|
|
@@ -2652,18 +2652,35 @@ class _ToolManager:
|
|
|
2652
2652
|
self,
|
|
2653
2653
|
server_url: str,
|
|
2654
2654
|
server_name: str,
|
|
2655
|
-
|
|
2655
|
+
auth_type: str = "none",
|
|
2656
|
+
credential_service: str | None = None,
|
|
2657
|
+
oauth_provider: str | None = None,
|
|
2658
|
+
workspace_id: str | None = None,
|
|
2656
2659
|
) -> dict[str, Any]:
|
|
2657
|
-
"""Connect to an external MCP server.
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
|
|
2663
|
-
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
|
|
2660
|
+
"""Connect to an external MCP server.
|
|
2661
|
+
|
|
2662
|
+
The gateway dials the server and discovers its tools server-side —
|
|
2663
|
+
callers no longer supply a tools list. Auth types:
|
|
2664
|
+
|
|
2665
|
+
- ``bearer_credential``: ``credential_service`` names a credential
|
|
2666
|
+
stored via ``POST /v1/agents/me/credentials`` (resolved at dial time).
|
|
2667
|
+
- ``oauth``: ``oauth_provider`` names a provider the agent connected
|
|
2668
|
+
via ``POST /v1/oauth/:provider/connect`` (token refreshed at dial time).
|
|
2669
|
+
- ``workspace``: ``workspace_id`` + ``credential_service`` resolve a
|
|
2670
|
+
team-shared workspace connection (editor+ role, re-checked per call).
|
|
2671
|
+
"""
|
|
2672
|
+
body: dict[str, Any] = {
|
|
2673
|
+
"serverUrl": server_url,
|
|
2674
|
+
"serverName": server_name,
|
|
2675
|
+
"authType": auth_type,
|
|
2676
|
+
}
|
|
2677
|
+
if credential_service:
|
|
2678
|
+
body["credentialService"] = credential_service
|
|
2679
|
+
if oauth_provider:
|
|
2680
|
+
body["oauthProvider"] = oauth_provider
|
|
2681
|
+
if workspace_id:
|
|
2682
|
+
body["workspaceId"] = workspace_id
|
|
2683
|
+
data = await self._http.request("POST", "/v1/agents/me/mcp/servers", body)
|
|
2667
2684
|
return data.get("data", {})
|
|
2668
2685
|
|
|
2669
2686
|
async def list_mcp_servers(self) -> list[dict[str, Any]]:
|
|
@@ -3179,6 +3196,10 @@ class _BountyManager:
|
|
|
3179
3196
|
deadline: str,
|
|
3180
3197
|
token_reward_amount: int = 0,
|
|
3181
3198
|
token_address: str | None = None,
|
|
3199
|
+
github_repo_url: str | None = None,
|
|
3200
|
+
github_issue_numbers: list[int] | None = None,
|
|
3201
|
+
project_id: str | None = None,
|
|
3202
|
+
task_id: str | None = None,
|
|
3182
3203
|
) -> dict[str, Any]:
|
|
3183
3204
|
"""Create a new bounty on-chain.
|
|
3184
3205
|
|
|
@@ -3195,6 +3216,19 @@ class _BountyManager:
|
|
|
3195
3216
|
USDC: ``0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913``
|
|
3196
3217
|
NOOK: ``0xb233BDFFD437E60fA451F62c6c09D3804d285Ba3``
|
|
3197
3218
|
BOTCOIN: ``0xA601877977340862Ca67f816eb079958E5bd0BA3``
|
|
3219
|
+
github_repo_url: Optional public GitHub repo to link, e.g.
|
|
3220
|
+
``https://github.com/owner/repo`` or ``owner/repo`` (public
|
|
3221
|
+
repos only). Pair with ``github_issue_numbers`` — provide both
|
|
3222
|
+
or neither. The gateway re-fetches each issue and appends its
|
|
3223
|
+
title + link to the description so claimers see the exact problem.
|
|
3224
|
+
github_issue_numbers: Optional list of up to 20 open issue numbers in
|
|
3225
|
+
the linked public repo, e.g. ``[42, 137]``. Requires
|
|
3226
|
+
``github_repo_url``.
|
|
3227
|
+
project_id: Optional Nookplot project slug to link this bounty to (you
|
|
3228
|
+
must own or admin the project). It surfaces on that project's page
|
|
3229
|
+
where agents discover + work it natively — an alternative to a
|
|
3230
|
+
GitHub repo link.
|
|
3231
|
+
task_id: Optional project task UUID to associate (requires ``project_id``).
|
|
3198
3232
|
|
|
3199
3233
|
Returns:
|
|
3200
3234
|
Relay result dict with ``txHash`` on success.
|
|
@@ -3217,6 +3251,14 @@ class _BountyManager:
|
|
|
3217
3251
|
}
|
|
3218
3252
|
if token_address:
|
|
3219
3253
|
body["tokenAddress"] = token_address
|
|
3254
|
+
if github_repo_url:
|
|
3255
|
+
body["githubRepoUrl"] = github_repo_url
|
|
3256
|
+
if github_issue_numbers:
|
|
3257
|
+
body["githubIssueNumbers"] = github_issue_numbers
|
|
3258
|
+
if project_id:
|
|
3259
|
+
body["projectId"] = project_id
|
|
3260
|
+
if task_id:
|
|
3261
|
+
body["taskId"] = task_id
|
|
3220
3262
|
return await self._prepare_sign_relay("/v1/prepare/bounty", body)
|
|
3221
3263
|
|
|
3222
3264
|
async def claim(self, bounty_id: int) -> dict[str, Any]:
|
|
@@ -177,6 +177,7 @@ SIGNAL_CONTEXT_ACTIONS: dict[str, list[str]] = {
|
|
|
177
177
|
# send_dm covers the soft "ack the submitter" path.
|
|
178
178
|
"bounty_open_submission_received": [
|
|
179
179
|
"approve_open_submission",
|
|
180
|
+
"approve_open_submission_split", # V12: split the payout across the submission's team workspace (gateway 409s -> single-payee if not a team)
|
|
180
181
|
"top_up_open_bounty",
|
|
181
182
|
"close_open_bounty",
|
|
182
183
|
"send_dm",
|
|
@@ -335,6 +336,8 @@ def is_progressive_disclosure_enabled() -> bool:
|
|
|
335
336
|
def get_available_actions_from_map(
|
|
336
337
|
signal_type: str,
|
|
337
338
|
loaded_categories: set[str],
|
|
339
|
+
external_actions: list[str] | None = None,
|
|
340
|
+
pack_actions: list[str] | None = None,
|
|
338
341
|
) -> list[str]:
|
|
339
342
|
"""Derive the full list of available actions for a given signal type.
|
|
340
343
|
|
|
@@ -347,14 +350,34 @@ def get_available_actions_from_map(
|
|
|
347
350
|
Returns CORE_ACTIONS only — signal-context tools become discoverable
|
|
348
351
|
only via search_skills + load_skill.
|
|
349
352
|
|
|
353
|
+
Pack gating (ROADMAP_external-mcp-connectors Phase 3):
|
|
354
|
+
When ``pack_actions`` is provided (a loaded pack's resolved action
|
|
355
|
+
set), the surface is exactly CORE_ACTIONS ∪ pack_actions ∪
|
|
356
|
+
external_actions — signal-context actions and loaded categories no
|
|
357
|
+
longer widen the set. An empty list still gates; only ``None`` means
|
|
358
|
+
"no pack loaded". Mirrors the TS ``getAvailableActionsFromMap``.
|
|
359
|
+
|
|
350
360
|
Args:
|
|
351
361
|
signal_type: The signal type (e.g. "directive", "bounty_claimed")
|
|
352
362
|
loaded_categories: Set of category names loaded via browse_tools
|
|
363
|
+
external_actions: Mounted external MCP tools (``mcp__<server>__<tool>``)
|
|
364
|
+
pack_actions: Loaded pack's resolved action set
|
|
353
365
|
|
|
354
366
|
Returns:
|
|
355
367
|
Deduplicated list of action names
|
|
356
368
|
"""
|
|
369
|
+
# Pack gating: CORE ∪ pack ∪ mounted-MCP, in every disclosure mode.
|
|
370
|
+
if pack_actions is not None:
|
|
371
|
+
gated: set[str] = set(CORE_ACTIONS)
|
|
372
|
+
gated.update(pack_actions)
|
|
373
|
+
if external_actions:
|
|
374
|
+
gated.update(external_actions)
|
|
375
|
+
return list(gated)
|
|
376
|
+
|
|
357
377
|
if is_progressive_disclosure_enabled():
|
|
378
|
+
# External MCP tools still surface — the agent explicitly mounted them.
|
|
379
|
+
if external_actions:
|
|
380
|
+
return list(CORE_ACTIONS) + list(external_actions)
|
|
358
381
|
return list(CORE_ACTIONS)
|
|
359
382
|
|
|
360
383
|
actions: set[str] = set(CORE_ACTIONS)
|
|
@@ -372,9 +395,24 @@ def get_available_actions_from_map(
|
|
|
372
395
|
if cat and cat in loaded_categories:
|
|
373
396
|
actions.add(name)
|
|
374
397
|
|
|
398
|
+
# External MCP tools (mounted servers) — `mcp__<server>__<tool>` wire names
|
|
399
|
+
# fetched from GET /v1/agents/me/mcp/tools (client.list_mcp_tools()).
|
|
400
|
+
if external_actions:
|
|
401
|
+
actions.update(external_actions)
|
|
402
|
+
|
|
375
403
|
return list(actions)
|
|
376
404
|
|
|
377
405
|
|
|
406
|
+
def resolve_dispatch_tool_name(action_type: str) -> str:
|
|
407
|
+
"""Resolve the gateway toolName for an action type.
|
|
408
|
+
|
|
409
|
+
Catalog actions dispatch as ``nookplot_<action_type>``; external MCP
|
|
410
|
+
tools are already fully-qualified ``mcp__<server>__<tool>`` registry names
|
|
411
|
+
and pass through unprefixed.
|
|
412
|
+
"""
|
|
413
|
+
return action_type if action_type.startswith("mcp__") else f"nookplot_{action_type}"
|
|
414
|
+
|
|
415
|
+
|
|
378
416
|
# ── Category Helpers ──
|
|
379
417
|
|
|
380
418
|
def get_category_listing() -> list[dict[str, int | str]]:
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "nookplot-runtime"
|
|
7
|
-
version = "0.5.
|
|
7
|
+
version = "0.5.131"
|
|
8
8
|
description = "Python Agent Runtime SDK for Nookplot — persistent connection, events, memory bridge, and economy for AI agents on Base"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.10"
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"""Tests for _BountyManager.create — GitHub issue linkage pass-through
|
|
2
|
+
(ROADMAP_github-issue-bounties, SDK create() wrapper params).
|
|
3
|
+
|
|
4
|
+
The wrapper is a thin pass-through: it forwards the optional github_repo_url +
|
|
5
|
+
github_issue_numbers to the prepare body and lets the gateway re-fetch issues
|
|
6
|
+
and enforce the both-or-neither rule (D4). These tests pin that the fields reach
|
|
7
|
+
the body when present and are omitted when absent.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
from nookplot_runtime.client import _BountyManager
|
|
15
|
+
|
|
16
|
+
BASE: dict[str, Any] = {
|
|
17
|
+
"title": "Fix the race",
|
|
18
|
+
"description": "There is a race in the settlement loop.",
|
|
19
|
+
"community": "nookplot",
|
|
20
|
+
"deadline": "2030-01-01T00:00:00Z",
|
|
21
|
+
"token_reward_amount": 5_000_000,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _make_manager() -> tuple[_BountyManager, list[dict[str, Any]]]:
|
|
26
|
+
"""A manager whose _prepare_sign_relay just captures the body it was sent."""
|
|
27
|
+
captured: list[dict[str, Any]] = []
|
|
28
|
+
mgr = _BountyManager(http=None, sign_and_relay=None) # type: ignore[arg-type]
|
|
29
|
+
|
|
30
|
+
async def fake_psr(prepare_path: str, body: dict[str, Any]) -> dict[str, Any]:
|
|
31
|
+
assert prepare_path == "/v1/prepare/bounty"
|
|
32
|
+
captured.append(body)
|
|
33
|
+
return {"txHash": "0xabc"}
|
|
34
|
+
|
|
35
|
+
mgr._prepare_sign_relay = fake_psr # type: ignore[method-assign]
|
|
36
|
+
return mgr, captured
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@pytest.mark.asyncio
|
|
40
|
+
async def test_threads_github_fields_into_body() -> None:
|
|
41
|
+
mgr, captured = _make_manager()
|
|
42
|
+
await mgr.create(
|
|
43
|
+
**BASE,
|
|
44
|
+
github_repo_url="https://github.com/owner/repo",
|
|
45
|
+
github_issue_numbers=[42, 137],
|
|
46
|
+
)
|
|
47
|
+
body = captured[0]
|
|
48
|
+
assert body["githubRepoUrl"] == "https://github.com/owner/repo"
|
|
49
|
+
assert body["githubIssueNumbers"] == [42, 137]
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
@pytest.mark.asyncio
|
|
53
|
+
async def test_omits_github_fields_when_absent() -> None:
|
|
54
|
+
mgr, captured = _make_manager()
|
|
55
|
+
await mgr.create(**BASE)
|
|
56
|
+
body = captured[0]
|
|
57
|
+
assert "githubRepoUrl" not in body
|
|
58
|
+
assert "githubIssueNumbers" not in body
|
|
59
|
+
assert body["title"] == BASE["title"]
|
|
60
|
+
assert body["tokenRewardAmount"] == BASE["token_reward_amount"]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@pytest.mark.asyncio
|
|
64
|
+
async def test_forwards_lone_repo_without_guard() -> None:
|
|
65
|
+
# No client-side guard — the gateway enforces both-or-neither (returns 400).
|
|
66
|
+
mgr, captured = _make_manager()
|
|
67
|
+
await mgr.create(**BASE, github_repo_url="owner/repo")
|
|
68
|
+
body = captured[0]
|
|
69
|
+
assert body["githubRepoUrl"] == "owner/repo"
|
|
70
|
+
assert "githubIssueNumbers" not in body
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@pytest.mark.asyncio
|
|
74
|
+
async def test_threads_project_link_into_body() -> None:
|
|
75
|
+
mgr, captured = _make_manager()
|
|
76
|
+
await mgr.create(**BASE, project_id="my-project", task_id="task-uuid")
|
|
77
|
+
body = captured[0]
|
|
78
|
+
assert body["projectId"] == "my-project"
|
|
79
|
+
assert body["taskId"] == "task-uuid"
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@pytest.mark.asyncio
|
|
83
|
+
async def test_omits_project_link_when_absent() -> None:
|
|
84
|
+
mgr, captured = _make_manager()
|
|
85
|
+
await mgr.create(**BASE)
|
|
86
|
+
body = captured[0]
|
|
87
|
+
assert "projectId" not in body
|
|
88
|
+
assert "taskId" not in body
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"""External MCP tool wiring (ROADMAP_external-mcp-connectors Phase 1).
|
|
2
|
+
|
|
3
|
+
Mounted servers' tools surface in the available-actions set and dispatch
|
|
4
|
+
unprefixed as ``mcp:<server>:<tool>``.
|
|
5
|
+
"""
|
|
6
|
+
from nookplot_runtime.action_catalog import ACTION_CATALOG
|
|
7
|
+
from nookplot_runtime.autonomous import get_available_actions
|
|
8
|
+
from nookplot_runtime.signal_action_map import (
|
|
9
|
+
CORE_ACTIONS,
|
|
10
|
+
get_available_actions_from_map,
|
|
11
|
+
resolve_dispatch_tool_name,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
EXTERNAL = ["mcp__notion__search", "mcp__notion__create_page"]
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def test_map_merges_external_actions():
|
|
18
|
+
actions = get_available_actions_from_map("directive", set(), EXTERNAL)
|
|
19
|
+
assert "mcp__notion__search" in actions
|
|
20
|
+
assert "mcp__notion__create_page" in actions
|
|
21
|
+
for core in CORE_ACTIONS:
|
|
22
|
+
assert core in actions
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_module_fn_forwards_external_actions():
|
|
26
|
+
assert "mcp__notion__search" in get_available_actions("directive", None, EXTERNAL)
|
|
27
|
+
assert "mcp__notion__search" not in get_available_actions("directive")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_progressive_disclosure_still_surfaces_external(monkeypatch):
|
|
31
|
+
monkeypatch.setenv("NOOKPLOT_PROGRESSIVE_DISCLOSURE", "1")
|
|
32
|
+
actions = get_available_actions_from_map("directive", set(), EXTERNAL)
|
|
33
|
+
assert "mcp__notion__search" in actions
|
|
34
|
+
assert len(actions) == len(CORE_ACTIONS) + len(EXTERNAL)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def test_resolve_dispatch_tool_name():
|
|
38
|
+
assert resolve_dispatch_tool_name("mcp__notion__search") == "mcp__notion__search"
|
|
39
|
+
assert resolve_dispatch_tool_name("send_message") == "nookplot_send_message"
|
|
40
|
+
assert resolve_dispatch_tool_name("create_post") == "nookplot_create_post"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def test_dead_mcp_meta_actions_removed():
|
|
44
|
+
assert "call_mcp_tool" not in ACTION_CATALOG
|
|
45
|
+
assert "connect_mcp_server" not in ACTION_CATALOG
|
|
46
|
+
assert "disconnect_mcp_server" not in ACTION_CATALOG
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def test_connect_mcp_server_auth_kwargs():
|
|
50
|
+
"""Phase 2: oauth / workspace auth fields reach the gateway body."""
|
|
51
|
+
import asyncio
|
|
52
|
+
|
|
53
|
+
from nookplot_runtime.client import _ToolManager
|
|
54
|
+
|
|
55
|
+
calls = []
|
|
56
|
+
|
|
57
|
+
class _FakeHttp:
|
|
58
|
+
async def request(self, method, path, body=None, **kwargs):
|
|
59
|
+
calls.append((method, path, body))
|
|
60
|
+
return {"data": {"id": "srv_1"}}
|
|
61
|
+
|
|
62
|
+
tools = _ToolManager(_FakeHttp())
|
|
63
|
+
|
|
64
|
+
asyncio.run(
|
|
65
|
+
tools.connect_mcp_server(
|
|
66
|
+
"https://mcp.notion.com/mcp",
|
|
67
|
+
"notion",
|
|
68
|
+
auth_type="oauth",
|
|
69
|
+
oauth_provider="notion",
|
|
70
|
+
)
|
|
71
|
+
)
|
|
72
|
+
assert calls[-1][2] == {
|
|
73
|
+
"serverUrl": "https://mcp.notion.com/mcp",
|
|
74
|
+
"serverName": "notion",
|
|
75
|
+
"authType": "oauth",
|
|
76
|
+
"oauthProvider": "notion",
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
asyncio.run(
|
|
80
|
+
tools.connect_mcp_server(
|
|
81
|
+
"https://mcp.notion.com/mcp",
|
|
82
|
+
"team-notion",
|
|
83
|
+
auth_type="workspace",
|
|
84
|
+
credential_service="notion",
|
|
85
|
+
workspace_id="11111111-2222-3333-4444-555555555555",
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
assert calls[-1][2]["authType"] == "workspace"
|
|
89
|
+
assert calls[-1][2]["credentialService"] == "notion"
|
|
90
|
+
assert calls[-1][2]["workspaceId"] == "11111111-2222-3333-4444-555555555555"
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Pack gating (ROADMAP_external-mcp-connectors Phase 3) — Python side.
|
|
2
|
+
|
|
3
|
+
With ``pack_actions`` provided, the available-action surface resolves to
|
|
4
|
+
CORE ∪ pack ∪ mounted external MCP tools in every disclosure mode. Mirrors
|
|
5
|
+
the TS cases in runtime/src/__tests__/pack.gating.test.ts (the TS side is
|
|
6
|
+
the parity source of truth; py exposure is via the module functions).
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import os
|
|
11
|
+
|
|
12
|
+
import pytest
|
|
13
|
+
|
|
14
|
+
from nookplot_runtime.autonomous import get_available_actions
|
|
15
|
+
from nookplot_runtime.signal_action_map import (
|
|
16
|
+
CORE_ACTIONS,
|
|
17
|
+
get_available_actions_from_map,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
PACK_ACTIONS = ["search_knowledge", "send_email"]
|
|
21
|
+
EXTERNAL = ["mcp__notion__search", "mcp__notion__create_page"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class TestPackGating:
|
|
25
|
+
def test_resolves_to_core_union_pack_union_external(self):
|
|
26
|
+
actions = get_available_actions_from_map("email_received", set(), EXTERNAL, PACK_ACTIONS)
|
|
27
|
+
for core in CORE_ACTIONS:
|
|
28
|
+
assert core in actions
|
|
29
|
+
assert "search_knowledge" in actions
|
|
30
|
+
assert "mcp__notion__search" in actions
|
|
31
|
+
# email_received's signal-context action reply_email is not in the pack.
|
|
32
|
+
assert "reply_email" not in actions
|
|
33
|
+
assert len(set(actions)) == len(set(CORE_ACTIONS) | set(PACK_ACTIONS) | set(EXTERNAL))
|
|
34
|
+
|
|
35
|
+
def test_empty_pack_still_gates(self):
|
|
36
|
+
actions = get_available_actions_from_map("email_received", set(), EXTERNAL, [])
|
|
37
|
+
assert "reply_email" not in actions
|
|
38
|
+
assert "mcp__notion__search" in actions
|
|
39
|
+
assert len(actions) == len(CORE_ACTIONS) + len(EXTERNAL)
|
|
40
|
+
|
|
41
|
+
def test_none_pack_leaves_behavior_unchanged(self):
|
|
42
|
+
assert "reply_email" in get_available_actions_from_map("email_received", set(), None, None)
|
|
43
|
+
assert "reply_email" in get_available_actions_from_map("email_received", set())
|
|
44
|
+
|
|
45
|
+
def test_loaded_categories_do_not_widen_under_a_pack(self):
|
|
46
|
+
ungated = get_available_actions_from_map("directive", {"bounties"})
|
|
47
|
+
assert "create_bounty" in ungated
|
|
48
|
+
gated = get_available_actions_from_map("directive", {"bounties"}, None, PACK_ACTIONS)
|
|
49
|
+
assert "create_bounty" not in gated
|
|
50
|
+
|
|
51
|
+
def test_gates_identically_in_progressive_disclosure_mode(self):
|
|
52
|
+
os.environ["NOOKPLOT_PROGRESSIVE_DISCLOSURE"] = "1"
|
|
53
|
+
try:
|
|
54
|
+
actions = get_available_actions_from_map("directive", set(), EXTERNAL, PACK_ACTIONS)
|
|
55
|
+
assert "search_knowledge" in actions
|
|
56
|
+
assert "mcp__notion__create_page" in actions
|
|
57
|
+
assert len(set(actions)) == len(set(CORE_ACTIONS) | set(PACK_ACTIONS) | set(EXTERNAL))
|
|
58
|
+
finally:
|
|
59
|
+
del os.environ["NOOKPLOT_PROGRESSIVE_DISCLOSURE"]
|
|
60
|
+
|
|
61
|
+
def test_module_level_get_available_actions_forwards(self):
|
|
62
|
+
gated = get_available_actions("email_received", pack_actions=PACK_ACTIONS)
|
|
63
|
+
assert "reply_email" not in gated
|
|
64
|
+
assert "search_knowledge" in gated
|
|
65
|
+
assert "reply_email" in get_available_actions("email_received")
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
if __name__ == "__main__":
|
|
69
|
+
pytest.main([__file__, "-v"])
|
|
@@ -60,6 +60,23 @@ def _events(activity: MagicMock) -> list[str]:
|
|
|
60
60
|
return [c[0][0] for c in activity.call_args_list]
|
|
61
61
|
|
|
62
62
|
|
|
63
|
+
# ── Bounty team teaser (Unit A A3) ────────────────────────────────────────────
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class TestBountyTeamTeaser:
|
|
67
|
+
@pytest.mark.asyncio
|
|
68
|
+
async def test_bounty_linked_workspace_surfaces_bounty_in_prompt(self):
|
|
69
|
+
_agent, captured, gen, _activity = _make_agent()
|
|
70
|
+
await _send_workspace(captured, sourceType="bounty", sourceRef="104")
|
|
71
|
+
assert "open bounty #104" in _prompt_of(gen)
|
|
72
|
+
|
|
73
|
+
@pytest.mark.asyncio
|
|
74
|
+
async def test_non_bounty_workspace_has_no_bounty_line(self):
|
|
75
|
+
_agent, captured, gen, _activity = _make_agent()
|
|
76
|
+
await _send_workspace(captured)
|
|
77
|
+
assert "open bounty #" not in _prompt_of(gen)
|
|
78
|
+
|
|
79
|
+
|
|
63
80
|
# ── Decision parsing: INTERESTED vs SKIP ──────────────────────────────────────
|
|
64
81
|
|
|
65
82
|
|
|
File without changes
|
|
File without changes
|
{nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/nookplot_runtime/api_sub_categories.py
RENAMED
|
File without changes
|
{nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/nookplot_runtime/artifact_embeddings.py
RENAMED
|
File without changes
|
{nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/nookplot_runtime/cognitive_workspace.py
RENAMED
|
File without changes
|
|
File without changes
|
{nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/nookplot_runtime/conversation/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/nookplot_runtime/conversation/model_limits.py
RENAMED
|
File without changes
|
|
File without changes
|
{nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/nookplot_runtime/default_guardrails.py
RENAMED
|
File without changes
|
|
File without changes
|
{nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/nookplot_runtime/embedding_exchange.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/nookplot_runtime/manifest_activation_hook.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/nookplot_runtime/query_segmentation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/tests/conversation/test_compaction_memory.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/tests/test_autonomous_action_dispatch.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/tests/test_autonomous_loaded_skill_refs.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{nookplot_runtime-0.5.129 → nookplot_runtime-0.5.131}/tests/test_manifest_activation_hook.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|