getstack 0.1.0__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- getstack-0.2.0/.gitignore +61 -0
- getstack-0.2.0/CLAUDE.md +47 -0
- {getstack-0.1.0 → getstack-0.2.0}/PKG-INFO +1 -1
- {getstack-0.1.0 → getstack-0.2.0}/pyproject.toml +1 -1
- {getstack-0.1.0 → getstack-0.2.0}/src/getstack/__init__.py +26 -1
- getstack-0.2.0/src/getstack/audit.py +56 -0
- {getstack-0.1.0 → getstack-0.2.0}/src/getstack/client.py +4 -3
- getstack-0.2.0/src/getstack/credentials.py +44 -0
- getstack-0.2.0/src/getstack/identity.py +119 -0
- {getstack-0.1.0 → getstack-0.2.0}/src/getstack/notifications.py +10 -5
- {getstack-0.1.0 → getstack-0.2.0}/src/getstack/passports.py +44 -0
- getstack-0.2.0/src/getstack/proxy.py +109 -0
- getstack-0.2.0/src/getstack/scan.py +77 -0
- getstack-0.2.0/src/getstack/security_events.py +27 -0
- getstack-0.2.0/src/getstack/skills.py +199 -0
- {getstack-0.1.0 → getstack-0.2.0}/src/getstack/types.py +88 -0
- getstack-0.1.0/.gitignore +0 -38
- getstack-0.1.0/src/getstack/audit.py +0 -26
- getstack-0.1.0/src/getstack/credentials.py +0 -33
- {getstack-0.1.0 → getstack-0.2.0}/LICENSE +0 -0
- {getstack-0.1.0 → getstack-0.2.0}/README.md +0 -0
- {getstack-0.1.0 → getstack-0.2.0}/src/getstack/agents.py +0 -0
- {getstack-0.1.0 → getstack-0.2.0}/src/getstack/auth.py +0 -0
- {getstack-0.1.0 → getstack-0.2.0}/src/getstack/dropoffs.py +0 -0
- {getstack-0.1.0 → getstack-0.2.0}/src/getstack/errors.py +0 -0
- {getstack-0.1.0 → getstack-0.2.0}/src/getstack/py.typed +0 -0
- {getstack-0.1.0 → getstack-0.2.0}/src/getstack/reviews.py +0 -0
- {getstack-0.1.0 → getstack-0.2.0}/src/getstack/services.py +0 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
node_modules/
|
|
2
|
+
dist/
|
|
3
|
+
.turbo/
|
|
4
|
+
*.tsbuildinfo
|
|
5
|
+
__pycache__/
|
|
6
|
+
*.pyc
|
|
7
|
+
|
|
8
|
+
# Environment / secrets
|
|
9
|
+
.env
|
|
10
|
+
.env.*
|
|
11
|
+
*.env
|
|
12
|
+
.npmrc
|
|
13
|
+
scripts/.env.cto
|
|
14
|
+
|
|
15
|
+
# Strategy docs (not sensitive, but private)
|
|
16
|
+
stack-claude-code-spec-v3.md
|
|
17
|
+
stack-gtm-distribution.md
|
|
18
|
+
stack-platform-spec-v2.md
|
|
19
|
+
|
|
20
|
+
# IDE
|
|
21
|
+
.vscode/settings.json
|
|
22
|
+
.idea/
|
|
23
|
+
|
|
24
|
+
# OS
|
|
25
|
+
.DS_Store
|
|
26
|
+
Thumbs.db
|
|
27
|
+
|
|
28
|
+
# Next.js
|
|
29
|
+
.next/
|
|
30
|
+
out/
|
|
31
|
+
|
|
32
|
+
# MCP publisher
|
|
33
|
+
mcp-publisher.exe
|
|
34
|
+
.mcpregistry_*
|
|
35
|
+
|
|
36
|
+
# Fly.io deployment config (contains app names, regions)
|
|
37
|
+
**/fly.toml
|
|
38
|
+
!apps/*/fly.toml
|
|
39
|
+
!**/fly.toml.example
|
|
40
|
+
|
|
41
|
+
# Logs
|
|
42
|
+
*.log
|
|
43
|
+
npm-debug.log*
|
|
44
|
+
|
|
45
|
+
# Claude Code edit audit log (per-machine paper trail of hook-logged mutations)
|
|
46
|
+
.claude/audit/
|
|
47
|
+
|
|
48
|
+
# Claude Code per-machine state (background scheduler)
|
|
49
|
+
.claude/scheduled_tasks.lock
|
|
50
|
+
|
|
51
|
+
# OpenAPI spec cache (regenerated by scripts/verify-intents-openapi.ts)
|
|
52
|
+
scripts/.openapi-cache/
|
|
53
|
+
|
|
54
|
+
# Local dev screenshots
|
|
55
|
+
*-stripe.png
|
|
56
|
+
|
|
57
|
+
# YC application session exports (founder voice + redacted creds — never commit)
|
|
58
|
+
docs/yc-q18/
|
|
59
|
+
docs/yc-application-session-preface.md
|
|
60
|
+
*-YC.md
|
|
61
|
+
*-yc.md
|
getstack-0.2.0/CLAUDE.md
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# getstack (Python SDK)
|
|
2
|
+
|
|
3
|
+
Published to PyPI as `getstack`. Not a workspace member of the main monorepo
|
|
4
|
+
(Python has its own tooling via `pyproject.toml` + its own tests).
|
|
5
|
+
|
|
6
|
+
## Purpose
|
|
7
|
+
|
|
8
|
+
- Python ergonomic equivalent of `@getstackrun/sdk`
|
|
9
|
+
- `with stack.passports.mission(...) as m:` context manager that handles
|
|
10
|
+
checkpoints + checkout automatically
|
|
11
|
+
- Same RESTful coverage: agents, passports, services, credentials, drop-offs,
|
|
12
|
+
skills, identity, audit, notifications, security-events, proxy
|
|
13
|
+
|
|
14
|
+
## Where to look
|
|
15
|
+
|
|
16
|
+
- Package metadata: `sdk-python/pyproject.toml`
|
|
17
|
+
- Module entry: `sdk-python/src/getstack/`
|
|
18
|
+
- Tests: `sdk-python/tests/`
|
|
19
|
+
- User-facing quickstart + examples: `sdk-python/README.md`
|
|
20
|
+
- License: `sdk-python/LICENSE`
|
|
21
|
+
|
|
22
|
+
## Key divergence from JS SDK
|
|
23
|
+
|
|
24
|
+
The Python SDK has a **mission context manager** the JS SDK doesn't have:
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
with stack.passports.mission(
|
|
28
|
+
agent_id=agent.id,
|
|
29
|
+
intent="Process invoices from Slack",
|
|
30
|
+
services=["slack", "stripe"],
|
|
31
|
+
checkpoint_interval="5m",
|
|
32
|
+
) as mission:
|
|
33
|
+
mission.log("slack", "read_channel", "#invoices")
|
|
34
|
+
mission.log("stripe", "create_invoice")
|
|
35
|
+
# Checkpoints submitted automatically on schedule
|
|
36
|
+
# Checkout submitted automatically when the block exits
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
When the JS SDK adds an equivalent, align the shape (same keyword args,
|
|
40
|
+
same method names) for cross-language familiarity.
|
|
41
|
+
|
|
42
|
+
## Gotchas
|
|
43
|
+
|
|
44
|
+
- **Not installed by `npm install` at monorepo root.** Python devs run `pip install -e sdk-python/` or `uv pip install -e sdk-python/`. The JS monorepo lockfile does not resolve Python deps.
|
|
45
|
+
- **Published to PyPI separately.** There's no coordinated release between the JS + Python SDKs; they can (and sometimes do) drift in version numbers.
|
|
46
|
+
- **Same authentication posture.** API keys, session JWTs, and the same `sk_live_*` prefix convention. Token type detection works identically to JS.
|
|
47
|
+
- **Offline passport verification is also available** (mirrors `verify_passport_offline` from the JS SDK) — but confirm the exact function name in `sdk-python/src/getstack/` before citing.
|
|
@@ -43,6 +43,11 @@ from .dropoffs import DropoffService
|
|
|
43
43
|
from .reviews import ReviewService
|
|
44
44
|
from .notifications import NotificationService
|
|
45
45
|
from .audit import AuditService
|
|
46
|
+
from .proxy import ProxyService as ProxySvc, ProxyResponse
|
|
47
|
+
from .scan import ScanService
|
|
48
|
+
from .security_events import SecurityEventService as SecurityEventSvc
|
|
49
|
+
from .skills import SkillService
|
|
50
|
+
from .identity import IdentityService
|
|
46
51
|
from .types import (
|
|
47
52
|
Agent,
|
|
48
53
|
Passport,
|
|
@@ -56,6 +61,13 @@ from .types import (
|
|
|
56
61
|
Credential,
|
|
57
62
|
NotificationChannel,
|
|
58
63
|
ToolCall,
|
|
64
|
+
Skill,
|
|
65
|
+
SkillInvocation,
|
|
66
|
+
SkillRequest,
|
|
67
|
+
IdentityProvider,
|
|
68
|
+
IdentityClaim,
|
|
69
|
+
IdentitySettings,
|
|
70
|
+
VerificationSession,
|
|
59
71
|
)
|
|
60
72
|
from .errors import (
|
|
61
73
|
StackError,
|
|
@@ -66,7 +78,7 @@ from .errors import (
|
|
|
66
78
|
PassportBlockedError,
|
|
67
79
|
)
|
|
68
80
|
|
|
69
|
-
__version__ = "0.
|
|
81
|
+
__version__ = "0.2.0"
|
|
70
82
|
|
|
71
83
|
__all__ = [
|
|
72
84
|
"Stack",
|
|
@@ -86,6 +98,14 @@ __all__ = [
|
|
|
86
98
|
"NotificationChannel",
|
|
87
99
|
"Mission",
|
|
88
100
|
"ToolCall",
|
|
101
|
+
"ProxyResponse",
|
|
102
|
+
"Skill",
|
|
103
|
+
"SkillInvocation",
|
|
104
|
+
"SkillRequest",
|
|
105
|
+
"IdentityProvider",
|
|
106
|
+
"IdentityClaim",
|
|
107
|
+
"IdentitySettings",
|
|
108
|
+
"VerificationSession",
|
|
89
109
|
# Errors
|
|
90
110
|
"StackError",
|
|
91
111
|
"NotFoundError",
|
|
@@ -129,6 +149,11 @@ class Stack:
|
|
|
129
149
|
self.reviews = ReviewService(self._client)
|
|
130
150
|
self.notifications = NotificationService(self._client)
|
|
131
151
|
self.audit = AuditService(self._client)
|
|
152
|
+
self.proxy = ProxySvc(self._client)
|
|
153
|
+
self.scan = ScanService(self._client)
|
|
154
|
+
self.security_events = SecurityEventSvc(self._client)
|
|
155
|
+
self.skills = SkillService(self._client)
|
|
156
|
+
self.identity = IdentityService(self._client)
|
|
132
157
|
|
|
133
158
|
@classmethod
|
|
134
159
|
def from_session(cls, session_token: str, **kwargs) -> Stack:
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Audit log access."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .client import HttpClient
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class AuditService:
|
|
11
|
+
def __init__(self, client: HttpClient):
|
|
12
|
+
self._client = client
|
|
13
|
+
|
|
14
|
+
def list(
|
|
15
|
+
self,
|
|
16
|
+
limit: int = 20,
|
|
17
|
+
since: int | None = None,
|
|
18
|
+
agent_id: str | None = None,
|
|
19
|
+
passport_jti: str | None = None,
|
|
20
|
+
) -> dict[str, Any]:
|
|
21
|
+
"""Tail recent audit entries (newest-first).
|
|
22
|
+
|
|
23
|
+
Returns ``{"entries": [...], "max_timestamp": int | None}``.
|
|
24
|
+
Pass ``since`` (epoch ms) to fetch only entries newer than the
|
|
25
|
+
last seen timestamp — useful for incremental polling. Pass
|
|
26
|
+
``agent_id`` or ``passport_jti`` to narrow to a single agent
|
|
27
|
+
or single passport.
|
|
28
|
+
"""
|
|
29
|
+
params: dict[str, str] = {"limit": str(limit)}
|
|
30
|
+
if since is not None:
|
|
31
|
+
params["since"] = str(since)
|
|
32
|
+
if agent_id is not None:
|
|
33
|
+
params["agent_id"] = agent_id
|
|
34
|
+
if passport_jti is not None:
|
|
35
|
+
params["passport_jti"] = passport_jti
|
|
36
|
+
return self._client.get("/v1/audit", params=params)
|
|
37
|
+
|
|
38
|
+
def chain_head(self) -> dict[str, Any]:
|
|
39
|
+
"""Current chain head — latest entry hash + entry count."""
|
|
40
|
+
return self._client.get("/v1/audit/chain-head")
|
|
41
|
+
|
|
42
|
+
def verify_chain(
|
|
43
|
+
self,
|
|
44
|
+
from_date: str | None = None,
|
|
45
|
+
to_date: str | None = None,
|
|
46
|
+
limit: int | None = None,
|
|
47
|
+
) -> dict[str, Any]:
|
|
48
|
+
"""Walk the per-operator chain and report tamper status."""
|
|
49
|
+
params: dict[str, str] = {}
|
|
50
|
+
if from_date is not None:
|
|
51
|
+
params["from"] = from_date
|
|
52
|
+
if to_date is not None:
|
|
53
|
+
params["to"] = to_date
|
|
54
|
+
if limit is not None:
|
|
55
|
+
params["limit"] = str(limit)
|
|
56
|
+
return self._client.get("/v1/audit/verify-chain", params=params)
|
|
@@ -20,10 +20,11 @@ class HttpClient:
|
|
|
20
20
|
self.base_url = base_url.rstrip("/")
|
|
21
21
|
self._client = httpx.Client(timeout=timeout)
|
|
22
22
|
|
|
23
|
-
def request(self, method: str, path: str, json: Any = None, params: dict[str, Any] | None = None) -> Any:
|
|
23
|
+
def request(self, method: str, path: str, json: Any = None, params: dict[str, Any] | None = None, extra_headers: dict[str, str] | None = None) -> Any:
|
|
24
24
|
headers = {
|
|
25
25
|
"Content-Type": "application/json",
|
|
26
26
|
**self.auth.get_headers(),
|
|
27
|
+
**(extra_headers or {}),
|
|
27
28
|
}
|
|
28
29
|
|
|
29
30
|
url = f"{self.base_url}{path}"
|
|
@@ -41,8 +42,8 @@ class HttpClient:
|
|
|
41
42
|
|
|
42
43
|
return resp.json()
|
|
43
44
|
|
|
44
|
-
def get(self, path: str, params: dict[str, Any] | None = None) -> Any:
|
|
45
|
-
return self.request("GET", path, params=params)
|
|
45
|
+
def get(self, path: str, params: dict[str, Any] | None = None, extra_headers: dict[str, str] | None = None) -> Any:
|
|
46
|
+
return self.request("GET", path, params=params, extra_headers=extra_headers)
|
|
46
47
|
|
|
47
48
|
def post(self, path: str, json: Any = None) -> Any:
|
|
48
49
|
return self.request("POST", path, json=json)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"""Credential retrieval."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
from urllib.parse import quote
|
|
7
|
+
|
|
8
|
+
from .client import HttpClient
|
|
9
|
+
from .types import Credential
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class CredentialService:
|
|
13
|
+
def __init__(self, client: HttpClient):
|
|
14
|
+
self._client = client
|
|
15
|
+
|
|
16
|
+
def get(self, provider: str, passport_token: str | None = None) -> Credential:
|
|
17
|
+
"""Retrieve credential for a provider. Optionally pass passport token for security signal tracking."""
|
|
18
|
+
extra_headers = {}
|
|
19
|
+
if passport_token:
|
|
20
|
+
extra_headers["X-Passport-Token"] = passport_token
|
|
21
|
+
data = self._client.get(
|
|
22
|
+
f"/v1/credentials/{quote(provider)}",
|
|
23
|
+
extra_headers=extra_headers if extra_headers else None,
|
|
24
|
+
)
|
|
25
|
+
return Credential(
|
|
26
|
+
provider=data["provider"],
|
|
27
|
+
credential=data.get("credential"),
|
|
28
|
+
credentials=data.get("credentials"),
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
def get_by_connection(self, connection_id: str, passport_token: str | None = None) -> Credential:
|
|
32
|
+
"""Retrieve credential by connection ID."""
|
|
33
|
+
extra_headers = {}
|
|
34
|
+
if passport_token:
|
|
35
|
+
extra_headers["X-Passport-Token"] = passport_token
|
|
36
|
+
data = self._client.get(
|
|
37
|
+
f"/v1/credentials/by-connection/{connection_id}",
|
|
38
|
+
extra_headers=extra_headers if extra_headers else None,
|
|
39
|
+
)
|
|
40
|
+
return Credential(
|
|
41
|
+
provider=data["provider"],
|
|
42
|
+
credential=data.get("credential"),
|
|
43
|
+
credentials=data.get("credentials"),
|
|
44
|
+
)
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"""Identity verification — providers, claims, and verification flow."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .client import HttpClient
|
|
8
|
+
from .types import IdentityProvider, IdentityClaim, IdentitySettings, VerificationSession
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _to_provider(data: dict[str, Any]) -> IdentityProvider:
|
|
12
|
+
return IdentityProvider(
|
|
13
|
+
key=data["key"],
|
|
14
|
+
name=data["name"],
|
|
15
|
+
layers=data.get("layers", []),
|
|
16
|
+
assurance=data.get("assurance", ""),
|
|
17
|
+
description=data.get("description", ""),
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _to_claim(data: dict[str, Any]) -> IdentityClaim:
|
|
22
|
+
return IdentityClaim(
|
|
23
|
+
id=data["id"],
|
|
24
|
+
operator_id=data["operator_id"],
|
|
25
|
+
provider_key=data["provider_key"],
|
|
26
|
+
layer=data["layer"],
|
|
27
|
+
claim_type=data["claim_type"],
|
|
28
|
+
claim_ref=data["claim_ref"],
|
|
29
|
+
assurance_level=data["assurance_level"],
|
|
30
|
+
verified_at=data["verified_at"],
|
|
31
|
+
expires_at=data.get("expires_at"),
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class IdentityService:
|
|
36
|
+
def __init__(self, client: HttpClient):
|
|
37
|
+
self._client = client
|
|
38
|
+
|
|
39
|
+
def get_settings(self) -> IdentitySettings:
|
|
40
|
+
data = self._client.get("/v1/operators/me/identity-settings")
|
|
41
|
+
return IdentitySettings(
|
|
42
|
+
identity_claim_ttl=data["identity_claim_ttl"],
|
|
43
|
+
identity_claim_inheritance=data["identity_claim_inheritance"],
|
|
44
|
+
identity_auto_revoke=data["identity_auto_revoke"],
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def update_settings(
|
|
48
|
+
self,
|
|
49
|
+
*,
|
|
50
|
+
identity_claim_ttl: str | None = None,
|
|
51
|
+
identity_claim_inheritance: str | None = None,
|
|
52
|
+
identity_auto_revoke: bool | None = None,
|
|
53
|
+
) -> IdentitySettings:
|
|
54
|
+
body: dict[str, Any] = {}
|
|
55
|
+
if identity_claim_ttl is not None:
|
|
56
|
+
body["identity_claim_ttl"] = identity_claim_ttl
|
|
57
|
+
if identity_claim_inheritance is not None:
|
|
58
|
+
body["identity_claim_inheritance"] = identity_claim_inheritance
|
|
59
|
+
if identity_auto_revoke is not None:
|
|
60
|
+
body["identity_auto_revoke"] = identity_auto_revoke
|
|
61
|
+
data = self._client.patch("/v1/operators/me/identity-settings", json=body)
|
|
62
|
+
return IdentitySettings(
|
|
63
|
+
identity_claim_ttl=data["identity_claim_ttl"],
|
|
64
|
+
identity_claim_inheritance=data["identity_claim_inheritance"],
|
|
65
|
+
identity_auto_revoke=data["identity_auto_revoke"],
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
def list_providers(self) -> list[IdentityProvider]:
|
|
69
|
+
data = self._client.get("/v1/identity/providers")
|
|
70
|
+
return [_to_provider(p) for p in data]
|
|
71
|
+
|
|
72
|
+
def initiate_verification(
|
|
73
|
+
self,
|
|
74
|
+
provider_key: str,
|
|
75
|
+
*,
|
|
76
|
+
requested_claims: list[str] | None = None,
|
|
77
|
+
return_url: str | None = None,
|
|
78
|
+
) -> VerificationSession:
|
|
79
|
+
body: dict[str, Any] = {"provider_key": provider_key}
|
|
80
|
+
if requested_claims:
|
|
81
|
+
body["requested_claims"] = requested_claims
|
|
82
|
+
if return_url:
|
|
83
|
+
body["return_url"] = return_url
|
|
84
|
+
data = self._client.post("/v1/identity/verify/initiate", json=body)
|
|
85
|
+
return VerificationSession(
|
|
86
|
+
session_ref=data["session_ref"],
|
|
87
|
+
authorization_url=data.get("authorization_url"),
|
|
88
|
+
status=data.get("status", "pending"),
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
def complete_verification(
|
|
92
|
+
self,
|
|
93
|
+
provider_key: str,
|
|
94
|
+
session_ref: str,
|
|
95
|
+
) -> dict[str, Any]:
|
|
96
|
+
return self._client.post(
|
|
97
|
+
"/v1/identity/verify/complete",
|
|
98
|
+
json={"provider_key": provider_key, "session_ref": session_ref},
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
def grant_delegation(self, delegated_scopes: list[str]) -> IdentityClaim:
|
|
102
|
+
data = self._client.post(
|
|
103
|
+
"/v1/identity/delegate",
|
|
104
|
+
json={"delegated_scopes": delegated_scopes},
|
|
105
|
+
)
|
|
106
|
+
return _to_claim(data)
|
|
107
|
+
|
|
108
|
+
def list_claims(self) -> list[IdentityClaim]:
|
|
109
|
+
data = self._client.get("/v1/identity/claims")
|
|
110
|
+
return [_to_claim(c) for c in data]
|
|
111
|
+
|
|
112
|
+
def revoke_claim(self, claim_id: str) -> dict[str, Any]:
|
|
113
|
+
return self._client.delete(f"/v1/identity/claims/{claim_id}")
|
|
114
|
+
|
|
115
|
+
def get_service_requirements(self, service_id: str) -> dict[str, Any]:
|
|
116
|
+
return self._client.get(f"/v1/services/{service_id}/requirements")
|
|
117
|
+
|
|
118
|
+
def set_service_requirement(self, service_id: str, **kwargs: Any) -> dict[str, Any]:
|
|
119
|
+
return self._client.post(f"/v1/services/{service_id}/requirements", json=kwargs)
|
|
@@ -44,11 +44,16 @@ class NotificationService:
|
|
|
44
44
|
body["events"] = events
|
|
45
45
|
return _to_channel(self._client.post("/v1/notifications/channels", json=body))
|
|
46
46
|
|
|
47
|
-
def
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
def send_verification_code(self, channel_id: str) -> dict[str, Any]:
|
|
48
|
+
"""Send a verification code to the channel."""
|
|
49
|
+
return self._client.post(f"/v1/notifications/channels/{channel_id}/send-code")
|
|
50
|
+
|
|
51
|
+
def verify(self, channel_id: str, code: str) -> dict[str, Any]:
|
|
52
|
+
"""Submit a verification code."""
|
|
53
|
+
return self._client.post(
|
|
54
|
+
f"/v1/notifications/channels/{channel_id}/verify",
|
|
55
|
+
json={"code": code},
|
|
56
|
+
)
|
|
52
57
|
|
|
53
58
|
def test(self, channel_id: str) -> dict[str, Any]:
|
|
54
59
|
return self._client.post(f"/v1/notifications/channels/{channel_id}/test")
|
|
@@ -9,6 +9,7 @@ from contextlib import contextmanager
|
|
|
9
9
|
from typing import Any, Generator
|
|
10
10
|
|
|
11
11
|
from .client import HttpClient
|
|
12
|
+
from .proxy import ProxyService, ProxyResponse
|
|
12
13
|
from .types import Passport, CheckpointResult, CheckoutResult, PassportReport, ToolCall
|
|
13
14
|
|
|
14
15
|
|
|
@@ -85,6 +86,49 @@ class Mission:
|
|
|
85
86
|
"""Record that delegation occurred to a child agent."""
|
|
86
87
|
self._delegated_to.append(child_agent_id)
|
|
87
88
|
|
|
89
|
+
@property
|
|
90
|
+
def token(self) -> str | None:
|
|
91
|
+
"""Current passport token, for passing to credential retrieval."""
|
|
92
|
+
return self.passport.token if self.passport else None
|
|
93
|
+
|
|
94
|
+
def proxy(
|
|
95
|
+
self,
|
|
96
|
+
service: str,
|
|
97
|
+
url: str,
|
|
98
|
+
method: str = "GET",
|
|
99
|
+
*,
|
|
100
|
+
headers: dict[str, str] | None = None,
|
|
101
|
+
body: Any = None,
|
|
102
|
+
query: dict[str, str] | None = None,
|
|
103
|
+
_proxy_service: ProxyService | None = None,
|
|
104
|
+
) -> ProxyResponse:
|
|
105
|
+
"""Send a request through STACK's credential proxy using this mission's passport.
|
|
106
|
+
|
|
107
|
+
Requires passing the proxy service from the Stack client, or set it via
|
|
108
|
+
``mission._proxy = stack.proxy`` before calling.
|
|
109
|
+
|
|
110
|
+
Args:
|
|
111
|
+
service: Provider slug (e.g. "openai", "slack").
|
|
112
|
+
url: Full target URL.
|
|
113
|
+
method: HTTP method.
|
|
114
|
+
headers: Extra headers (auth headers will be stripped).
|
|
115
|
+
body: Request body.
|
|
116
|
+
query: Query parameters.
|
|
117
|
+
_proxy_service: ProxyService instance (falls back to self._proxy).
|
|
118
|
+
"""
|
|
119
|
+
ps = _proxy_service or getattr(self, "_proxy", None)
|
|
120
|
+
if ps is None:
|
|
121
|
+
raise RuntimeError(
|
|
122
|
+
"No proxy service available. Set mission._proxy = stack.proxy before calling proxy()."
|
|
123
|
+
)
|
|
124
|
+
resp = ps.request(
|
|
125
|
+
service, url, method,
|
|
126
|
+
headers=headers, body=body, query=query,
|
|
127
|
+
passport_token=self.token,
|
|
128
|
+
)
|
|
129
|
+
self.log(service, method, url)
|
|
130
|
+
return resp
|
|
131
|
+
|
|
88
132
|
def _enter(self) -> None:
|
|
89
133
|
"""Issue passport and start checkpoint timer."""
|
|
90
134
|
self.passport = self._service.issue(
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""Credential proxy — send requests through STACK without seeing secrets."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .client import HttpClient
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ProxyResponse:
|
|
11
|
+
"""Response from a proxied request."""
|
|
12
|
+
|
|
13
|
+
__slots__ = ("status", "headers", "body")
|
|
14
|
+
|
|
15
|
+
def __init__(self, status: int, headers: dict[str, str], body: Any):
|
|
16
|
+
self.status = status
|
|
17
|
+
self.headers = headers
|
|
18
|
+
self.body = body
|
|
19
|
+
|
|
20
|
+
def ok(self) -> bool:
|
|
21
|
+
return 200 <= self.status < 400
|
|
22
|
+
|
|
23
|
+
def json(self) -> Any:
|
|
24
|
+
return self.body
|
|
25
|
+
|
|
26
|
+
def __repr__(self) -> str:
|
|
27
|
+
return f"ProxyResponse(status={self.status})"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class ProxyService:
|
|
31
|
+
def __init__(self, client: HttpClient):
|
|
32
|
+
self._client = client
|
|
33
|
+
|
|
34
|
+
def request(
|
|
35
|
+
self,
|
|
36
|
+
service: str,
|
|
37
|
+
url: str,
|
|
38
|
+
method: str = "GET",
|
|
39
|
+
*,
|
|
40
|
+
headers: dict[str, str] | None = None,
|
|
41
|
+
body: Any | None = None,
|
|
42
|
+
query: dict[str, str] | None = None,
|
|
43
|
+
passport_token: str | None = None,
|
|
44
|
+
) -> ProxyResponse:
|
|
45
|
+
"""Send a request through STACK's credential proxy.
|
|
46
|
+
|
|
47
|
+
Args:
|
|
48
|
+
service: Provider slug (e.g. "openai", "slack").
|
|
49
|
+
url: Full target URL.
|
|
50
|
+
method: HTTP method.
|
|
51
|
+
headers: Extra headers (auth headers will be stripped).
|
|
52
|
+
body: Request body.
|
|
53
|
+
query: Query parameters.
|
|
54
|
+
passport_token: Passport JWT (required).
|
|
55
|
+
"""
|
|
56
|
+
extra_headers = {}
|
|
57
|
+
if passport_token:
|
|
58
|
+
extra_headers["X-Passport-Token"] = passport_token
|
|
59
|
+
|
|
60
|
+
payload: dict[str, Any] = {
|
|
61
|
+
"service": service,
|
|
62
|
+
"url": url,
|
|
63
|
+
"method": method,
|
|
64
|
+
}
|
|
65
|
+
if headers:
|
|
66
|
+
payload["headers"] = headers
|
|
67
|
+
if body is not None:
|
|
68
|
+
payload["body"] = body
|
|
69
|
+
if query:
|
|
70
|
+
payload["query"] = query
|
|
71
|
+
|
|
72
|
+
data = self._client.request(
|
|
73
|
+
"POST",
|
|
74
|
+
"/v1/proxy",
|
|
75
|
+
json=payload,
|
|
76
|
+
extra_headers=extra_headers if extra_headers else None,
|
|
77
|
+
)
|
|
78
|
+
return ProxyResponse(
|
|
79
|
+
status=data.get("status", 0),
|
|
80
|
+
headers=data.get("headers", {}),
|
|
81
|
+
body=data.get("body"),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
def get(self, service: str, url: str, **kwargs) -> ProxyResponse:
|
|
85
|
+
"""GET through proxy."""
|
|
86
|
+
return self.request(service, url, "GET", **kwargs)
|
|
87
|
+
|
|
88
|
+
def post(self, service: str, url: str, **kwargs) -> ProxyResponse:
|
|
89
|
+
"""POST through proxy."""
|
|
90
|
+
return self.request(service, url, "POST", **kwargs)
|
|
91
|
+
|
|
92
|
+
def put(self, service: str, url: str, **kwargs) -> ProxyResponse:
|
|
93
|
+
"""PUT through proxy."""
|
|
94
|
+
return self.request(service, url, "PUT", **kwargs)
|
|
95
|
+
|
|
96
|
+
def patch(self, service: str, url: str, **kwargs) -> ProxyResponse:
|
|
97
|
+
"""PATCH through proxy."""
|
|
98
|
+
return self.request(service, url, "PATCH", **kwargs)
|
|
99
|
+
|
|
100
|
+
def delete(self, service: str, url: str, **kwargs) -> ProxyResponse:
|
|
101
|
+
"""DELETE through proxy."""
|
|
102
|
+
return self.request(service, url, "DELETE", **kwargs)
|
|
103
|
+
|
|
104
|
+
def usage(self) -> dict:
|
|
105
|
+
"""Get monthly proxy usage stats.
|
|
106
|
+
|
|
107
|
+
Returns dict with: tier, limit, used, remaining, period.
|
|
108
|
+
"""
|
|
109
|
+
return self._client.request("GET", "/v1/proxy/usage")
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"""Content-scanning service — `/v1/scan` (L1+L2 prompt-injection detector).
|
|
2
|
+
|
|
3
|
+
Scan retrieved content (emails, documents, web pages, API responses)
|
|
4
|
+
for prompt-injection markers BEFORE feeding it into your LLM. Reuses
|
|
5
|
+
the same detector chain that runs on `/v1/proxy` and
|
|
6
|
+
`/v1/skills/:id/invoke` — L1 regex catalog + L2 encoding-aware
|
|
7
|
+
normalization.
|
|
8
|
+
|
|
9
|
+
Example::
|
|
10
|
+
|
|
11
|
+
result = stack.scan.scan(
|
|
12
|
+
content=email_body,
|
|
13
|
+
context="email",
|
|
14
|
+
source=sender_address,
|
|
15
|
+
)
|
|
16
|
+
if result["verdict"] == "critical":
|
|
17
|
+
raise RuntimeError(f"Indirect injection caught: {result['match']['pattern_id']}")
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
from .client import HttpClient
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ScanService:
|
|
28
|
+
def __init__(self, client: HttpClient):
|
|
29
|
+
self._client = client
|
|
30
|
+
|
|
31
|
+
def scan(
|
|
32
|
+
self,
|
|
33
|
+
content: str | dict[str, Any],
|
|
34
|
+
*,
|
|
35
|
+
context: str | None = None,
|
|
36
|
+
source: str | None = None,
|
|
37
|
+
passport_token: str | None = None,
|
|
38
|
+
) -> dict[str, Any]:
|
|
39
|
+
"""Scan retrieved content for prompt-injection markers.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
content: The content to scan. Strings are scanned directly;
|
|
43
|
+
dicts are walked one level deep, same as proxy bodies.
|
|
44
|
+
context: Optional context tag — one of "email", "document",
|
|
45
|
+
"webpage", "chat_log", "api_response", "calendar", "other".
|
|
46
|
+
source: Optional source identifier (URL / sender / filename).
|
|
47
|
+
passport_token: Optional passport JWT for attribution to a
|
|
48
|
+
specific agent context.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
Dict with keys: verdict ("clean" | "suspicious" | "critical"),
|
|
52
|
+
scan_id, duration_ms, match (or None).
|
|
53
|
+
"""
|
|
54
|
+
payload: dict[str, Any] = {"content": content}
|
|
55
|
+
if context is not None:
|
|
56
|
+
payload["context"] = context
|
|
57
|
+
if source is not None:
|
|
58
|
+
payload["source"] = source
|
|
59
|
+
|
|
60
|
+
extra_headers = None
|
|
61
|
+
if passport_token:
|
|
62
|
+
extra_headers = {"X-Passport-Token": passport_token}
|
|
63
|
+
|
|
64
|
+
return self._client.request(
|
|
65
|
+
"POST",
|
|
66
|
+
"/v1/scan",
|
|
67
|
+
json=payload,
|
|
68
|
+
extra_headers=extra_headers,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
def usage(self) -> dict[str, Any]:
|
|
72
|
+
"""Get monthly scan usage stats.
|
|
73
|
+
|
|
74
|
+
Returns:
|
|
75
|
+
Dict with keys: tier, limit, used, remaining, period.
|
|
76
|
+
"""
|
|
77
|
+
return self._client.request("GET", "/v1/scan/usage")
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Security event management."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from .client import HttpClient
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class SecurityEventService:
|
|
11
|
+
def __init__(self, client: HttpClient):
|
|
12
|
+
self._client = client
|
|
13
|
+
|
|
14
|
+
def list(self, agent_id: str | None = None, page: int = 1, limit: int = 50) -> dict[str, Any]:
|
|
15
|
+
"""List unresolved security events."""
|
|
16
|
+
params: dict[str, str] = {"page": str(page), "limit": str(limit)}
|
|
17
|
+
if agent_id:
|
|
18
|
+
params["agent_id"] = agent_id
|
|
19
|
+
return self._client.get("/v1/security-events", params=params)
|
|
20
|
+
|
|
21
|
+
def get(self, event_id: str) -> dict[str, Any]:
|
|
22
|
+
"""Get a specific security event."""
|
|
23
|
+
return self._client.get(f"/v1/security-events/{event_id}")
|
|
24
|
+
|
|
25
|
+
def resolve(self, event_id: str) -> dict[str, Any]:
|
|
26
|
+
"""Mark a security event as resolved."""
|
|
27
|
+
return self._client.post(f"/v1/security-events/{event_id}/resolve")
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"""Skills marketplace — publish, invoke, browse, and manage skills."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .client import HttpClient
|
|
9
|
+
from .types import Skill, SkillInvocation, SkillRequest
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def _to_skill(data: dict[str, Any]) -> Skill:
|
|
13
|
+
return Skill(
|
|
14
|
+
id=data["id"],
|
|
15
|
+
operator_id=data["operator_id"],
|
|
16
|
+
name=data["name"],
|
|
17
|
+
description=data["description"],
|
|
18
|
+
version=data.get("version", "1.0.0"),
|
|
19
|
+
tags=data.get("tags", []),
|
|
20
|
+
trust_level_required=data.get("trust_level_required", "L0"),
|
|
21
|
+
status=data.get("status", "active"),
|
|
22
|
+
execution_mode=data.get("execution_mode", "open"),
|
|
23
|
+
price_per_invocation=data.get("price_per_invocation", 0),
|
|
24
|
+
invocation_count=data.get("invocation_count", 0),
|
|
25
|
+
created_at=data["created_at"],
|
|
26
|
+
updated_at=data["updated_at"],
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _to_invocation(data: dict[str, Any]) -> SkillInvocation:
|
|
31
|
+
return SkillInvocation(
|
|
32
|
+
id=data["id"],
|
|
33
|
+
skill_id=data["skill_id"],
|
|
34
|
+
status=data["status"],
|
|
35
|
+
output=data.get("output"),
|
|
36
|
+
error=data.get("error"),
|
|
37
|
+
expires_at=data.get("expires_at", ""),
|
|
38
|
+
created_at=data.get("created_at", ""),
|
|
39
|
+
started_at=data.get("started_at"),
|
|
40
|
+
completed_at=data.get("completed_at"),
|
|
41
|
+
llm_tokens_input=data.get("llm_tokens_input"),
|
|
42
|
+
llm_tokens_output=data.get("llm_tokens_output"),
|
|
43
|
+
execution_duration_ms=data.get("execution_duration_ms"),
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _to_request(data: dict[str, Any]) -> SkillRequest:
|
|
48
|
+
return SkillRequest(
|
|
49
|
+
id=data["id"],
|
|
50
|
+
operator_id=data["operator_id"],
|
|
51
|
+
description=data["description"],
|
|
52
|
+
desired_input_schema=data.get("desired_input_schema"),
|
|
53
|
+
desired_output_schema=data.get("desired_output_schema"),
|
|
54
|
+
max_price_cents=data.get("max_price_cents"),
|
|
55
|
+
tags=data.get("tags", []),
|
|
56
|
+
status=data.get("status", "open"),
|
|
57
|
+
created_at=data["created_at"],
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class SkillService:
|
|
62
|
+
def __init__(self, client: HttpClient):
|
|
63
|
+
self._client = client
|
|
64
|
+
|
|
65
|
+
# ─── Publishing ──────────────────────────────────────────────────
|
|
66
|
+
|
|
67
|
+
def publish(self, **kwargs: Any) -> Skill:
|
|
68
|
+
"""Publish a new skill to the marketplace."""
|
|
69
|
+
return _to_skill(self._client.post("/v1/skills", json=kwargs))
|
|
70
|
+
|
|
71
|
+
def get(self, skill_id: str) -> Skill:
|
|
72
|
+
return _to_skill(self._client.get(f"/v1/skills/{skill_id}"))
|
|
73
|
+
|
|
74
|
+
def browse(
|
|
75
|
+
self,
|
|
76
|
+
*,
|
|
77
|
+
query: str | None = None,
|
|
78
|
+
tags: str | None = None,
|
|
79
|
+
trust_level: str | None = None,
|
|
80
|
+
status: str | None = None,
|
|
81
|
+
limit: int | None = None,
|
|
82
|
+
offset: int | None = None,
|
|
83
|
+
) -> list[Skill]:
|
|
84
|
+
params: dict[str, str] = {}
|
|
85
|
+
if query:
|
|
86
|
+
params["query"] = query
|
|
87
|
+
if tags:
|
|
88
|
+
params["tags"] = tags
|
|
89
|
+
if trust_level:
|
|
90
|
+
params["trust_level"] = trust_level
|
|
91
|
+
if status:
|
|
92
|
+
params["status"] = status
|
|
93
|
+
if limit is not None:
|
|
94
|
+
params["limit"] = str(limit)
|
|
95
|
+
if offset is not None:
|
|
96
|
+
params["offset"] = str(offset)
|
|
97
|
+
data = self._client.get("/v1/skills", params=params)
|
|
98
|
+
return [_to_skill(s) for s in data]
|
|
99
|
+
|
|
100
|
+
def list_owned(self) -> list[Skill]:
|
|
101
|
+
data = self._client.get("/v1/skills/mine")
|
|
102
|
+
return [_to_skill(s) for s in data]
|
|
103
|
+
|
|
104
|
+
def update(self, skill_id: str, **kwargs: Any) -> Skill:
|
|
105
|
+
return _to_skill(self._client.patch(f"/v1/skills/{skill_id}", json=kwargs))
|
|
106
|
+
|
|
107
|
+
def suspend(self, skill_id: str) -> Skill:
|
|
108
|
+
return _to_skill(self._client.post(f"/v1/skills/{skill_id}/suspend"))
|
|
109
|
+
|
|
110
|
+
def activate(self, skill_id: str) -> Skill:
|
|
111
|
+
return _to_skill(self._client.post(f"/v1/skills/{skill_id}/activate"))
|
|
112
|
+
|
|
113
|
+
def check_trust(self, skill_id: str) -> dict[str, Any]:
|
|
114
|
+
# Trust claims are resolved server-side from the caller's DB-stored
|
|
115
|
+
# identity claims; callers no longer pass them in.
|
|
116
|
+
return self._client.post(f"/v1/skills/{skill_id}/check-trust", json={})
|
|
117
|
+
|
|
118
|
+
# ─── Invocations ─────────────────────────────────────────────────
|
|
119
|
+
|
|
120
|
+
def invoke(
|
|
121
|
+
self,
|
|
122
|
+
skill_id: str,
|
|
123
|
+
*,
|
|
124
|
+
agent_id: str,
|
|
125
|
+
input: dict[str, Any],
|
|
126
|
+
passport_id: str | None = None,
|
|
127
|
+
) -> SkillInvocation:
|
|
128
|
+
body: dict[str, Any] = {"agent_id": agent_id, "input": input}
|
|
129
|
+
if passport_id:
|
|
130
|
+
body["passport_id"] = passport_id
|
|
131
|
+
return _to_invocation(self._client.post(f"/v1/skills/{skill_id}/invoke", json=body))
|
|
132
|
+
|
|
133
|
+
def get_invocation(self, invocation_id: str) -> SkillInvocation:
|
|
134
|
+
return _to_invocation(self._client.get(f"/v1/skills/invocations/{invocation_id}"))
|
|
135
|
+
|
|
136
|
+
def list_invocations(self) -> list[SkillInvocation]:
|
|
137
|
+
data = self._client.get("/v1/skills/invocations/mine")
|
|
138
|
+
return [_to_invocation(i) for i in data]
|
|
139
|
+
|
|
140
|
+
def list_pending_invocations(self, skill_id: str | None = None) -> list[SkillInvocation]:
|
|
141
|
+
params = {"skill_id": skill_id} if skill_id else {}
|
|
142
|
+
data = self._client.get("/v1/skills/invocations/pending", params=params)
|
|
143
|
+
return [_to_invocation(i) for i in data]
|
|
144
|
+
|
|
145
|
+
def claim_invocation(self, invocation_id: str) -> dict[str, Any]:
|
|
146
|
+
return self._client.post(f"/v1/skills/invocations/{invocation_id}/claim")
|
|
147
|
+
|
|
148
|
+
def complete_invocation(self, invocation_id: str, output: dict[str, Any]) -> dict[str, Any]:
|
|
149
|
+
return self._client.post(
|
|
150
|
+
f"/v1/skills/invocations/{invocation_id}/complete",
|
|
151
|
+
json={"output": output},
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
def poll(
|
|
155
|
+
self,
|
|
156
|
+
invocation_id: str,
|
|
157
|
+
*,
|
|
158
|
+
interval_seconds: float = 1.0,
|
|
159
|
+
timeout_seconds: float = 60.0,
|
|
160
|
+
) -> SkillInvocation:
|
|
161
|
+
"""Poll an invocation until it reaches a terminal state."""
|
|
162
|
+
start = time.time()
|
|
163
|
+
while time.time() - start < timeout_seconds:
|
|
164
|
+
result = self.get_invocation(invocation_id)
|
|
165
|
+
if result.status in ("completed", "failed", "expired"):
|
|
166
|
+
return result
|
|
167
|
+
time.sleep(interval_seconds)
|
|
168
|
+
raise TimeoutError(f"Invocation {invocation_id} timed out after {timeout_seconds}s")
|
|
169
|
+
|
|
170
|
+
# ─── Requests ────────────────────────────────────────────────────
|
|
171
|
+
|
|
172
|
+
def post_request(self, **kwargs: Any) -> SkillRequest:
|
|
173
|
+
return _to_request(self._client.post("/v1/skill-requests", json=kwargs))
|
|
174
|
+
|
|
175
|
+
def list_requests(
|
|
176
|
+
self,
|
|
177
|
+
*,
|
|
178
|
+
status: str | None = None,
|
|
179
|
+
limit: int | None = None,
|
|
180
|
+
offset: int | None = None,
|
|
181
|
+
) -> list[SkillRequest]:
|
|
182
|
+
params: dict[str, str] = {}
|
|
183
|
+
if status:
|
|
184
|
+
params["status"] = status
|
|
185
|
+
if limit is not None:
|
|
186
|
+
params["limit"] = str(limit)
|
|
187
|
+
if offset is not None:
|
|
188
|
+
params["offset"] = str(offset)
|
|
189
|
+
data = self._client.get("/v1/skill-requests", params=params)
|
|
190
|
+
return [_to_request(r) for r in data]
|
|
191
|
+
|
|
192
|
+
def get_request(self, request_id: str) -> SkillRequest:
|
|
193
|
+
return _to_request(self._client.get(f"/v1/skill-requests/{request_id}"))
|
|
194
|
+
|
|
195
|
+
def get_request_matches(self, request_id: str) -> dict[str, Any]:
|
|
196
|
+
return self._client.get(f"/v1/skill-requests/{request_id}/matches")
|
|
197
|
+
|
|
198
|
+
def suggest_composition(self, request_id: str) -> dict[str, Any]:
|
|
199
|
+
return self._client.get(f"/v1/skill-requests/{request_id}/suggest")
|
|
@@ -154,3 +154,91 @@ class ToolCall:
|
|
|
154
154
|
if self.target:
|
|
155
155
|
d["target"] = self.target
|
|
156
156
|
return d
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
# ─── Skills ──────────────────────────────────────────────────────────
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@dataclass
|
|
163
|
+
class Skill:
|
|
164
|
+
id: str
|
|
165
|
+
operator_id: str
|
|
166
|
+
name: str
|
|
167
|
+
description: str
|
|
168
|
+
version: str = "1.0.0"
|
|
169
|
+
tags: list[str] = field(default_factory=list)
|
|
170
|
+
trust_level_required: str = "L0"
|
|
171
|
+
status: str = "active"
|
|
172
|
+
execution_mode: str = "open"
|
|
173
|
+
price_per_invocation: int = 0
|
|
174
|
+
invocation_count: int = 0
|
|
175
|
+
created_at: str = ""
|
|
176
|
+
updated_at: str = ""
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
@dataclass
|
|
180
|
+
class SkillInvocation:
|
|
181
|
+
id: str
|
|
182
|
+
skill_id: str
|
|
183
|
+
status: str
|
|
184
|
+
output: Any = None
|
|
185
|
+
error: dict[str, str] | None = None
|
|
186
|
+
expires_at: str = ""
|
|
187
|
+
created_at: str = ""
|
|
188
|
+
started_at: str | None = None
|
|
189
|
+
completed_at: str | None = None
|
|
190
|
+
llm_tokens_input: int | None = None
|
|
191
|
+
llm_tokens_output: int | None = None
|
|
192
|
+
execution_duration_ms: int | None = None
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
@dataclass
|
|
196
|
+
class SkillRequest:
|
|
197
|
+
id: str
|
|
198
|
+
operator_id: str
|
|
199
|
+
description: str
|
|
200
|
+
desired_input_schema: dict[str, Any] | None = None
|
|
201
|
+
desired_output_schema: dict[str, Any] | None = None
|
|
202
|
+
max_price_cents: int | None = None
|
|
203
|
+
tags: list[str] = field(default_factory=list)
|
|
204
|
+
status: str = "open"
|
|
205
|
+
created_at: str = ""
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
# ─── Identity ────────────────────────────────────────────────────────
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@dataclass
|
|
212
|
+
class IdentityProvider:
|
|
213
|
+
key: str
|
|
214
|
+
name: str
|
|
215
|
+
layers: list[str] = field(default_factory=list)
|
|
216
|
+
assurance: str = ""
|
|
217
|
+
description: str = ""
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
@dataclass
|
|
221
|
+
class IdentityClaim:
|
|
222
|
+
id: str
|
|
223
|
+
operator_id: str
|
|
224
|
+
provider_key: str
|
|
225
|
+
layer: str
|
|
226
|
+
claim_type: str
|
|
227
|
+
claim_ref: str
|
|
228
|
+
assurance_level: str
|
|
229
|
+
verified_at: str
|
|
230
|
+
expires_at: str | None = None
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@dataclass
|
|
234
|
+
class IdentitySettings:
|
|
235
|
+
identity_claim_ttl: str # 30d | 90d | 180d | 1y
|
|
236
|
+
identity_claim_inheritance: str # auto | opt_in
|
|
237
|
+
identity_auto_revoke: bool
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@dataclass
|
|
241
|
+
class VerificationSession:
|
|
242
|
+
session_ref: str
|
|
243
|
+
authorization_url: str | None = None
|
|
244
|
+
status: str = "pending"
|
getstack-0.1.0/.gitignore
DELETED
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
node_modules/
|
|
2
|
-
dist/
|
|
3
|
-
.turbo/
|
|
4
|
-
*.tsbuildinfo
|
|
5
|
-
|
|
6
|
-
# Environment / secrets
|
|
7
|
-
.env
|
|
8
|
-
.env.*
|
|
9
|
-
*.env
|
|
10
|
-
|
|
11
|
-
# Strategy docs (not sensitive, but private)
|
|
12
|
-
stack-claude-code-spec-v3.md
|
|
13
|
-
stack-gtm-distribution.md
|
|
14
|
-
stack-platform-spec-v2.md
|
|
15
|
-
|
|
16
|
-
# IDE
|
|
17
|
-
.vscode/settings.json
|
|
18
|
-
.idea/
|
|
19
|
-
|
|
20
|
-
# OS
|
|
21
|
-
.DS_Store
|
|
22
|
-
Thumbs.db
|
|
23
|
-
|
|
24
|
-
# Next.js
|
|
25
|
-
.next/
|
|
26
|
-
out/
|
|
27
|
-
|
|
28
|
-
# MCP publisher
|
|
29
|
-
mcp-publisher.exe
|
|
30
|
-
.mcpregistry_*
|
|
31
|
-
|
|
32
|
-
# Fly.io deployment config (contains app names, regions)
|
|
33
|
-
**/fly.toml
|
|
34
|
-
!**/fly.toml.example
|
|
35
|
-
|
|
36
|
-
# Logs
|
|
37
|
-
*.log
|
|
38
|
-
npm-debug.log*
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
"""Audit log access."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from typing import Any
|
|
6
|
-
|
|
7
|
-
from .client import HttpClient
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
class AuditService:
|
|
11
|
-
def __init__(self, client: HttpClient):
|
|
12
|
-
self._client = client
|
|
13
|
-
|
|
14
|
-
def list(
|
|
15
|
-
self,
|
|
16
|
-
page: int = 1,
|
|
17
|
-
limit: int = 50,
|
|
18
|
-
action: str | None = None,
|
|
19
|
-
agent_id: str | None = None,
|
|
20
|
-
) -> list[dict[str, Any]]:
|
|
21
|
-
params: dict[str, str] = {"page": str(page), "limit": str(limit)}
|
|
22
|
-
if action:
|
|
23
|
-
params["action"] = action
|
|
24
|
-
if agent_id:
|
|
25
|
-
params["agent_id"] = agent_id
|
|
26
|
-
return self._client.get("/v1/audit", params=params)
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
"""Credential retrieval."""
|
|
2
|
-
|
|
3
|
-
from __future__ import annotations
|
|
4
|
-
|
|
5
|
-
from typing import Any
|
|
6
|
-
from urllib.parse import quote
|
|
7
|
-
|
|
8
|
-
from .client import HttpClient
|
|
9
|
-
from .types import Credential
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
class CredentialService:
|
|
13
|
-
def __init__(self, client: HttpClient):
|
|
14
|
-
self._client = client
|
|
15
|
-
|
|
16
|
-
def get(self, provider: str, passport: Any | None = None) -> Credential:
|
|
17
|
-
headers_extra = {}
|
|
18
|
-
if passport and hasattr(passport, "token"):
|
|
19
|
-
headers_extra["X-Passport-Token"] = passport.token
|
|
20
|
-
data = self._client.get(f"/v1/credentials/{quote(provider)}")
|
|
21
|
-
return Credential(
|
|
22
|
-
provider=data["provider"],
|
|
23
|
-
credential=data.get("credential"),
|
|
24
|
-
credentials=data.get("credentials"),
|
|
25
|
-
)
|
|
26
|
-
|
|
27
|
-
def get_by_connection(self, connection_id: str) -> Credential:
|
|
28
|
-
data = self._client.get(f"/v1/credentials/by-connection/{connection_id}")
|
|
29
|
-
return Credential(
|
|
30
|
-
provider=data["provider"],
|
|
31
|
-
credential=data.get("credential"),
|
|
32
|
-
credentials=data.get("credentials"),
|
|
33
|
-
)
|
|
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
|