insightfactory-cli 1.1.2.dev29__tar.gz → 1.1.2.dev30__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.
Files changed (73) hide show
  1. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/.gitignore +1 -0
  2. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/CHANGELOG.md +7 -0
  3. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/PKG-INFO +1 -1
  4. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/pyproject.toml +1 -1
  5. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/src/if_cli/router/upstream.py +44 -1
  6. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/tests/test_router_upstream.py +32 -4
  7. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/.github/workflows/ci.yml +0 -0
  8. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/.github/workflows/claude.yml +0 -0
  9. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/.github/workflows/release.yml +0 -0
  10. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/.python-version +0 -0
  11. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/AGENTS.md +0 -0
  12. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/CLAUDE.md +0 -0
  13. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/LICENSE +0 -0
  14. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/README.md +0 -0
  15. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/src/if_cli/__init__.py +0 -0
  16. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/src/if_cli/__main__.py +0 -0
  17. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/src/if_cli/assets/__init__.py +0 -0
  18. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/src/if_cli/assets/insightfactoryai-logo.svg +0 -0
  19. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/src/if_cli/cache.py +0 -0
  20. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/src/if_cli/callback_page.py +0 -0
  21. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/src/if_cli/cli.py +0 -0
  22. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/src/if_cli/colour.py +0 -0
  23. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/src/if_cli/commands/__init__.py +0 -0
  24. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/src/if_cli/commands/api.py +0 -0
  25. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/src/if_cli/commands/config.py +0 -0
  26. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/src/if_cli/commands/login.py +0 -0
  27. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/src/if_cli/commands/logout.py +0 -0
  28. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/src/if_cli/commands/mcp.py +0 -0
  29. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/src/if_cli/commands/profiles.py +0 -0
  30. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/src/if_cli/commands/set_token.py +0 -0
  31. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/src/if_cli/commands/token.py +0 -0
  32. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/src/if_cli/config.py +0 -0
  33. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/src/if_cli/constants.py +0 -0
  34. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/src/if_cli/http.py +0 -0
  35. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/src/if_cli/main.py +0 -0
  36. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/src/if_cli/oauth.py +0 -0
  37. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/src/if_cli/router/__init__.py +0 -0
  38. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/src/if_cli/router/catalog.py +0 -0
  39. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/src/if_cli/router/content.py +0 -0
  40. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/src/if_cli/router/log.py +0 -0
  41. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/src/if_cli/router/policy.py +0 -0
  42. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/src/if_cli/router/server.py +0 -0
  43. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/src/if_cli/runtime.py +0 -0
  44. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/tests/__init__.py +0 -0
  45. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/tests/cache_writer.py +0 -0
  46. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/tests/conftest.py +0 -0
  47. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/tests/helpers.py +0 -0
  48. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/tests/servers.py +0 -0
  49. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/tests/test_api.py +0 -0
  50. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/tests/test_api_command.py +0 -0
  51. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/tests/test_cache.py +0 -0
  52. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/tests/test_cli.py +0 -0
  53. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/tests/test_config.py +0 -0
  54. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/tests/test_config_command.py +0 -0
  55. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/tests/test_login.py +0 -0
  56. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/tests/test_mcp_command.py +0 -0
  57. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/tests/test_oauth.py +0 -0
  58. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/tests/test_oauth_flow.py +0 -0
  59. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/tests/test_oauth_force_refresh.py +0 -0
  60. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/tests/test_profiles.py +0 -0
  61. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/tests/test_programmatic_api.py +0 -0
  62. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/tests/test_release_tools.py +0 -0
  63. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/tests/test_router_catalog.py +0 -0
  64. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/tests/test_router_content.py +0 -0
  65. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/tests/test_router_policy.py +0 -0
  66. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/tests/test_router_resources.py +0 -0
  67. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/tests/test_router_server.py +0 -0
  68. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/tests/test_runtime.py +0 -0
  69. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/tests/test_set_token.py +0 -0
  70. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/tests/test_token.py +0 -0
  71. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/tools/check_release.py +0 -0
  72. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/tools/release_notes.py +0 -0
  73. {insightfactory_cli-1.1.2.dev29 → insightfactory_cli-1.1.2.dev30}/uv.lock +0 -0
