vincs 0.1.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.
- vincs-0.1.0/.gitignore +52 -0
- vincs-0.1.0/PKG-INFO +79 -0
- vincs-0.1.0/README.md +52 -0
- vincs-0.1.0/hatch_build.py +50 -0
- vincs-0.1.0/pyproject.toml +38 -0
- vincs-0.1.0/src/vinc/__init__.py +8 -0
- vincs-0.1.0/src/vinc/__main__.py +3 -0
- vincs-0.1.0/src/vinc/_runtime/api/__init__.py +16 -0
- vincs-0.1.0/src/vinc/_runtime/api/activate.py +226 -0
- vincs-0.1.0/src/vinc/_runtime/api/auth.py +119 -0
- vincs-0.1.0/src/vinc/_runtime/api/backup.py +50 -0
- vincs-0.1.0/src/vinc/_runtime/api/changes.py +127 -0
- vincs-0.1.0/src/vinc/_runtime/api/cli.py +1035 -0
- vincs-0.1.0/src/vinc/_runtime/api/cli_account.py +30 -0
- vincs-0.1.0/src/vinc/_runtime/api/cli_output.py +177 -0
- vincs-0.1.0/src/vinc/_runtime/api/cli_target.py +48 -0
- vincs-0.1.0/src/vinc/_runtime/api/cli_tools.py +230 -0
- vincs-0.1.0/src/vinc/_runtime/api/cli_transport.py +122 -0
- vincs-0.1.0/src/vinc/_runtime/api/docstore.py +2350 -0
- vincs-0.1.0/src/vinc/_runtime/api/docsync.py +163 -0
- vincs-0.1.0/src/vinc/_runtime/api/export.py +217 -0
- vincs-0.1.0/src/vinc/_runtime/api/fragment.py +1299 -0
- vincs-0.1.0/src/vinc/_runtime/api/graph_algo.py +417 -0
- vincs-0.1.0/src/vinc/_runtime/api/graphmd.py +620 -0
- vincs-0.1.0/src/vinc/_runtime/api/jobs.py +238 -0
- vincs-0.1.0/src/vinc/_runtime/api/localmode.py +168 -0
- vincs-0.1.0/src/vinc/_runtime/api/mirror_provenance.py +147 -0
- vincs-0.1.0/src/vinc/_runtime/api/mirror_status.py +46 -0
- vincs-0.1.0/src/vinc/_runtime/api/node_mirror.py +187 -0
- vincs-0.1.0/src/vinc/_runtime/api/owner_fold.py +160 -0
- vincs-0.1.0/src/vinc/_runtime/api/package_contract.py +50 -0
- vincs-0.1.0/src/vinc/_runtime/api/packfile.py +127 -0
- vincs-0.1.0/src/vinc/_runtime/api/preference.py +137 -0
- vincs-0.1.0/src/vinc/_runtime/api/procutil.py +18 -0
- vincs-0.1.0/src/vinc/_runtime/api/query.py +7016 -0
- vincs-0.1.0/src/vinc/_runtime/api/refs.py +249 -0
- vincs-0.1.0/src/vinc/_runtime/api/remote_client.py +169 -0
- vincs-0.1.0/src/vinc/_runtime/api/revstamp.py +141 -0
- vincs-0.1.0/src/vinc/_runtime/api/role_contract.py +132 -0
- vincs-0.1.0/src/vinc/_runtime/api/role_gate_contract.py +57 -0
- vincs-0.1.0/src/vinc/_runtime/api/roles.py +64 -0
- vincs-0.1.0/src/vinc/_runtime/api/server.py +2627 -0
- vincs-0.1.0/src/vinc/_runtime/api/share.py +566 -0
- vincs-0.1.0/src/vinc/_runtime/api/spaceproxy.py +159 -0
- vincs-0.1.0/src/vinc/_runtime/api/sync.py +1324 -0
- vincs-0.1.0/src/vinc/_runtime/api/syncmerge.py +110 -0
- vincs-0.1.0/src/vinc/_runtime/api/textextract.py +167 -0
- vincs-0.1.0/src/vinc/_runtime/engines/__init__.py +0 -0
- vincs-0.1.0/src/vinc/_runtime/engines/body_candidates.py +94 -0
- vincs-0.1.0/src/vinc/_runtime/engines/domain_hierarchy.py +297 -0
- vincs-0.1.0/src/vinc/_runtime/engines/embed_pipeline.py +419 -0
- vincs-0.1.0/src/vinc/_runtime/engines/fragment_ingest.py +246 -0
- vincs-0.1.0/src/vinc/_runtime/engines/mentions_link.py +401 -0
- vincs-0.1.0/src/vinc/_runtime/engines/recall_ingest.py +429 -0
- vincs-0.1.0/src/vinc/_runtime/store/__init__.py +14 -0
- vincs-0.1.0/src/vinc/_runtime/store/backend.py +58 -0
- vincs-0.1.0/src/vinc/_runtime/store/chunking.py +162 -0
- vincs-0.1.0/src/vinc/_runtime/store/config.py +228 -0
- vincs-0.1.0/src/vinc/_runtime/store/diagnostics.py +36 -0
- vincs-0.1.0/src/vinc/_runtime/store/durable_generation.py +326 -0
- vincs-0.1.0/src/vinc/_runtime/store/embedded_backend.py +2435 -0
- vincs-0.1.0/src/vinc/_runtime/store/embedded_schema.py +685 -0
- vincs-0.1.0/src/vinc/_runtime/store/encoder_runtime.py +20 -0
- vincs-0.1.0/src/vinc/_runtime/store/lexical.py +102 -0
- vincs-0.1.0/src/vinc/_runtime/store/migrate.py +522 -0
- vincs-0.1.0/src/vinc/_runtime/store/migrate_themes.py +136 -0
- vincs-0.1.0/src/vinc/_runtime/store/open_diagnostics.py +5 -0
- vincs-0.1.0/src/vinc/_runtime/store/persistence.py +637 -0
- vincs-0.1.0/src/vinc/_runtime/store/pkns.py +192 -0
- vincs-0.1.0/src/vinc/_runtime/store/procutil.py +68 -0
- vincs-0.1.0/src/vinc/_runtime/store/provision.py +83 -0
- vincs-0.1.0/src/vinc/_runtime/store/querybase.py +495 -0
- vincs-0.1.0/src/vinc/_runtime/store/request_timing.py +130 -0
- vincs-0.1.0/src/vinc/_runtime/store/theme_store.py +288 -0
- vincs-0.1.0/src/vinc/_runtime/store/wal_guard.py +115 -0
- vincs-0.1.0/src/vinc/_runtime/vinc_mcp/__init__.py +22 -0
- vincs-0.1.0/src/vinc/_runtime/vinc_mcp/bridge.py +290 -0
- vincs-0.1.0/src/vinc/_runtime/vinc_mcp/edverify.py +222 -0
- vincs-0.1.0/src/vinc/_runtime/vinc_mcp/entitlement.py +359 -0
- vincs-0.1.0/src/vinc/_runtime/vinc_mcp/http_server.py +1655 -0
- vincs-0.1.0/src/vinc/_runtime/vinc_mcp/install.py +292 -0
- vincs-0.1.0/src/vinc/_runtime/vinc_mcp/keychain.py +88 -0
- vincs-0.1.0/src/vinc/_runtime/vinc_mcp/oauth.py +323 -0
- vincs-0.1.0/src/vinc/_runtime/vinc_mcp/openapi_v1.json +2227 -0
- vincs-0.1.0/src/vinc/_runtime/vinc_mcp/openapi_v1.py +77 -0
- vincs-0.1.0/src/vinc/_runtime/vinc_mcp/proxy.py +205 -0
- vincs-0.1.0/src/vinc/_runtime/vinc_mcp/remote.py +70 -0
- vincs-0.1.0/src/vinc/_runtime/vinc_mcp/rest_api.py +330 -0
- vincs-0.1.0/src/vinc/_runtime/vinc_mcp/rest_routes.py +114 -0
- vincs-0.1.0/src/vinc/_runtime/vinc_mcp/role_output_gate.py +357 -0
- vincs-0.1.0/src/vinc/_runtime/vinc_mcp/roles.py +547 -0
- vincs-0.1.0/src/vinc/_runtime/vinc_mcp/rpc.py +291 -0
- vincs-0.1.0/src/vinc/_runtime/vinc_mcp/stdio.py +36 -0
- vincs-0.1.0/src/vinc/_runtime/vinc_mcp/store.py +579 -0
- vincs-0.1.0/src/vinc/_runtime/vinc_mcp/syncclient.py +108 -0
- vincs-0.1.0/src/vinc/_runtime/vinc_mcp/toolcall.py +79 -0
- vincs-0.1.0/src/vinc/_runtime/vinc_mcp/tools.py +2306 -0
- vincs-0.1.0/src/vinc/_runtime/vinc_mcp/viewurl.py +139 -0
- vincs-0.1.0/src/vinc/_runtime/vinc_rag/__init__.py +6 -0
- vincs-0.1.0/src/vinc/_runtime/vinc_rag/embed/__init__.py +5 -0
- vincs-0.1.0/src/vinc/_runtime/vinc_rag/embed/encoder.py +254 -0
- vincs-0.1.0/src/vinc/_runtime/vinc_rag/embed/worker.py +987 -0
- vincs-0.1.0/src/vinc/_runtime/vinc_rag/rerank.py +397 -0
- vincs-0.1.0/src/vinc/_runtime/vinc_rag/retrieval.py +1429 -0
- vincs-0.1.0/src/vinc/_runtime/vinc_rag/search_relevance.py +83 -0
- vincs-0.1.0/src/vinc/cli.py +22 -0
- vincs-0.1.0/src/vinc/integrations/__init__.py +1 -0
- vincs-0.1.0/src/vinc/integrations/agent_framework/__init__.py +16 -0
- vincs-0.1.0/src/vinc/integrations/agent_framework/approval.py +39 -0
- vincs-0.1.0/src/vinc/integrations/agent_framework/output_gate.py +39 -0
- vincs-0.1.0/src/vinc/integrations/agent_framework/provider.py +196 -0
- vincs-0.1.0/src/vinc/integrations/agent_framework/py.typed +0 -0
- vincs-0.1.0/src/vinc/integrations/agent_framework/review.py +42 -0
- vincs-0.1.0/src/vinc/integrations/langgraph/__init__.py +41 -0
- vincs-0.1.0/src/vinc/integrations/langgraph/approval.py +29 -0
- vincs-0.1.0/src/vinc/integrations/langgraph/helpers.py +199 -0
- vincs-0.1.0/src/vinc/integrations/langgraph/middleware.py +202 -0
- vincs-0.1.0/src/vinc/integrations/langgraph/output_gate.py +71 -0
- vincs-0.1.0/src/vinc/integrations/langgraph/py.typed +0 -0
- vincs-0.1.0/src/vinc/integrations/langgraph/review.py +64 -0
- vincs-0.1.0/src/vinc/sdk/__init__.py +31 -0
- vincs-0.1.0/src/vinc/sdk/approval.py +118 -0
- vincs-0.1.0/src/vinc/sdk/client.py +629 -0
- vincs-0.1.0/src/vinc/sdk/context.py +211 -0
- vincs-0.1.0/src/vinc/sdk/episode.py +65 -0
- vincs-0.1.0/src/vinc/sdk/errors.py +77 -0
- vincs-0.1.0/src/vinc/sdk/handoff.py +34 -0
- vincs-0.1.0/src/vinc/sdk/output_gate.py +357 -0
- vincs-0.1.0/src/vinc/sdk/package.py +120 -0
- vincs-0.1.0/src/vinc/sdk/prefetch.py +91 -0
- vincs-0.1.0/src/vinc/sdk/py.typed +0 -0
- vincs-0.1.0/src/vinc/sdk/review.py +115 -0
- vincs-0.1.0/src/vinc/sdk/tools.py +74 -0
- vincs-0.1.0/tests/test_install.py +103 -0
vincs-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.pyc
|
|
3
|
+
.venv/
|
|
4
|
+
venv/
|
|
5
|
+
dist/
|
|
6
|
+
# 예외: 디자인 허브 산출물은 **계약**이라 추적한다. 소비 리포의 사본
|
|
7
|
+
# (app/src/tokens.*.css·theme.ts)이 커밋돼 있으므로 그 비교 대상도 리포에 있어야
|
|
8
|
+
# `sync_out.py --check`(CI 드리프트 게이트)가 성립한다. 없으면 게이트가
|
|
9
|
+
# "[MISSING·허브]" 로 죽는다 — 2026-08-08 CI 실패의 원인.
|
|
10
|
+
!design-hub/dist/
|
|
11
|
+
# 단, Claude Design 푸시 번들은 제외 — 업로드 직전에 만드는 임시 산출물이고
|
|
12
|
+
# 대조할 커밋된 소비 사본이 없다(그쪽은 claude.ai 프로젝트에 산다).
|
|
13
|
+
design-hub/dist/claude-design/
|
|
14
|
+
build/
|
|
15
|
+
*.egg-info/
|
|
16
|
+
models/
|
|
17
|
+
vinc_store
|
|
18
|
+
*.lbug
|
|
19
|
+
vinc_e2e/
|
|
20
|
+
node_modules/
|
|
21
|
+
app/src-tauri/target/
|
|
22
|
+
app/src-tauri/binaries/
|
|
23
|
+
app/src-tauri/gen/
|
|
24
|
+
# .claude/ 는 기기별 설정(launch.json·ui-events.jsonl·worktrees)이라 **어느 깊이에서도**
|
|
25
|
+
# 추적하지 않는다. 예외는 이제 없다. 역할 껍데기는 `agents/shells/` 에서 추적되고
|
|
26
|
+
# (`agents_sync.py --check` 가 CI 에서 대조), 하네스가 읽는 사본은 리포 밖 워크스페이스
|
|
27
|
+
# 루트에 `--install` 이 쓴다. 리포 안 `.claude/agents` 는 체크아웃의 브랜치를 따라
|
|
28
|
+
# 수가 달라지는 두 번째 로스터였다.
|
|
29
|
+
# ⚠ 전에는 루트의 `.claude/agents/` 만 되살리려고 네 줄을 겹쳐 썼다(한 줄로는 예외가
|
|
30
|
+
# 사문이 되거나 `_migration/**/.claude/` 중첩본이 새어 나갔다, 2026-08-23 PR #69).
|
|
31
|
+
# 예외가 사라졌으니 모든 깊이를 잡는 이 한 줄이면 된다.
|
|
32
|
+
.claude/
|
|
33
|
+
# 서명·접속 키 — 커밋되면 되돌릴 수 없다. 지금 키들은 리포 **바깥**(워크스페이스
|
|
34
|
+
# 루트)에 있어 커밋된 적이 없지만, 누가 안으로 옮겨도 막히도록 패턴을 박아 둔다.
|
|
35
|
+
# 업데이터 개인키를 잃으면 배포된 전 사용자가 자동 업데이트에서 영구 이탈한다
|
|
36
|
+
# (복구 경로 없음 — DEPLOYMENT.md §1.6 백업 절차 참조).
|
|
37
|
+
*.key
|
|
38
|
+
*.pem
|
|
39
|
+
*.p12
|
|
40
|
+
*.pfx
|
|
41
|
+
|
|
42
|
+
# uv 캐시 락파일 — 로컬 테스트 실행(`uv run`)이 만든다. 정본 의존성은
|
|
43
|
+
# pyproject.toml 이고, 이 락은 기기별 파생물이라 추적하지 않는다.
|
|
44
|
+
uv.lock
|
|
45
|
+
|
|
46
|
+
# 평가·리허설 부산물 — 모델 가중치 수백 MB 가 여기 앉는다.
|
|
47
|
+
# 이 워킹트리는 세션 공유라 남의 `git add -A` 가 이것을 집는다.
|
|
48
|
+
.cache/
|
|
49
|
+
work/
|
|
50
|
+
|
|
51
|
+
# design-hub capture PNGs are build products of scripts/capture_*.mjs; the manifests are committed
|
|
52
|
+
design-hub/reports/captures/**/*.png
|
vincs-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: vincs
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Vinc Python SDK, framework integrations and terminal CLI
|
|
5
|
+
Project-URL: Homepage, https://vincs.io
|
|
6
|
+
Project-URL: Documentation, https://vincs.io/docs/
|
|
7
|
+
Author: Vinculums
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Requires-Python: >=3.11
|
|
10
|
+
Requires-Dist: httpx<1,>=0.27
|
|
11
|
+
Requires-Dist: pydantic<3,>=2.7
|
|
12
|
+
Provides-Extra: agent-framework
|
|
13
|
+
Requires-Dist: agent-framework-core>=1.8.1; extra == 'agent-framework'
|
|
14
|
+
Provides-Extra: all
|
|
15
|
+
Requires-Dist: agent-framework-core>=1.8.1; extra == 'all'
|
|
16
|
+
Requires-Dist: langchain-core>=1.0; extra == 'all'
|
|
17
|
+
Requires-Dist: langchain>=1.0; extra == 'all'
|
|
18
|
+
Requires-Dist: langgraph>=1.0; extra == 'all'
|
|
19
|
+
Provides-Extra: langgraph
|
|
20
|
+
Requires-Dist: langchain-core>=1.0; extra == 'langgraph'
|
|
21
|
+
Requires-Dist: langchain>=1.0; extra == 'langgraph'
|
|
22
|
+
Requires-Dist: langgraph>=1.0; extra == 'langgraph'
|
|
23
|
+
Provides-Extra: test
|
|
24
|
+
Requires-Dist: anyio>=4; extra == 'test'
|
|
25
|
+
Requires-Dist: pytest>=8; extra == 'test'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# Vinc for Python and the terminal
|
|
29
|
+
|
|
30
|
+
Install the SDK and the `vinc` command together:
|
|
31
|
+
|
|
32
|
+
```sh
|
|
33
|
+
pip install vincs
|
|
34
|
+
vinc --help
|
|
35
|
+
vinc --version
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Use the account graph from the terminal with a member key created in your Vinc
|
|
39
|
+
account. Set `VINC_API_KEY` in your environment, then run:
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
vinc --target account whoami
|
|
43
|
+
vinc --target account search --query "release checklist"
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The account CLI uses the same commands and error handling as the desktop's CLI.
|
|
47
|
+
This Python install does not bundle the native local database, extensions or
|
|
48
|
+
embedding model. For a self-contained local CLI, use the signed terminal download
|
|
49
|
+
from [the CLI installation guide](https://vincs.io/docs/cli/#install).
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from vinc import Vinc, AsyncVinc
|
|
53
|
+
|
|
54
|
+
client = Vinc() # reads VINC_API_KEY
|
|
55
|
+
answer = client.brief("release checklist")
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Choose integrations using one package name:
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
pip install "vincs[langgraph]"
|
|
62
|
+
pip install "vincs[agent-framework]"
|
|
63
|
+
pip install "vincs[all]"
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from vinc.integrations.langgraph import get_vinc_context
|
|
68
|
+
from vinc.integrations.agent_framework import VincContextProvider
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
These modules are built from the same source as `vinc-client`, `vinc-langgraph`
|
|
72
|
+
and `vinc-agent-framework`. Existing installations and imports keep working;
|
|
73
|
+
the unified package does not overwrite their files. Framework imports require
|
|
74
|
+
the corresponding extra. Importing the base SDK does not load a framework or
|
|
75
|
+
start a local database. No conversation is automatically written to Vinc.
|
|
76
|
+
|
|
77
|
+
The Python distribution version is independent of the product version reported
|
|
78
|
+
by `vinc --version`. See the [SDK guide](https://vincs.io/docs/frameworks/) for
|
|
79
|
+
key types, read-only use and explicit writes.
|
vincs-0.1.0/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# Vinc for Python and the terminal
|
|
2
|
+
|
|
3
|
+
Install the SDK and the `vinc` command together:
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
pip install vincs
|
|
7
|
+
vinc --help
|
|
8
|
+
vinc --version
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
Use the account graph from the terminal with a member key created in your Vinc
|
|
12
|
+
account. Set `VINC_API_KEY` in your environment, then run:
|
|
13
|
+
|
|
14
|
+
```sh
|
|
15
|
+
vinc --target account whoami
|
|
16
|
+
vinc --target account search --query "release checklist"
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
The account CLI uses the same commands and error handling as the desktop's CLI.
|
|
20
|
+
This Python install does not bundle the native local database, extensions or
|
|
21
|
+
embedding model. For a self-contained local CLI, use the signed terminal download
|
|
22
|
+
from [the CLI installation guide](https://vincs.io/docs/cli/#install).
|
|
23
|
+
|
|
24
|
+
```python
|
|
25
|
+
from vinc import Vinc, AsyncVinc
|
|
26
|
+
|
|
27
|
+
client = Vinc() # reads VINC_API_KEY
|
|
28
|
+
answer = client.brief("release checklist")
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Choose integrations using one package name:
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
pip install "vincs[langgraph]"
|
|
35
|
+
pip install "vincs[agent-framework]"
|
|
36
|
+
pip install "vincs[all]"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
```python
|
|
40
|
+
from vinc.integrations.langgraph import get_vinc_context
|
|
41
|
+
from vinc.integrations.agent_framework import VincContextProvider
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
These modules are built from the same source as `vinc-client`, `vinc-langgraph`
|
|
45
|
+
and `vinc-agent-framework`. Existing installations and imports keep working;
|
|
46
|
+
the unified package does not overwrite their files. Framework imports require
|
|
47
|
+
the corresponding extra. Importing the base SDK does not load a framework or
|
|
48
|
+
start a local database. No conversation is automatically written to Vinc.
|
|
49
|
+
|
|
50
|
+
The Python distribution version is independent of the product version reported
|
|
51
|
+
by `vinc --version`. See the [SDK guide](https://vincs.io/docs/frameworks/) for
|
|
52
|
+
key types, read-only use and explicit writes.
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"""Assemble the unified distribution from the canonical repository packages."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import re
|
|
5
|
+
import tempfile
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CustomBuildHook(BuildHookInterface):
|
|
12
|
+
def initialize(self, version, build_data):
|
|
13
|
+
root = Path(self.root)
|
|
14
|
+
# sdist 에는 이미 조립된 소스가 있다. 저장소 밖에서 다시 빌드할 수 있어야 한다.
|
|
15
|
+
if (root / "src/vinc/sdk/client.py").is_file():
|
|
16
|
+
return
|
|
17
|
+
repo = root.parents[2]
|
|
18
|
+
sources = {
|
|
19
|
+
"sdk": repo / "integrations/client/python/src/vinc_client",
|
|
20
|
+
"integrations/langgraph": repo / "integrations/langgraph/python/src/vinc_langgraph",
|
|
21
|
+
"integrations/agent_framework": repo / "integrations/agent-framework/python/src/vinc_agent_framework",
|
|
22
|
+
**{f"_runtime/{name}": repo / name
|
|
23
|
+
for name in ("api", "store", "engines", "vinc_mcp", "vinc_rag")},
|
|
24
|
+
}
|
|
25
|
+
self._stage = tempfile.TemporaryDirectory(prefix="vincs-build-")
|
|
26
|
+
stage = Path(self._stage.name)
|
|
27
|
+
prefix = "src/vinc" if self.target_name == "sdist" else "vinc"
|
|
28
|
+
for destination, source in sources.items():
|
|
29
|
+
if not source.is_dir():
|
|
30
|
+
raise RuntimeError(f"canonical package source missing: {source}")
|
|
31
|
+
for path in sorted(source.rglob("*")):
|
|
32
|
+
if not path.is_file() or "__pycache__" in path.parts:
|
|
33
|
+
continue
|
|
34
|
+
if path.suffix not in {".py", ".json"} and path.name != "py.typed":
|
|
35
|
+
continue
|
|
36
|
+
relative = Path(destination) / path.relative_to(source)
|
|
37
|
+
target = stage / relative
|
|
38
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
if path.suffix == ".py" and destination.startswith("integrations/"):
|
|
40
|
+
# 별도 배포판의 파일을 덮어쓰지 않고 같은 SDK 소스에 연결한다.
|
|
41
|
+
text = path.read_text(encoding="utf-8")
|
|
42
|
+
text = re.sub(r"(?m)^(from\s+)vinc_client(?=[.\s])", r"\1vinc.sdk", text)
|
|
43
|
+
target.write_text(text, encoding="utf-8")
|
|
44
|
+
else:
|
|
45
|
+
target.write_bytes(path.read_bytes())
|
|
46
|
+
build_data["force_include"][str(target)] = f"{prefix}/{relative.as_posix()}"
|
|
47
|
+
|
|
48
|
+
def finalize(self, version, build_data, artifact_path):
|
|
49
|
+
if hasattr(self, "_stage"):
|
|
50
|
+
self._stage.cleanup()
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.25"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "vincs"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Vinc Python SDK, framework integrations and terminal CLI"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.11"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
authors = [{ name = "Vinculums" }]
|
|
13
|
+
dependencies = ["httpx>=0.27,<1", "pydantic>=2.7,<3"]
|
|
14
|
+
|
|
15
|
+
[project.optional-dependencies]
|
|
16
|
+
langgraph = ["langgraph>=1.0", "langchain-core>=1.0", "langchain>=1.0"]
|
|
17
|
+
agent-framework = ["agent-framework-core>=1.8.1"]
|
|
18
|
+
all = ["vincs[langgraph,agent-framework]"]
|
|
19
|
+
test = ["pytest>=8", "anyio>=4"]
|
|
20
|
+
|
|
21
|
+
[project.scripts]
|
|
22
|
+
vinc = "vinc.cli:main"
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Homepage = "https://vincs.io"
|
|
26
|
+
Documentation = "https://vincs.io/docs/"
|
|
27
|
+
|
|
28
|
+
[tool.hatch.build.targets.wheel]
|
|
29
|
+
packages = ["src/vinc"]
|
|
30
|
+
|
|
31
|
+
[tool.hatch.build.targets.sdist]
|
|
32
|
+
include = ["src", "pyproject.toml", "README.md", "hatch_build.py", "tests"]
|
|
33
|
+
|
|
34
|
+
[tool.hatch.build.hooks.custom]
|
|
35
|
+
path = "hatch_build.py"
|
|
36
|
+
|
|
37
|
+
[tool.pytest.ini_options]
|
|
38
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""Vinc SDK. Framework dependencies are loaded only by their integration."""
|
|
2
|
+
from .sdk import * # noqa: F403
|
|
3
|
+
from .sdk import AsyncVincClient, VincClient, __all__ as _sdk_exports
|
|
4
|
+
|
|
5
|
+
__version__ = "0.1.0"
|
|
6
|
+
Vinc = VincClient
|
|
7
|
+
AsyncVinc = AsyncVincClient
|
|
8
|
+
__all__ = [*_sdk_exports, "Vinc", "AsyncVinc"]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Query API — read-only entry points over the memory store.
|
|
3
|
+
|
|
4
|
+
Three entry points sit on top of the ingestion engines:
|
|
5
|
+
recall : episodic memory retrieval (Episode nodes, time-ordered)
|
|
6
|
+
search : hybrid vector + BM25 retrieval over Chunk nodes
|
|
7
|
+
decide : decision support — graph traversal over policy/concept relations
|
|
8
|
+
|
|
9
|
+
Each entry point builds a deterministic *query plan* (testable without a
|
|
10
|
+
database) and, when a Neo4j driver is available, executes it.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from .query import QueryPlan, decide, recall, search
|
|
15
|
+
|
|
16
|
+
__all__ = ["QueryPlan", "recall", "search", "decide"]
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""Activate — make a slice of owned memory *portable* to an external AI.
|
|
3
|
+
|
|
4
|
+
P3 of the memory IA ("내 AI로 이식"). Given recall rows (owner-scoped, optionally
|
|
5
|
+
preference-ranked), build a **context pack** in two portable shapes:
|
|
6
|
+
|
|
7
|
+
- a Markdown brief (``pack.md``) a human or any LLM can read directly;
|
|
8
|
+
- a structured JSON (``pack.json``) for programmatic ingestion;
|
|
9
|
+
|
|
10
|
+
plus an **MCP resource manifest** (``mcp.json``) — the connector contract an MCP
|
|
11
|
+
server (mounted by weg-9000-orchestration) serves so a foreign AI can pull these
|
|
12
|
+
episodes as ``memory://{owner}/{id}`` resources.
|
|
13
|
+
|
|
14
|
+
Builders are pure (no I/O, no wall clock — ``generated_at`` is injected) so the
|
|
15
|
+
pack is deterministic and testable. ``activate`` orchestrates recall → pack and
|
|
16
|
+
optionally writes the three files.
|
|
17
|
+
|
|
18
|
+
python -m api.cli activate --owner user:weg1456 --rank preference --out ./pack
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
from dataclasses import dataclass, field
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
from . import query as q
|
|
27
|
+
|
|
28
|
+
MCP_SCHEMA_VERSION = "2024-11-05"
|
|
29
|
+
_URI_SCHEME = "memory"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _uri(owner: str, episode_id: str) -> str:
|
|
33
|
+
return f"{_URI_SCHEME}://{owner}/{episode_id}"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class ContextPack:
|
|
38
|
+
owner: str
|
|
39
|
+
query: str
|
|
40
|
+
generated_at: str | None
|
|
41
|
+
episodes: list[dict]
|
|
42
|
+
learner_model: list[dict] = field(default_factory=list) # how this person learns (U-patterns)
|
|
43
|
+
theme_id: str | None = None
|
|
44
|
+
# 주제 어휘(선택) — **authored 만** 실린다. proposed(재가 전 잠정)는 팩·내보내기에
|
|
45
|
+
# 하중을 지지 않는다(THESIS §6 잠정상태 · D11-R2). activate() 가 필터해 넣어준다.
|
|
46
|
+
vocabulary: list[dict] = field(default_factory=list)
|
|
47
|
+
|
|
48
|
+
# ── JSON shape (programmatic ingestion) ──────────────────────────────────
|
|
49
|
+
def to_json(self) -> dict:
|
|
50
|
+
out = {
|
|
51
|
+
"kind": "weg-9000-memory/context-pack",
|
|
52
|
+
"owner": self.owner,
|
|
53
|
+
"query": self.query,
|
|
54
|
+
"generated_at": self.generated_at,
|
|
55
|
+
"episode_count": len(self.episodes),
|
|
56
|
+
"episodes": self.episodes,
|
|
57
|
+
"learner_model": self.learner_model,
|
|
58
|
+
}
|
|
59
|
+
if self.theme_id is not None:
|
|
60
|
+
out["theme"] = {"theme_id": self.theme_id, "vocabulary": self.vocabulary}
|
|
61
|
+
return out
|
|
62
|
+
|
|
63
|
+
def _learner_section(self) -> list[str]:
|
|
64
|
+
if not self.learner_model:
|
|
65
|
+
return []
|
|
66
|
+
out = [
|
|
67
|
+
"",
|
|
68
|
+
f"## 학습법 — how {self.owner} learns",
|
|
69
|
+
"",
|
|
70
|
+
"> 이 개인의 학습 방식(인지 지문). 외부 AI는 이 방식에 맞춰 나를 도와야 한다.",
|
|
71
|
+
"",
|
|
72
|
+
]
|
|
73
|
+
for p in self.learner_model:
|
|
74
|
+
meta = []
|
|
75
|
+
if p.get("domain"):
|
|
76
|
+
meta.append(f"domain: {p['domain']}")
|
|
77
|
+
if p.get("observations") is not None:
|
|
78
|
+
meta.append(f"n={p['observations']}")
|
|
79
|
+
head = f"- **{p.get('code', '')} {p.get('name', '')}**".rstrip()
|
|
80
|
+
if meta:
|
|
81
|
+
head += f" ({' · '.join(meta)})"
|
|
82
|
+
out.append(head)
|
|
83
|
+
desc = (p.get("description") or "").strip()
|
|
84
|
+
if desc:
|
|
85
|
+
out.append(f" - {desc}")
|
|
86
|
+
return out
|
|
87
|
+
|
|
88
|
+
# ── Markdown brief (human / LLM-readable) ────────────────────────────────
|
|
89
|
+
def to_markdown(self) -> str:
|
|
90
|
+
parts = [
|
|
91
|
+
f"# Memory Context Pack — {self.owner}",
|
|
92
|
+
"",
|
|
93
|
+
f"> query: \"{self.query}\" · generated: {self.generated_at or 'n/a'} "
|
|
94
|
+
f"· {len(self.episodes)} episode(s) · {len(self.learner_model)} learning-pattern(s)",
|
|
95
|
+
"> Portable memory brief for ingestion into an external AI. "
|
|
96
|
+
"Source: weg-9000-memory.",
|
|
97
|
+
"",
|
|
98
|
+
]
|
|
99
|
+
if not self.episodes:
|
|
100
|
+
parts.append("_(no episodes in scope)_")
|
|
101
|
+
for ep in self.episodes:
|
|
102
|
+
pin = " 📌" if ep.get("pinned") else ""
|
|
103
|
+
tags = ep.get("tags") or []
|
|
104
|
+
meta = [f"date: {ep.get('date', 'n/a')}"]
|
|
105
|
+
if ep.get("domain"):
|
|
106
|
+
meta.append(f"domain: {ep['domain']}")
|
|
107
|
+
if tags:
|
|
108
|
+
meta.append("tags: " + ", ".join(tags))
|
|
109
|
+
if ep.get("score") is not None:
|
|
110
|
+
meta.append(f"score: {ep['score']}")
|
|
111
|
+
parts.append(f"## {ep.get('title') or ep.get('id', 'untitled')}{pin}")
|
|
112
|
+
parts.append("- " + " · ".join(meta))
|
|
113
|
+
summary = (ep.get("summary") or "").strip()
|
|
114
|
+
if summary:
|
|
115
|
+
parts += ["", summary]
|
|
116
|
+
parts += ["", "---", ""]
|
|
117
|
+
parts += self._learner_section()
|
|
118
|
+
return "\n".join(parts) + "\n"
|
|
119
|
+
|
|
120
|
+
# ── MCP resource manifest (connector contract) ───────────────────────────
|
|
121
|
+
def to_mcp_manifest(self) -> dict:
|
|
122
|
+
resources = [
|
|
123
|
+
{
|
|
124
|
+
"uri": _uri(self.owner, ep.get("id", "")),
|
|
125
|
+
"name": ep.get("title") or ep.get("id", ""),
|
|
126
|
+
"mimeType": "text/markdown",
|
|
127
|
+
"description": (ep.get("summary") or "")[:120],
|
|
128
|
+
}
|
|
129
|
+
for ep in self.episodes
|
|
130
|
+
]
|
|
131
|
+
if self.learner_model:
|
|
132
|
+
resources.append({
|
|
133
|
+
"uri": _uri(self.owner, "_learner"),
|
|
134
|
+
"name": f"learning method ({self.owner})",
|
|
135
|
+
"mimeType": "text/markdown",
|
|
136
|
+
"description": f"{len(self.learner_model)} learning pattern(s) — how this person learns",
|
|
137
|
+
})
|
|
138
|
+
return {
|
|
139
|
+
"schemaVersion": MCP_SCHEMA_VERSION,
|
|
140
|
+
"owner": self.owner,
|
|
141
|
+
"generated_at": self.generated_at,
|
|
142
|
+
"resources": resources,
|
|
143
|
+
"pack": {
|
|
144
|
+
"uri": _uri(self.owner, "_pack"),
|
|
145
|
+
"name": f"context pack ({self.owner})",
|
|
146
|
+
"mimeType": "application/json",
|
|
147
|
+
"description": f"{len(self.episodes)} episode(s), query=\"{self.query}\"",
|
|
148
|
+
},
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def build_context_pack(rows: list[dict], *, owner: str, query: str = "",
|
|
153
|
+
generated_at: str | None = None,
|
|
154
|
+
learner_model: list[dict] | None = None,
|
|
155
|
+
theme_id: str | None = None,
|
|
156
|
+
vocabulary: list[dict] | None = None) -> ContextPack:
|
|
157
|
+
"""Assemble a ContextPack from recall rows (pure). ``learner_model`` is the
|
|
158
|
+
person's LearnerPattern rows (how they learn) — carried so an export is "my
|
|
159
|
+
learning *and how I learn it*". ``vocabulary`` (authored 만) 은 주제 타입 어휘를
|
|
160
|
+
함께 이전한다 — 어휘는 소유·이전 단위(THESIS T4·D11-R2 §8)."""
|
|
161
|
+
episodes = [dict(r) for r in rows]
|
|
162
|
+
return ContextPack(owner=owner, query=query, generated_at=generated_at,
|
|
163
|
+
episodes=episodes, learner_model=[dict(p) for p in (learner_model or [])],
|
|
164
|
+
theme_id=theme_id, vocabulary=[dict(t) for t in (vocabulary or [])])
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def write_pack(pack: ContextPack, out_dir: str | Path) -> dict:
|
|
168
|
+
"""Write pack.md / pack.json / mcp.json into ``out_dir``. Returns the paths."""
|
|
169
|
+
out = Path(out_dir)
|
|
170
|
+
out.mkdir(parents=True, exist_ok=True)
|
|
171
|
+
md_path = out / "pack.md"
|
|
172
|
+
json_path = out / "pack.json"
|
|
173
|
+
mcp_path = out / "mcp.json"
|
|
174
|
+
md_path.write_text(pack.to_markdown(), encoding="utf-8")
|
|
175
|
+
json_path.write_text(json.dumps(pack.to_json(), ensure_ascii=False, indent=2), encoding="utf-8")
|
|
176
|
+
mcp_path.write_text(json.dumps(pack.to_mcp_manifest(), ensure_ascii=False, indent=2), encoding="utf-8")
|
|
177
|
+
return {"markdown": str(md_path), "json": str(json_path), "mcp": str(mcp_path)}
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
# ── Public entry point ───────────────────────────────────────────────────────
|
|
181
|
+
|
|
182
|
+
def activate(query: str = "", *, owner: str, tenant: str | None = None,
|
|
183
|
+
domain: str | None = None, workspace: str | None = None,
|
|
184
|
+
tags: list[str] | None = None, rank: str = "preference", limit: int = 20,
|
|
185
|
+
include_model: bool = False,
|
|
186
|
+
generated_at: str | None = None, out_dir: str | Path | None = None,
|
|
187
|
+
theme_id: str | None = None, themes=None,
|
|
188
|
+
cfg=None, store=None) -> dict:
|
|
189
|
+
"""Recall owner-scoped records and build a portable context pack.
|
|
190
|
+
|
|
191
|
+
``domain`` (free, user-defined) optionally scopes the pack to one dataset
|
|
192
|
+
partition; omit to export across all of the owner's domains. Defaults to
|
|
193
|
+
``rank="preference"`` — an export leads with the owner's preference signals.
|
|
194
|
+
|
|
195
|
+
``include_model`` (**default False** — Vinc 기본 export 은 learner 섹션 부재:
|
|
196
|
+
설계 §13 D6 · WO §3 F6, 학습 프레이밍 미승계 §1). 마이그레이션(A+2, §10)의
|
|
197
|
+
무손실 이관 시에만 ``True`` 로 learner 신호(LearnerPattern)를 포함한다 — 이때만
|
|
198
|
+
pack 에 학습법 섹션이 실린다(D4 는 pack 이 신호를 담을 수 **있게** 하는 것이지
|
|
199
|
+
기본 노출이 아니다). Preference 신호(pinned/tags/access_count/last_access)는
|
|
200
|
+
rank 무관 항상 수록된다(§10·D4). Writes three files when ``out_dir`` given."""
|
|
201
|
+
res = q.recall(query, owner=owner, tenant=tenant, domain=domain,
|
|
202
|
+
workspace=workspace, tags=tags, rank=rank, limit=limit, cfg=cfg, store=store)
|
|
203
|
+
model_rows = (q.learner_model(owner=owner, cfg=cfg, store=store).get("rows", [])
|
|
204
|
+
if include_model else [])
|
|
205
|
+
# 주제 어휘 동봉(선택) — **authored 만**. proposed(재가 전)는 팩에 실리지 않는다
|
|
206
|
+
# (THESIS §6 잠정상태 · D11-R2 수용기준). 단일 필터 라인.
|
|
207
|
+
vocabulary = ([t for t in themes.list_types(theme_id) if t.get("status") == "authored"]
|
|
208
|
+
if (theme_id and themes is not None) else None)
|
|
209
|
+
pack = build_context_pack(res.get("rows", []), owner=owner, query=query,
|
|
210
|
+
generated_at=generated_at, learner_model=model_rows,
|
|
211
|
+
theme_id=theme_id, vocabulary=vocabulary)
|
|
212
|
+
out: dict = {
|
|
213
|
+
"owner": owner,
|
|
214
|
+
"episode_count": len(pack.episodes),
|
|
215
|
+
"learner_pattern_count": len(pack.learner_model),
|
|
216
|
+
"executed": res.get("executed", False),
|
|
217
|
+
}
|
|
218
|
+
if out_dir is not None:
|
|
219
|
+
out["written"] = write_pack(pack, out_dir)
|
|
220
|
+
else:
|
|
221
|
+
out["pack"] = {
|
|
222
|
+
"markdown": pack.to_markdown(),
|
|
223
|
+
"json": pack.to_json(),
|
|
224
|
+
"mcp": pack.to_mcp_manifest(),
|
|
225
|
+
}
|
|
226
|
+
return out
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
"""로컬 로그인 코어 — 온보딩 프로필 + 세션 토큰(설계 [[vinc.spec.architecture §4]], F4).
|
|
3
|
+
|
|
4
|
+
Vinc 는 단일 사용자·오프라인 앱이다. "로그인"은 외부 인증 서버 왕복이 아니라
|
|
5
|
+
**로컬 자격 파일에 대한 패스프레이즈 검증**이다(F4 — 네트워크 왕복 없음). 검증에
|
|
6
|
+
성공하면 사이드카가 임의 **세션 토큰**을 발급하고, 데스크톱 셸(Tauri)이 이후 모든
|
|
7
|
+
HTTP 요청에 ``Authorization: Bearer <token>`` 로 실어 보낸다. 루프백 바인딩
|
|
8
|
+
(127.0.0.1) + 토큰이 이중 방어다(§3-1).
|
|
9
|
+
|
|
10
|
+
자격 해시는 **argon2id**(설계 §3-1 명시)로, ``argon2-cffi`` 옵션 extra(``vinc[server]``)
|
|
11
|
+
에서 lazy-import 한다 — 코어는 의존성 0(F5: 키 없이 기동). 미설치 환경에선 온보딩·
|
|
12
|
+
패스프레이즈 검증만 불가하고(명확한 오류) 앱 기동·조회는 그대로 동작한다.
|
|
13
|
+
|
|
14
|
+
토큰 발급·검증은 stdlib ``secrets`` 만 쓴다(argon2 불요) — 어떤 경로도 소켓을 열지
|
|
15
|
+
않는다(F4 실측: 소켓 차단 하에서도 동작).
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import secrets
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
|
|
23
|
+
# argon2id 파라미터(자격 파일 헤더에 함께 저장 — 재해시 없이 검증 가능).
|
|
24
|
+
# 대화형 로그인 1회 검증 기준의 보수적 기본값. (RFC 9106 2nd 권장 근방.)
|
|
25
|
+
_ARGON2_TIME_COST = 3
|
|
26
|
+
_ARGON2_MEMORY_COST = 64 * 1024 # 64 MiB
|
|
27
|
+
_ARGON2_PARALLELISM = 4
|
|
28
|
+
|
|
29
|
+
_CRED_KIND = "vinc/local-credential"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class AuthUnavailable(RuntimeError):
|
|
33
|
+
"""argon2 extra 미설치 — 온보딩/패스프레이즈 검증 불가(``pip install vinc[server]``)."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _argon2():
|
|
37
|
+
"""argon2 PasswordHasher 를 lazy-import(코어 의존성 0 유지 — F5). 미설치 시
|
|
38
|
+
:class:`AuthUnavailable`."""
|
|
39
|
+
try:
|
|
40
|
+
from argon2 import PasswordHasher, Type
|
|
41
|
+
except ImportError as exc: # pragma: no cover - 설치 여부에 따라
|
|
42
|
+
raise AuthUnavailable(
|
|
43
|
+
"argon2id 자격에는 'vinc[server]' extra 가 필요하다 "
|
|
44
|
+
"(pip install argon2-cffi). 코어·조회는 이것 없이 동작한다."
|
|
45
|
+
) from exc
|
|
46
|
+
return PasswordHasher(time_cost=_ARGON2_TIME_COST, memory_cost=_ARGON2_MEMORY_COST,
|
|
47
|
+
parallelism=_ARGON2_PARALLELISM, type=Type.ID)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def cred_path(data_dir: str | Path) -> Path:
|
|
51
|
+
"""자격 파일 경로. 데이터 디렉토리 옆 ``auth.json``(§3-2 — 데이터·설정 동거)."""
|
|
52
|
+
return Path(data_dir).parent / "auth.json"
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# ── 온보딩 / 검증 ────────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
def create_profile(passphrase: str, *, path: str | Path, force: bool = False) -> dict:
|
|
58
|
+
"""온보딩: 패스프레이즈를 argon2id 로 해시해 자격 파일을 만든다(1회). 이미
|
|
59
|
+
있으면 ``force`` 없이 덮어쓰지 않는다(계정 유실 방지)."""
|
|
60
|
+
p = Path(path)
|
|
61
|
+
if p.exists() and not force:
|
|
62
|
+
raise FileExistsError(f"자격 파일 이미 존재: {p} (재설정은 force=True)")
|
|
63
|
+
if not passphrase:
|
|
64
|
+
raise ValueError("빈 패스프레이즈 금지")
|
|
65
|
+
ph = _argon2()
|
|
66
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
67
|
+
payload = {"kind": _CRED_KIND, "algo": "argon2id", "hash": ph.hash(passphrase)}
|
|
68
|
+
p.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
|
|
69
|
+
# 자격 파일은 소유자만 읽도록(단일 사용자 데스크톱 — §3-3).
|
|
70
|
+
try:
|
|
71
|
+
p.chmod(0o600)
|
|
72
|
+
except OSError:
|
|
73
|
+
pass
|
|
74
|
+
return {"created": str(p), "algo": "argon2id"}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def verify_passphrase(passphrase: str, *, path: str | Path) -> bool:
|
|
78
|
+
"""패스프레이즈를 자격 파일과 대조(로컬만 — 네트워크 왕복 없음, F4). 파일
|
|
79
|
+
부재·불일치 시 False. argon2 미설치 시 :class:`AuthUnavailable`."""
|
|
80
|
+
p = Path(path)
|
|
81
|
+
if not p.exists():
|
|
82
|
+
return False
|
|
83
|
+
from argon2.exceptions import VerifyMismatchError # lazy — extra 없으면 아래 _argon2 가 먼저 raise
|
|
84
|
+
ph = _argon2()
|
|
85
|
+
try:
|
|
86
|
+
stored = json.loads(p.read_text(encoding="utf-8")).get("hash", "")
|
|
87
|
+
except (OSError, ValueError):
|
|
88
|
+
return False
|
|
89
|
+
try:
|
|
90
|
+
return bool(ph.verify(stored, passphrase))
|
|
91
|
+
except VerifyMismatchError:
|
|
92
|
+
return False
|
|
93
|
+
except Exception: # noqa: BLE001 - 손상 해시 등은 인증 실패로 취급
|
|
94
|
+
return False
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# ── 세션 토큰 ────────────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
def issue_token(nbytes: int = 32) -> str:
|
|
100
|
+
"""새 세션 토큰 발급(stdlib secrets — 소켓 없음, F4). 프로세스 수명 동안만 유효."""
|
|
101
|
+
return secrets.token_urlsafe(nbytes)
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
class SessionAuth:
|
|
105
|
+
"""사이드카 세션 토큰 보관·검증. 기동 시 1개 발급, 셸에 전달. 모든 요청의
|
|
106
|
+
``Authorization: Bearer`` 헤더를 상수시간 비교한다."""
|
|
107
|
+
|
|
108
|
+
def __init__(self, token: str | None = None):
|
|
109
|
+
self.token = token or issue_token()
|
|
110
|
+
|
|
111
|
+
def check_header(self, header_value: str | None) -> bool:
|
|
112
|
+
"""``Authorization`` 헤더 값이 세션 토큰과 일치하는지(Bearer 스킴). 상수시간
|
|
113
|
+
비교(secrets.compare_digest)로 타이밍 누출 방지."""
|
|
114
|
+
if not header_value:
|
|
115
|
+
return False
|
|
116
|
+
parts = header_value.split(None, 1)
|
|
117
|
+
if len(parts) != 2 or parts[0].lower() != "bearer":
|
|
118
|
+
return False
|
|
119
|
+
return secrets.compare_digest(parts[1], self.token)
|