Metadata-Version: 2.4
Name: walspro-ai
Version: 0.1.0
Summary: Headless OAuth device connector for wals.pro AI 4 weclapp
Author-email: "Wals.pro GmbH" <support@wals.pro>
License-Expression: Apache-2.0
Project-URL: Homepage, https://ai.wals.pro
Project-URL: Documentation, https://ai.wals.pro
Keywords: mcp,oauth,device-authorization,rfc8628,weclapp,erp
Classifier: Development Status :: 4 - Beta
Classifier: Environment :: Console
Classifier: Intended Audience :: System Administrators
Classifier: Operating System :: MacOS :: MacOS X
Classifier: Operating System :: Microsoft :: Windows
Classifier: Operating System :: POSIX :: Linux
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Office/Business
Classifier: Topic :: System :: Systems Administration :: Authentication/Directory
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: mcp<2,>=1.24
Requires-Dist: httpx<1,>=0.27.1
Requires-Dist: anyio<5,>=4.5
Provides-Extra: dev
Requires-Dist: pytest; extra == "dev"
Requires-Dist: pytest-asyncio; extra == "dev"
Requires-Dist: pytest-cov; extra == "dev"
Requires-Dist: ruff; extra == "dev"
Dynamic: license-file

# walspro-ai

Headless OAuth device connector for **wals.pro AI 4 weclapp**.

Install it on an agent host that cannot receive a browser callback — a CLI
agent, an SSH session, a container, a remote desktop. It performs an RFC 8628
device authorization against the hosted issuer and then forwards JSON-RPC
frames between a local stdio MCP client and the hosted `/v1/mcp` runtime.

It is a transport and authentication adapter only: it registers no MCP tools,
implements no business logic, and never sees weclapp API credentials. The tool
catalog, the user policy, the write-approval flow and the audit pipeline all
stay on the server.

"weclapp" is the ERP this connector talks to. This distribution is published by
Wals.pro GmbH and is not a weclapp SE product.

---

## Before you start: your workspace admin must enable device connections

Device authorization is **off by default** and is never derived from a plan or
a tier: a workspace administrator enables it, per workspace, once.

Until they do, the browser approval step fails with HTTP 503 and
`Tenant device enrollment capacity is not configured` — `login` then reports the
prerequisite explicitly instead of a generic failure. Ask your workspace admin
before filing a bug.

## Install

```bash
pipx install walspro-ai      # recommended: isolated, on PATH
pip install walspro-ai       # or into an existing environment
```

Python 3.10 or newer. The only runtime dependencies are the MCP SDK, `httpx`
and `anyio`.

## Use

```bash
walspro-ai login      # authorize this host (prints a URL and a user code)
walspro-ai status     # non-secret local connection status
walspro-ai config     # token-free MCP client configuration snippet
walspro-ai run        # stdio <-> /v1/mcp transport (started by the MCP client)
walspro-ai logout     # remove local credentials only
walspro-ai revoke     # revoke the server grant, then remove local credentials
```

`python -m walspro_ai <command>` is equivalent and works when the console
script is not on `PATH`.

Add the output of `walspro-ai config` to your MCP client configuration. It
contains no tokens; the credentials stay in the local credential file. Use
`--server-name` to change the generated entry key.

Global options: `--credentials PATH` (alternate credential file),
`--verbose` (print the underlying traceback on failure), `--version`.

`login` opens a browser only when one can plausibly be reached (a `BROWSER`
variable, macOS/Windows, or `DISPLAY`/`WAYLAND_DISPLAY`). On a headless Linux
host it prints the URL instead of blocking on a terminal browser. Force either
behaviour with `--open-browser` or `--no-browser`.

### Exit codes

| Code | Meaning |
|---|---|
| `0` | Success |
| `1` | Error (the message names the cause) |
| `2` | Local credentials were removed, but the server grant may still be active — revoke the connection in the dashboard |
| `130` | Interrupted (Ctrl-C) |

## Where credentials live

| Platform | Default path |
|---|---|
| Linux / BSD | `$XDG_CONFIG_HOME/walspro-ai/device.json`, otherwise `~/.config/walspro-ai/device.json` |
| macOS | `~/.config/walspro-ai/device.json` |
| Windows | `%APPDATA%\walspro-ai\device.json` |

The credential file is created exclusively (`O_EXCL`, `O_NOFOLLOW`) with mode
`0600`, rotated by `fsync` + atomic replace, and serialized across processes by
an OS file lock on a sidecar. On POSIX the connector refuses to read a
credential file that is group- or world-accessible or owned by another user.