@@ -9,3 +9,4 @@ __pycache__/
9
9
  dist/
10
10
  build/
11
11
  .DS_Store
12
+ .tmp/
@@ -39,6 +39,13 @@ last one has been released, so a cycle's PRs share a section.
39
39
  has to land above the released version, not merely above the base, and a PR into an
40
40
  open cycle has to add a line rather than reword one.
41
41
 
42
+ ### Fixed
43
+
44
+ - The router no longer turns a `confirmed:false` preview from a mutating tool into an
45
+ error pointing at a web UI. That path only belongs to the Databricks connect flow;
46
+ a form-mode approval gate now comes back as an ordinary result, so the caller
47
+ confirms by calling again with `confirmed:true` as intended.
48
+
42
49
  ## 1.1.1
43
50
 
44
51
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: insightfactory-cli
3
- Version: 1.1.2.dev29
3
+ Version: 1.1.2.dev30
4
4
  Summary: Profile-based authentication CLI for the InsightFactory Interfaces API
5
5
  Project-URL: Homepage, https://insightfactory.ai
6
6
  Author-email: "insightfactory.ai Support" <support@insightfactory.ai>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "insightfactory-cli"
3
- version = "1.1.2.dev29"
3
+ version = "1.1.2.dev30"
4
4
  description = "Profile-based authentication CLI for the InsightFactory Interfaces API"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.10"
@@ -138,6 +138,22 @@ class ConnectUrlRecorder:
138
138
  return types.ElicitResult(action="decline")
139
139
 
140
140
 
141
+ def _has_url_request(result: types.InputRequiredResult) -> bool:
142
+ """Whether any of the result's requests is the Databricks connect flow.
143
+
144
+ A request in any other mode (today, only `form`) gates a preview behind a
145
+ confirmation rather than asking for a trip to a browser, so its presence
146
+ alone never puts the result on the connect path. A URL-mode request does,
147
+ even one whose URL fails the origin check in `_connect_url` and so
148
+ contributes no link: the call still did not run, and the caller still
149
+ needs the existing refusal, not a result that reads as success.
150
+ """
151
+ return any(
152
+ isinstance(getattr(request, "params", None), types.ElicitRequestURLParams)
153
+ for request in (result.input_requests or {}).values()
154
+ )
155
+
156
+
141
157
  def _relayable_requests(result: types.InputRequiredResult, origin: str) -> tuple[list[str], str | None]:
142
158
  """The messages this router will repeat, and the first connect URL it will name.
143
159
 
@@ -279,7 +295,16 @@ class Upstream:
279
295
  result = await self._run(call, elicitation_callback=recorder)
280
296
  if isinstance(result, types.InputRequiredResult):
281
297
  lines, url = _relayable_requests(result, self.profile["host"])
282
- return self._connect_refusal(lines, url)
298
+ if _has_url_request(result):
299
+ return self._connect_refusal(lines, url)
300
+ # Every mutating tool gates a `confirmed:false` call behind a form-mode
301
+ # elicitation once the router advertises elicitation at all (see
302
+ # `ConnectUrlRecorder`). That gate is a preview, not a connect flow: the
303
+ # factory's own answer to a client with no elicitation capability is a
304
+ # plain `pending_confirmation` result, and the router owes the caller
305
+ # the closest thing it can relay to that, not an error pointing at a
306
+ # web UI the caller was never asked to visit.
307
+ return self._preview_result(lines)
283
308
  if recorder.declined_url_request:
284
309
  # The older-revision path. The server elicited in band and carried on
285
310
  # once the router declined, so the connect page is in the recorder
@@ -290,6 +315,24 @@ class Upstream:
290
315
  return self._connect_refusal(_result_texts(result), recorder.url)
291
316
  return result
292
317
 
318
+ def _preview_result(self, lines: list[str]) -> types.CallToolResult:
319
+ """A form-mode approval gate, relayed as the ordinary result it is.
320
+
321
+ This loses the structured `pending_confirmation` JSON a client with no
322
+ elicitation capability would see (the `approvalId`, `preview`,
323
+ `impactedResources`): the router has only the elicitation's message
324
+ text to work with, not the tool's own result body, which the factory
325
+ never sends once it has decided to gate the call. The message still
326
+ carries what a caller needs, and the caller still proceeds the same
327
+ way, by calling again with `confirmed:true`.
328
+
329
+ Not an error: the call is still pending, exactly as it would be for a
330
+ client that never advertised elicitation, and marking this `is_error`
331
+ would make a normal preview indistinguishable from the connect refusal
332
+ it is not.
333
+ """
334
+ return types.CallToolResult(content=[types.TextContent(text="\n".join(lines))], is_error=False)
335
+
293
336
  def _connect_refusal(self, lines: list[str], url: str | None) -> types.CallToolResult:
294
337
  """Turn the factory's request for input into a refusal naming the page to visit.
295
338
 
@@ -253,21 +253,49 @@ def test_call_tool_takes_the_first_same_origin_url():
253
253
  ]
254
254
 
255
255
 
256
- def test_call_tool_ignores_a_form_mode_request():
257
- """Answering a form means relaying it downstream, which the router does not do."""
256
+ def test_call_tool_relays_a_form_mode_request_as_a_preview_not_an_error():
257
+ """A `confirmed:false` mutating call gates behind a form-mode elicitation once
258
+ the router advertises elicitation at all; it is a preview, not a connect
259
+ flow, so it comes back as an ordinary result the caller can act on by
260
+ calling again with `confirmed:true`."""
258
261
  form = types.ElicitRequest(
259
262
  params=types.ElicitRequestFormParams(
260
- message="Pick one.", requested_schema=types.ElicitRequestedSchema(properties={})
263
+ message="Ready to add **1** task(s) to production line **NEIL_ROUTER_DEMO**. Ask the user to confirm.",
264
+ requested_schema=types.ElicitRequestedSchema(properties={}),
261
265
  )
262
266
  )
263
267
  upstream, _ = _upstream_returning(_input_required(form))
264
268
 
269
+ result = anyio.run(lambda: upstream.call_tool("add_tasks_to_production_line", {}))
270
+
271
+ assert not result.is_error
272
+ assert _texts(result) == [
273
+ "Ready to add **1** task(s) to production line **NEIL_ROUTER_DEMO**. Ask the user to confirm."
274
+ ]
275
+
276
+
277
+ def test_call_tool_treats_a_form_and_url_mix_as_the_connect_flow():
278
+ """A URL-mode request anywhere in the result keeps today's connect refusal,
279
+ even alongside a form-mode one: the connect flow must behave exactly as
280
+ before, so its presence, not the absence of other modes, decides the
281
+ outcome. The form's own message still rides along as an extra relayed
282
+ line, same as any other request that is not the chosen connect URL."""
283
+ form = types.ElicitRequest(
284
+ params=types.ElicitRequestFormParams(
285
+ message="Pick one.", requested_schema=types.ElicitRequestedSchema(properties={})
286
+ )
287
+ )
288
+ upstream, _ = _upstream_returning(
289
+ _input_required(form, _url_request("https://factory.example/my-profile/developer"))
290
+ )
291
+
265
292
  result = anyio.run(lambda: upstream.call_tool("run_sql_query", {}))
266
293
 
267
294
  assert result.is_error
268
295
  assert _texts(result) == [
269
296
  "Pick one.",
270
- "[prd] This factory needs input the router cannot relay. Start at https://factory.example.",
297
+ "Connect your Databricks account.",
298
+ "[prd] Connect at https://factory.example/my-profile/developer, then retry.",
271
299
  ]
272
300
 
273
301