**Windows, stated plainly.** Windows reports `0o666` for every writable file
regardless of its ACL, so the mode assertion is skipped there, and there is no
`os.getuid()` to compare against, so the ownership assertion is skipped too.
This connector does **not** read the Windows ACL as a substitute. What remains
on Windows is the symlink/regular-file check, the exclusive create and the
atomic replace; the file's actual confidentiality is whatever the containing
directory grants. Keep the credential file inside your own user profile (the
default location does) and do not point `--credentials` at a shared directory.
The POSIX checks themselves are never relaxed. Windows support is implemented
and unit-tested (the platform branches are exercised directly) but continuous
integration runs on Linux only, so please report anything that behaves
differently there — `support@wals.pro`.

**Ambient network configuration is trusted.** Both HTTP stacks honour the
process environment's proxy and CA settings (`HTTPS_PROXY`, `SSL_CERT_FILE`,
and the platform trust store), which is what makes the connector work behind a
corporate proxy. Treat the environment of the process your MCP client starts as
part of the trust boundary, and review any `env` block you paste into a client
configuration.

`status` and `config` never print bearer or refresh tokens.

## Recovering from a broken credential file

`logout` removes the local credential file without validating it, so a
truncated, hand-edited or wrongly-permissioned file never locks you out. It
does **not** end the connection on the server — the device grant stays active
and keeps occupying an enrollment slot until you `revoke` it or remove the
connection in the dashboard.

`revoke` calls the RFC 7009 revocation endpoint. If the stored file no longer
validates, it still recovers the client id, refresh token and revocation
endpoint from it and revokes with those, as long as the endpoint is the
canonical one on the origin the file itself names. The outcomes are:

| Situation | Exit | Local file |
|---|---|---|
| Grant revoked | `0` | removed |
| File invalid, but a token was recoverable and revoked | `0` | removed |
| File unreadable, nothing revocable | `2` | removed — revoke in the dashboard |
| Revocation call failed (issuer down, network) | `1` | **kept** — use `logout` to remove it |

After any of these, `login` works again.

## Migrating from the connector that shipped inside the server

Until this release the command was `weclapp-mcp-device`, installed by the
proprietary `weclapp-mcp` distribution, and its credentials lived in
`~/.config/weclapp-mcp/device.json` (`%APPDATA%\weclapp-mcp\device.json` on
Windows). That file holds a live refresh-token family that **this** connector
cannot revoke, and the command that could is gone.

Before or right after switching: revoke that connection in the dashboard under
Connections, then delete the old directory. `login` prints a reminder when it
finds one.

## Support

There is no public issue tracker. Report defects and questions to
`support@wals.pro`; include the connector version (`walspro-ai --version`) and
the exact message, never the credential file.

## Security properties

- Strict origin binding: issuer, token, revocation and resource URLs must all
  live on the same canonical origin, on their exact canonical paths.
- The OAuth calls refuse redirects, time out, and cap responses at 64 KiB.
- Access tokens rotate with the refresh token; a `401` triggers exactly one
  refresh and one retry. A rotation whose local write fails reports that the
  connection is gone rather than keeping a token the server already retired.
- Tokens are never printed, logged or passed on the command line, `--verbose`
  included. MCP SDK and `httpx` log records are silenced unless you ask for
  them, because their tracebacks quote raw upstream response bytes.
- `run` fails loudly. The transport read is bounded, and a transport failure
  answers every unanswered request with a JSON-RPC error naming the sanitized
  cause and exits non-zero — it never leaves your MCP client waiting.
- One stable public client identity per local profile; it is re-registered only
  after the authorization server explicitly retires it.

## License

Apache-2.0. See `LICENSE`. Copyright 2026 Wals.pro GmbH.

The complete source of this distribution — including its test suite and the
changelog — ships in the sdist (`pip download --no-binary :all: walspro-ai`).
The repository it is developed in is private, so the package deliberately
declares no `Source` or `Changelog` URL rather than a link that 404s.

## Release note for maintainers

The PyPI distribution name was registered **before** any install instruction
naming it reached a customer-facing surface. That ordering is deliberate and
not optional: an unregistered name printed in a dashboard, a README or a doc is
claimable by anyone, and the first person to claim it would be shipping the
package customers install as their OAuth device connector. Register the name
first, then merge the instruction.
