org-knowledge-layer 0.2.0__tar.gz → 0.3.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.
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/.claude/settings.local.json +26 -1
- org_knowledge_layer-0.3.0/CHANGELOG.md +58 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/PKG-INFO +12 -10
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/README.md +10 -8
- org_knowledge_layer-0.3.0/docs/DEPLOY.md +171 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/pyproject.toml +1 -1
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/src/okl/cli.py +26 -2
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/src/okl/client.py +29 -2
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/src/okl/mcp_server.py +35 -8
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/src/okl/service.py +44 -11
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/tests/test_okl.py +285 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/.claude/hooks/stop-okl-encode.sh +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/.claude/hooks/userpromptsubmit-okl-check.sh +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/.claude/settings.json +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/.github/pull_request_template.md +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/.github/workflows/ci.yml +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/.github/workflows/okl-verify.yml +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/.gitignore +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/AGENTS.md +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/CLAUDE.md +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/CONTRIBUTING.md +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/LICENSE +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/SECURITY.md +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/ci/okl-verify.yml +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/docs/ab-results-chart.png +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/docs/ab-results-chart.svg +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/docs/decisions/2026-07-17-flat-retrieval-until-scale.md +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/docs/decisions/2026-07-21-subject-tags-controlled-vocabulary.md +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/docs/okl-sixth-surface.excalidraw +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/docs/okl-sixth-surface.svg +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/docs/posts/01-memory-that-outlives-the-run.md +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/docs/posts/02-dont-let-a-step-grade-itself.md +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/docs/posts/03-enforcement-or-good-intentions.md +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/evals/README.md +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/evals/REPORT.md +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/evals/ab_harness.py +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/evals/results/ab-20260829-2300.json +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/evals/results/ab-20260829-2315.json +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/evals/results/ab-20260830-0003.json +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/evals/results/ab-20260830-0148.json +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/evals/results/ab-20260901-0133.json +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/evals/results/e2e-20260830/README.md +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/evals/results/e2e-20260830/briefed-userpromptsubmit-lint.yml +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/evals/results/e2e-20260830/control-lint.yml +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/evals/results/e2e-20260830/hook.log +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/evals/results/e2e-20260830/service-record-500.log +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/evals/results/e2e-20260830/session-briefed-pretooluse.txt +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/evals/results/e2e-20260830/session-briefed-userpromptsubmit.txt +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/evals/results/e2e-20260830/session-control.txt +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/evals/tasks.jsonl +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/hooks/stop-okl-encode.sh +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/hooks/userpromptsubmit-okl-check.sh +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/seed/dotnet-canon.json +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/seed/dotnet-decisions.json +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/seed/dotnet-defects.json +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/seed/dotnet-review-surfaces.json +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/seed/frontend-canon.json +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/seed/geospatial-deeptime-defects.json +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/seed/geospatial-defects.json +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/seed/geospatial-enforcement-defects.json +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/seed/geospatial-eval-defects.json +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/seed/rag-defects.json +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/seed/react-defects.json +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/src/okl/__init__.py +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/src/okl/__main__.py +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/src/okl/bootstrap.py +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/src/okl/core.py +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/src/okl/drift.py +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/src/okl/scaffold/MANIFEST.md +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/src/okl/scaffold/ci/method-gates.yml +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/src/okl/scaffold/ci/okl-verify.yml +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/src/okl/scaffold/claude/agents/architecture-reviewer.md +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/src/okl/scaffold/claude/commands/check-rules.md +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/src/okl/scaffold/claude/commands/feature-spec.md +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/src/okl/scaffold/claude/commands/seed-from-codebase.md +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/src/okl/scaffold/claude/rules/example-area.md +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/src/okl/scaffold/claude/skills/RECOMMENDED-COMPANIONS.md +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/src/okl/scaffold/claude/skills/encoding-loop/SKILL.md +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/src/okl/scaffold/claude/skills/verify-before-claiming/SKILL.md +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/src/okl/scaffold/evals/README.md +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/src/okl/scaffold/evals/cases.jsonl +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/src/okl/scaffold/evals/run_evals.py +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/src/okl/scaffold/gates/check-canon-size.sh +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/src/okl/scaffold/gates/check-diagram-pairs.sh +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/src/okl/scaffold/gates/check-doc-orphans.sh +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/src/okl/scaffold/gates/check-links.sh +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/src/okl/scaffold/gates/check-retractions.sh +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/src/okl/scaffold/gates/check-tombstones.sh +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/src/okl/scaffold/gates/run-gates.sh +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/src/okl/scaffold/hooks/hooks.json +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/src/okl/scaffold/hooks/stop-okl-encode.sh +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/src/okl/scaffold/hooks/userpromptsubmit-okl-check.sh +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/src/okl/scaffold/plugin/plugin.json +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/src/okl/scaffold/profiles/dotnet/README.md +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/src/okl/scaffold/profiles/dotnet/rules/architecture.md +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/src/okl/scaffold/profiles/dotnet/rules/messaging.md +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/src/okl/scaffold/profiles/dotnet/rules/performance-and-data.md +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/src/okl/scaffold/profiles/dotnet/rules/security.md +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/src/okl/scaffold/profiles/geospatial/README.md +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/src/okl/scaffold/profiles/geospatial/rules/geospatial-ml.md +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/src/okl/scaffold/profiles/python-rag/README.md +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/src/okl/scaffold/profiles/python-rag/rules/fastapi-backend.md +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/src/okl/scaffold/profiles/python-rag/rules/project-structure.md +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/src/okl/scaffold/profiles/python-rag/rules/rag-pipeline.md +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/src/okl/scaffold/profiles/react/README.md +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/src/okl/scaffold/profiles/react/rules/frontend.md +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/src/okl/scaffold/registries/RETRACTIONS.md +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/src/okl/scaffold/registries/tombstones.txt +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/src/okl/scaffold/root/CLAUDE.md +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/src/okl/scaffold/root/METHOD.md +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/src/okl/scaffold_cmd.py +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/src/okl/seed.py +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/src/okl/store.py +0 -0
- {org_knowledge_layer-0.2.0 → org_knowledge_layer-0.3.0}/tests/test_scaffold.py +0 -0
|
@@ -130,7 +130,32 @@
|
|
|
130
130
|
"Bash(check)",
|
|
131
131
|
"Bash(okl bootstrap *)",
|
|
132
132
|
"Bash(git -c core.hooksPath=/dev/null commit -qm 'First-run experience: seeding is a choice, briefings are capped, empty states are honest *)",
|
|
133
|
-
"Bash(git -c core.hooksPath=/dev/null commit -q --amend -F /tmp/msg.txt)"
|
|
133
|
+
"Bash(git -c core.hooksPath=/dev/null commit -q --amend -F /tmp/msg.txt)",
|
|
134
|
+
"Bash(git -c core.hooksPath=/dev/null commit -qm 'v0.2.0 *)",
|
|
135
|
+
"Bash(command -v postgres pg_ctl initdb)",
|
|
136
|
+
"Read(//opt/homebrew/opt/**)",
|
|
137
|
+
"Bash(python3 -c \"import mcp; print\\(mcp.__version__ if hasattr\\(mcp,'__version__'\\) else 'installed'\\)\")",
|
|
138
|
+
"Bash(python3 -m pip install -q \"psycopg[binary]>=3.1\")",
|
|
139
|
+
"Bash(python3 -m pip install -q \"mcp>=1.2\")",
|
|
140
|
+
"Bash(python3 -c \"import mcp; print\\('mcp installed'\\)\")",
|
|
141
|
+
"Bash(python3 -c \"import mcp, importlib.metadata as md; print\\('installed mcp version:', md.version\\('mcp'\\)\\)\")",
|
|
142
|
+
"Bash(python3 -c \"from mcp.server.mcpserver import MCPServer; print\\('MCPServer exists'\\); import inspect; print\\('has .tool\\(\\):', hasattr\\(MCPServer, 'tool'\\)\\); print\\('has .list_tools\\(\\):', hasattr\\(MCPServer,'list_tools'\\)\\); print\\('has .run\\(\\):', hasattr\\(MCPServer,'run'\\)\\)\")",
|
|
143
|
+
"Bash(python3 -m pytest -q -k \"mcp_server_builds or postgres_matches\" -v)",
|
|
144
|
+
"Bash(OKL_TEST_POSTGRES_URL=\"postgresql://joshuadell@/postgres?host=/tmp/oklpg&port=55432\" python3 -m pytest -q -k \"postgres_matches\" -v)",
|
|
145
|
+
"Bash(python3 -c \"import importlib.metadata as m; print\\(m.version\\('mcp'\\)\\)\")",
|
|
146
|
+
"Bash(OKL_TEST_POSTGRES_URL=\"postgresql://joshuadell@/postgres?host=/tmp/oklpg&port=55432\" python3 -m pytest -k \"postgres_matches\")",
|
|
147
|
+
"Bash(export OKL_TEST_POSTGRES_URL=\"postgresql://okl@/okltest?host=/tmp/oklpg&port=55432\")",
|
|
148
|
+
"Bash(python3 -m pytest -q -k \"postgres_matches\")",
|
|
149
|
+
"Bash(export OKL_DATABASE_URL=\"postgresql://okl@/okltest?host=/tmp/oklpg&port=55432\")",
|
|
150
|
+
"Bash(export OKL_TOKEN=\"deploy-test-secret\")",
|
|
151
|
+
"Bash(python3 -m uvicorn --factory okl.service:create_app --host 127.0.0.1 --port 8791)",
|
|
152
|
+
"Bash(echo \"started pid $!\")",
|
|
153
|
+
"Bash(curl -s http://127.0.0.1:8791/health)",
|
|
154
|
+
"Bash(OKL_TEST_POSTGRES_URL=\"postgresql://okl@/okltest?host=/tmp/oklpg&port=55432\" python3 -m pytest -q)",
|
|
155
|
+
"Bash(pkill -f uvicorn)",
|
|
156
|
+
"Bash(pg_ctl -D /tmp/oklpg/data stop -m immediate)",
|
|
157
|
+
"Bash(tmutil listlocalsnapshots *)",
|
|
158
|
+
"Bash(python3 -c \"import json,sys; print\\(', '.join\\(sorted\\(json.load\\(sys.stdin\\)['releases']\\)\\)\\)\")"
|
|
134
159
|
],
|
|
135
160
|
"additionalDirectories": [
|
|
136
161
|
"/Users/joshuadell/Dev/okl/e2e/scratch-briefed/.okl",
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.3.0
|
|
4
|
+
|
|
5
|
+
Everything here came from running three things that had been written but never
|
|
6
|
+
executed: the MCP server, the Postgres backend, and a deployment.
|
|
7
|
+
|
|
8
|
+
### Security
|
|
9
|
+
|
|
10
|
+
- **The service token now covers reads.** Previously `OKL_TOKEN` gated writes only, so
|
|
11
|
+
an unauthenticated `GET /nodes` returned the entire store — every recorded defect,
|
|
12
|
+
retired identifier and architecture decision. Every route now requires the token when
|
|
13
|
+
it is set, except `/health`, which is left open for schedulers and returns no record
|
|
14
|
+
content.
|
|
15
|
+
- **`okl connect --token` no longer commits your secret.** The token is stored in
|
|
16
|
+
cleartext in `.okl/config.json`, and a comment claimed the directory was gitignored
|
|
17
|
+
while nothing wrote a `.gitignore`. `okl init` and `okl connect` now write
|
|
18
|
+
`.okl/.gitignore`.
|
|
19
|
+
- **A rejected check no longer reports success.** A 401 surfaced as `ValueError` rather
|
|
20
|
+
than `OKLUnreachable`, so an unauthorized `okl check` exited 0 with a traceback — which
|
|
21
|
+
a pre-task hook reads as "no rules apply". It now fails closed with exit 2, as does
|
|
22
|
+
every other command, via a backstop in `main()`.
|
|
23
|
+
|
|
24
|
+
**Breaking:** if you run a service with `OKL_TOKEN` set, clients must upgrade too.
|
|
25
|
+
Clients older than 0.3.0 send no credential on `GET` requests and will get 401s from
|
|
26
|
+
`okl drift` and the recurrence metric. Upgrade the service and its clients together, or
|
|
27
|
+
unset `OKL_TOKEN` during the rollover.
|
|
28
|
+
|
|
29
|
+
### Fixed
|
|
30
|
+
|
|
31
|
+
- `uvicorn okl.service:app` served a module-level `None`: the process started, bound the
|
|
32
|
+
port, passed a port-liveness check and returned 500 to every request. The app is now
|
|
33
|
+
built lazily in a module `__getattr__`, so the standard ASGI entrypoint works while
|
|
34
|
+
importing the module still does not touch the database.
|
|
35
|
+
- The MCP server could not start under `mcp` 2.x, which renamed `FastMCP` to
|
|
36
|
+
`MCPServer` — and the error handler told you to install the extra you had just
|
|
37
|
+
installed. Both names are tried, and the real import error is reported.
|
|
38
|
+
- Every MCP `okl_record` call with `scope="repo"` failed. The repo default used
|
|
39
|
+
`setdefault`, which cannot replace an explicit `None`, and the MCP tools pass every
|
|
40
|
+
field explicitly.
|
|
41
|
+
- MCP validation errors raised as an opaque "Error executing tool". They now return the
|
|
42
|
+
complaint, so an agent that invents a tag is told the vocabulary.
|
|
43
|
+
|
|
44
|
+
### Added
|
|
45
|
+
|
|
46
|
+
- `docs/DEPLOY.md`: the shared-service deployment path, including a throwaway Postgres
|
|
47
|
+
for trying it locally and what each failure mode looks like. Every command in it was
|
|
48
|
+
run against a real Postgres and a real service.
|
|
49
|
+
- Tests covering the live MCP server, the ASGI entrypoint, service auth on reads, the
|
|
50
|
+
fail-closed 401, and the config `.gitignore`.
|
|
51
|
+
- The Postgres/SQLite parity test now runs in a scratch schema it creates and drops. The
|
|
52
|
+
first version opened with `DELETE FROM node` against whatever `OKL_TEST_POSTGRES_URL`
|
|
53
|
+
pointed at, which would have destroyed the store of anyone who set it to their real
|
|
54
|
+
service.
|
|
55
|
+
|
|
56
|
+
## 0.2.0 and earlier
|
|
57
|
+
|
|
58
|
+
See the git history.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: org-knowledge-layer
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: Org Knowledge Layer — an installable sixth surface that carries encoded engineering lessons across repos.
|
|
5
5
|
Author: Joshua Dell
|
|
6
6
|
License: MIT
|
|
@@ -566,17 +566,19 @@ okl serve --port 8080
|
|
|
566
566
|
|
|
567
567
|
```bash
|
|
568
568
|
pip install "org-knowledge-layer[service]"
|
|
569
|
-
OKL_DATABASE_URL="
|
|
569
|
+
OKL_DATABASE_URL="postgresql://user:pass@host/okl" OKL_TOKEN="a-shared-secret" okl serve
|
|
570
570
|
# repos then: okl connect https://your-host --token a-shared-secret
|
|
571
571
|
```
|
|
572
572
|
|
|
573
|
-
`OKL_TOKEN
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
573
|
+
**Set `OKL_TOKEN`.** With it, every route requires the bearer token except `/health`
|
|
574
|
+
(left open so schedulers can probe it). Without it, every route is open — including
|
|
575
|
+
`GET /nodes`, which hands the whole store to anyone who can reach the port. A mature
|
|
576
|
+
store is a catalogue of your known defects and internal architecture, which is a map of
|
|
577
|
+
where you are weak. It is a single shared secret with no per-repo scoping or rotation;
|
|
578
|
+
put a real authenticating proxy in front if you need more.
|
|
579
|
+
|
|
580
|
+
Full instructions, including a throwaway Postgres for trying it locally and what the
|
|
581
|
+
failure modes look like: **[docs/DEPLOY.md](docs/DEPLOY.md)**.
|
|
580
582
|
|
|
581
583
|
## Agent integration (MCP)
|
|
582
584
|
|
|
@@ -534,17 +534,19 @@ okl serve --port 8080
|
|
|
534
534
|
|
|
535
535
|
```bash
|
|
536
536
|
pip install "org-knowledge-layer[service]"
|
|
537
|
-
OKL_DATABASE_URL="
|
|
537
|
+
OKL_DATABASE_URL="postgresql://user:pass@host/okl" OKL_TOKEN="a-shared-secret" okl serve
|
|
538
538
|
# repos then: okl connect https://your-host --token a-shared-secret
|
|
539
539
|
```
|
|
540
540
|
|
|
541
|
-
`OKL_TOKEN
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
541
|
+
**Set `OKL_TOKEN`.** With it, every route requires the bearer token except `/health`
|
|
542
|
+
(left open so schedulers can probe it). Without it, every route is open — including
|
|
543
|
+
`GET /nodes`, which hands the whole store to anyone who can reach the port. A mature
|
|
544
|
+
store is a catalogue of your known defects and internal architecture, which is a map of
|
|
545
|
+
where you are weak. It is a single shared secret with no per-repo scoping or rotation;
|
|
546
|
+
put a real authenticating proxy in front if you need more.
|
|
547
|
+
|
|
548
|
+
Full instructions, including a throwaway Postgres for trying it locally and what the
|
|
549
|
+
failure modes look like: **[docs/DEPLOY.md](docs/DEPLOY.md)**.
|
|
548
550
|
|
|
549
551
|
## Agent integration (MCP)
|
|
550
552
|
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# Deploying the shared service
|
|
2
|
+
|
|
3
|
+
okl runs in two modes. **Local mode** keeps a SQLite file next to your config and needs
|
|
4
|
+
no deployment; it is the right choice for one person on one machine, and everything in
|
|
5
|
+
the README works there. **Shared mode** puts one service in front of one database so
|
|
6
|
+
that every repo, on every machine, reads and writes the same curated knowledge. This
|
|
7
|
+
document covers the second.
|
|
8
|
+
|
|
9
|
+
The reason to bother: a lesson recorded in one repo is worth something only if a
|
|
10
|
+
different repo, weeks later, is told about it before it makes the same mistake. That
|
|
11
|
+
cross-repo hop is the whole point of the layer, and it needs a shared store.
|
|
12
|
+
|
|
13
|
+
Every command below was run against a real Postgres and a real service while writing
|
|
14
|
+
this page. Where something is **not** verified, it says so.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## 1. A database
|
|
19
|
+
|
|
20
|
+
Postgres is the supported shared backend. The service selects it with one environment
|
|
21
|
+
variable and nothing else in the code changes:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
export OKL_DATABASE_URL="postgresql://user:pass@host:5432/okl"
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Any managed Postgres works — RDS, Cloud SQL, Neon, Supabase, Fly Postgres. The schema
|
|
28
|
+
is created on first connect; there is no migration step to run.
|
|
29
|
+
|
|
30
|
+
To try it locally first, a throwaway cluster in four commands:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
initdb -D /tmp/oklpg/data -U okl --auth=trust
|
|
34
|
+
pg_ctl -D /tmp/oklpg/data -o "-p 55432 -k /tmp/oklpg -c listen_addresses=''" start
|
|
35
|
+
createdb -h /tmp/oklpg -p 55432 -U okl okltest
|
|
36
|
+
export OKL_TEST_POSTGRES_URL="postgresql://okl@/okltest?host=/tmp/oklpg&port=55432"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
That last variable also switches on the backend-parity test, which loads one corpus into
|
|
40
|
+
both SQLite and Postgres and asserts they rank the same record first for the same query:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
pytest -k postgres_matches
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Run it when you change anything about retrieval. SQLite ranks with BM25 and Postgres with
|
|
47
|
+
`ts_rank`; they have diverged before, and the failure mode is quiet — a repo that moves to
|
|
48
|
+
the shared service silently gets worse retrieval than it had on a laptop. The test builds
|
|
49
|
+
its tables in a temporary schema and drops them afterwards, so it is safe to point at a
|
|
50
|
+
database that already has data, though pointing it at production is still a strange thing
|
|
51
|
+
to do.
|
|
52
|
+
|
|
53
|
+
## 2. A token
|
|
54
|
+
|
|
55
|
+
**Set `OKL_TOKEN`.** It is optional in the code and mandatory in practice:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
export OKL_TOKEN="$(python3 -c 'import secrets; print(secrets.token_urlsafe(32))')"
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
With it set, every endpoint requires `Authorization: Bearer <token>` except `/health`.
|
|
62
|
+
Without it, every endpoint is open to anyone who can reach the port — including
|
|
63
|
+
`GET /nodes`, which returns the entire store in one request. A mature okl store is a
|
|
64
|
+
catalogue of your organization's known defects, its retired identifiers and its internal
|
|
65
|
+
architecture decisions. That is a map of where you are weak, and it is precisely the
|
|
66
|
+
material the layer exists to accumulate.
|
|
67
|
+
|
|
68
|
+
`/health` stays open deliberately: schedulers and load balancers probe it before they
|
|
69
|
+
hold any credential, and a service that cannot be health-checked never goes live. It
|
|
70
|
+
returns a record count and a backend name, never record content.
|
|
71
|
+
|
|
72
|
+
The token is a single shared secret with no per-repo scoping, rotation or audit trail.
|
|
73
|
+
That is honest for what this is. If you need more, put a real authenticating proxy in
|
|
74
|
+
front and leave `OKL_TOKEN` set underneath as defence in depth.
|
|
75
|
+
|
|
76
|
+
## 3. Run it
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
pip install "org-knowledge-layer[service]"
|
|
80
|
+
okl serve --port 8080 # convenience wrapper
|
|
81
|
+
uvicorn okl.service:app --host 0.0.0.0 --port 8080 # standard ASGI entrypoint
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Both work and serve the same app. Use the second one under a process manager or in a
|
|
85
|
+
container, since it is the form every platform's default start command takes.
|
|
86
|
+
|
|
87
|
+
Confirm it is up:
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
curl -s localhost:8080/health
|
|
91
|
+
# {"ok":true,"nodes":5,"backend":"postgresql"}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
`backend` in that response is worth reading on every deploy. If it says `sqlite` when you
|
|
95
|
+
expected `postgresql`, `OKL_DATABASE_URL` did not reach the process, and the service is
|
|
96
|
+
happily serving an empty file-backed store that will vanish with the container.
|
|
97
|
+
|
|
98
|
+
## 4. Point repos at it
|
|
99
|
+
|
|
100
|
+
In each repo:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
okl init --repo checkout-api
|
|
104
|
+
okl connect https://okl.internal --token "$OKL_TOKEN"
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`connect` writes the URL and token to `.okl/config.json`, and okl drops a `.gitignore`
|
|
108
|
+
inside `.okl/` so that file is never committed. Prefer supplying the token through the
|
|
109
|
+
`OKL_TOKEN` environment variable in CI and shared machines; the config file is a
|
|
110
|
+
convenience for a developer laptop.
|
|
111
|
+
|
|
112
|
+
That is the whole client setup. From then on:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
# in checkout-api
|
|
116
|
+
okl record --type Defect --scope org \
|
|
117
|
+
--title "Refund amount trusted from the client body" \
|
|
118
|
+
--symptom "a refund endpoint reads amount from the request" \
|
|
119
|
+
--fix "look the original charge up server-side and refund that"
|
|
120
|
+
|
|
121
|
+
# later, in billing-worker — a different repo, a different machine
|
|
122
|
+
okl check --task "add an endpoint that issues a refund" --format actions
|
|
123
|
+
# OKL — 1 rule(s) apply before you start:
|
|
124
|
+
# - FIX: Refund amount trusted from the client body [when: a refund endpoint reads amount from the request]
|
|
125
|
+
# -> look the original charge up server-side and refund that
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Note the scope. `--scope org` is what makes a lesson cross repo boundaries; `--scope repo`
|
|
129
|
+
keeps it local to the repo that recorded it. Getting this wrong is the most common way a
|
|
130
|
+
shared store disappoints: everything is recorded, nothing propagates.
|
|
131
|
+
|
|
132
|
+
## 5. What failure looks like
|
|
133
|
+
|
|
134
|
+
The service being unreachable, or refusing your token, must never be reported as "no
|
|
135
|
+
rules apply". Silence and safety are indistinguishable to an agent, and only one of them
|
|
136
|
+
is safe. Both cases exit non-zero and say which is which:
|
|
137
|
+
|
|
138
|
+
```
|
|
139
|
+
$ okl check --task "issue a refund" # no token
|
|
140
|
+
OKL REFUSED THE CHECK — refusing to report a clean check.
|
|
141
|
+
OKL service rejected the request (401): missing or bad bearer token
|
|
142
|
+
|
|
143
|
+
$ okl check --task "issue a refund" # service down
|
|
144
|
+
OKL UNREACHABLE — refusing to report a clean check.
|
|
145
|
+
OKL service unreachable at http://okl.internal/check: ...
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
The pre-task hook and the CI verifier both treat a non-zero exit as a block. If you wrap
|
|
149
|
+
okl in your own tooling, do the same: a check that cannot run has not passed.
|
|
150
|
+
|
|
151
|
+
## Containers
|
|
152
|
+
|
|
153
|
+
**Not verified.** No Docker daemon was available where this was written, so the project
|
|
154
|
+
ships no Dockerfile rather than an untested one. The service is an ordinary ASGI app with
|
|
155
|
+
no filesystem state when `OKL_DATABASE_URL` points at Postgres, so a container is four
|
|
156
|
+
lines over `python:3.12-slim`: install `org-knowledge-layer[service]`, expose the port,
|
|
157
|
+
and run the `uvicorn okl.service:app` command above. Set `OKL_DATABASE_URL` and
|
|
158
|
+
`OKL_TOKEN` as secrets, and point the platform's health check at `/health`.
|
|
159
|
+
|
|
160
|
+
If you build one, a PR adding it — with the build running in CI, so the claim is backed
|
|
161
|
+
by something — is welcome.
|
|
162
|
+
|
|
163
|
+
## Cost of the shared mode
|
|
164
|
+
|
|
165
|
+
Worth stating plainly before you commit to it. One service and one database is one more
|
|
166
|
+
thing to run, monitor and back up, and okl gives you no operational tooling for any of
|
|
167
|
+
that. It has no migrations, no backup command, no per-repo access control and no audit
|
|
168
|
+
log. Local mode has none of those problems.
|
|
169
|
+
|
|
170
|
+
Move to shared mode when a second repo actually needs a first repo's lessons. Before
|
|
171
|
+
that, the file on your laptop is doing the same job with none of the operations.
|
|
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
|
|
|
6
6
|
# Distribution name only. The CLI command, the import package, and the repo are all `okl`;
|
|
7
7
|
# PyPI rejects `okl` as too similar to the existing `oki` (l/i are treated as confusable).
|
|
8
8
|
name = "org-knowledge-layer"
|
|
9
|
-
version = "0.
|
|
9
|
+
version = "0.3.0"
|
|
10
10
|
description = "Org Knowledge Layer — an installable sixth surface that carries encoded engineering lessons across repos."
|
|
11
11
|
readme = "README.md"
|
|
12
12
|
requires-python = ">=3.10"
|
|
@@ -184,6 +184,13 @@ def cmd_check(args) -> int:
|
|
|
184
184
|
# FAIL CLOSED — loud, non-zero, no reassuring empty result.
|
|
185
185
|
print(f"OKL UNREACHABLE — refusing to report a clean check.\n{e}", file=sys.stderr)
|
|
186
186
|
return 2
|
|
187
|
+
except ValueError as e:
|
|
188
|
+
# A 4xx (usually a 401 against a token-protected service) is a REFUSED check,
|
|
189
|
+
# not a clean one, so it fails closed just the same. It gets its own message
|
|
190
|
+
# because the fix is different: a credential, not connectivity. Before this,
|
|
191
|
+
# an unauthorized check exited 0 with a raw urllib traceback.
|
|
192
|
+
print(f"OKL REFUSED THE CHECK — refusing to report a clean check.\n{e}", file=sys.stderr)
|
|
193
|
+
return 2
|
|
187
194
|
if args.format == "json":
|
|
188
195
|
_print_json(result)
|
|
189
196
|
elif args.format == "actions":
|
|
@@ -203,7 +210,16 @@ def cmd_record(args) -> int:
|
|
|
203
210
|
id=args.id)
|
|
204
211
|
if args.repo:
|
|
205
212
|
kwargs["repo"] = args.repo
|
|
206
|
-
|
|
213
|
+
try:
|
|
214
|
+
node_id = client.record(**{k: v for k, v in kwargs.items() if v is not None})
|
|
215
|
+
except ValueError as e:
|
|
216
|
+
# An unknown tag or a malformed scope is the caller's mistake, and the exception
|
|
217
|
+
# text names the vocabulary they need. A traceback buries that under a stack.
|
|
218
|
+
print(f"NOT RECORDED — {e}", file=sys.stderr)
|
|
219
|
+
return 2
|
|
220
|
+
except OKLUnreachable as e:
|
|
221
|
+
print(f"NOT RECORDED — {e}", file=sys.stderr)
|
|
222
|
+
return 2
|
|
207
223
|
print(node_id)
|
|
208
224
|
return 0
|
|
209
225
|
|
|
@@ -589,7 +605,15 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
589
605
|
|
|
590
606
|
def main(argv: list[str] | None = None) -> int:
|
|
591
607
|
args = build_parser().parse_args(argv)
|
|
592
|
-
|
|
608
|
+
try:
|
|
609
|
+
return args.func(args)
|
|
610
|
+
except (ValueError, OKLUnreachable) as e:
|
|
611
|
+
# The backstop, so no command can ever answer a rejected or unreachable service
|
|
612
|
+
# with a Python traceback. Commands that can say something more specific catch
|
|
613
|
+
# these themselves and never reach here; this exists so the ones that do not —
|
|
614
|
+
# and the ones added later — still exit non-zero with a line a human can act on.
|
|
615
|
+
print(f"OKL: {e}", file=sys.stderr)
|
|
616
|
+
return 2
|
|
593
617
|
|
|
594
618
|
|
|
595
619
|
if __name__ == "__main__":
|
|
@@ -40,6 +40,15 @@ def load_config() -> dict[str, Any]:
|
|
|
40
40
|
def save_config(data: dict[str, Any], root: Path | None = None) -> Path:
|
|
41
41
|
d = (root or Path.cwd()) / CONFIG_DIR
|
|
42
42
|
d.mkdir(parents=True, exist_ok=True)
|
|
43
|
+
# Ignore the whole directory, from inside it. Everything okl writes here is either
|
|
44
|
+
# machine-local (`okl_bin`, an absolute interpreter path) or secret (`token`, the
|
|
45
|
+
# service bearer credential that `okl connect --token` stores in cleartext), and the
|
|
46
|
+
# local store lands here too. The code claimed ".okl/ is gitignored" while writing
|
|
47
|
+
# nothing to make that true, so a `git add .` after `okl connect --token` committed
|
|
48
|
+
# a shared secret. A .gitignore inside .okl/ needs no edit to the repo's own.
|
|
49
|
+
gitignore = d / ".gitignore"
|
|
50
|
+
if not gitignore.exists():
|
|
51
|
+
gitignore.write_text("# okl: machine-local config, credentials and local store\n*\n")
|
|
43
52
|
path = d / CONFIG_FILE
|
|
44
53
|
path.write_text(json.dumps(data, indent=2) + "\n")
|
|
45
54
|
return path
|
|
@@ -115,7 +124,11 @@ class Client:
|
|
|
115
124
|
def record(self, **kwargs) -> str:
|
|
116
125
|
# Default the repo in BOTH modes: `--scope repo` needs it to become repo:<name>,
|
|
117
126
|
# and the remote path used to skip this (found by E2E: 400 on every repo-scoped record).
|
|
118
|
-
|
|
127
|
+
# `setdefault` is not enough: callers that pass every field explicitly (the MCP
|
|
128
|
+
# tools do) send repo=None, so the key EXISTS and setdefault leaves the None in
|
|
129
|
+
# place. Found by live-testing the MCP server: every scope="repo" record failed.
|
|
130
|
+
if kwargs.get("repo") is None:
|
|
131
|
+
kwargs["repo"] = self.repo
|
|
119
132
|
if self.mode == "remote":
|
|
120
133
|
return self._post("/record", kwargs)["id"]
|
|
121
134
|
return core.record(self._local_store(), **kwargs)
|
|
@@ -157,10 +170,24 @@ class Client:
|
|
|
157
170
|
return self._local_store().all_nodes()
|
|
158
171
|
|
|
159
172
|
def _get(self, path: str) -> dict:
|
|
173
|
+
# The token goes on GETs too. It used to go only on POSTs, which was survivable
|
|
174
|
+
# only because the service left reads open; once reads are gated, an unauthenticated
|
|
175
|
+
# GET makes drift detection (/nodes) and the recurrence metric fail against every
|
|
176
|
+
# private deployment — and a 401 here surfaces as "unreachable", i.e. as an outage.
|
|
160
177
|
url = self.service_url.rstrip("/") + path
|
|
178
|
+
req = _req.Request(url)
|
|
179
|
+
token = os.environ.get("OKL_TOKEN") or self.config.get("token")
|
|
180
|
+
if token:
|
|
181
|
+
req.add_header("Authorization", f"Bearer {token}")
|
|
161
182
|
try:
|
|
162
|
-
with _req.urlopen(
|
|
183
|
+
with _req.urlopen(req, timeout=10) as resp:
|
|
163
184
|
return json.loads(resp.read())
|
|
185
|
+
except HTTPError as e:
|
|
186
|
+
if 400 <= e.code < 500:
|
|
187
|
+
raise ValueError(
|
|
188
|
+
f"OKL service rejected the request ({e.code} {e.reason}). "
|
|
189
|
+
"If this is 401, set OKL_TOKEN or add \"token\" to .okl/config.json.") from e
|
|
190
|
+
raise OKLUnreachable(f"OKL service error at {url}: {e.code} {e.reason}") from e
|
|
164
191
|
except URLError as e:
|
|
165
192
|
raise OKLUnreachable(f"OKL service unreachable at {url}: {e}") from e
|
|
166
193
|
|
|
@@ -13,12 +13,33 @@ from .client import Client, OKLUnreachable
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
def _build():
|
|
16
|
-
|
|
17
|
-
from mcp.server.fastmcp import FastMCP
|
|
18
|
-
except ImportError as e: # pragma: no cover
|
|
19
|
-
raise RuntimeError("The MCP server needs the 'mcp' package — install okl[mcp]") from e
|
|
16
|
+
"""Construct the MCP server, tolerating both major versions of the SDK.
|
|
20
17
|
|
|
21
|
-
mcp
|
|
18
|
+
The class was renamed in mcp 2.x: `mcp.server.fastmcp.FastMCP` became
|
|
19
|
+
`mcp.server.mcpserver.MCPServer`. The decorator API we use (`.tool()`) is the same
|
|
20
|
+
on both, so try the newer name first and fall back. Without this, `pip install
|
|
21
|
+
org-knowledge-layer[mcp]` resolves to 2.x and every tool call fails.
|
|
22
|
+
"""
|
|
23
|
+
server_cls = None
|
|
24
|
+
errors = []
|
|
25
|
+
for module, name in (("mcp.server.mcpserver", "MCPServer"), # mcp >= 2
|
|
26
|
+
("mcp.server.fastmcp", "FastMCP")): # mcp 1.x
|
|
27
|
+
try:
|
|
28
|
+
server_cls = getattr(__import__(module, fromlist=[name]), name)
|
|
29
|
+
break
|
|
30
|
+
except (ImportError, AttributeError) as e:
|
|
31
|
+
errors.append(f"{module}.{name}: {e}")
|
|
32
|
+
if server_cls is None:
|
|
33
|
+
# Surface the REAL cause. Saying "install okl[mcp]" to someone who just did is
|
|
34
|
+
# the same failure as reporting a validation error as an outage: the message
|
|
35
|
+
# sends them to fix a thing that is not broken.
|
|
36
|
+
raise RuntimeError(
|
|
37
|
+
"Could not load an MCP server class from the installed 'mcp' package.\n"
|
|
38
|
+
+ "\n".join(f" tried {e}" for e in errors)
|
|
39
|
+
+ "\nInstall the extra with `pip install \"org-knowledge-layer[mcp]\"`, or report "
|
|
40
|
+
"this if the SDK has changed again.")
|
|
41
|
+
|
|
42
|
+
mcp = server_cls("okl")
|
|
22
43
|
client = Client()
|
|
23
44
|
|
|
24
45
|
@mcp.tool()
|
|
@@ -62,9 +83,15 @@ def _build():
|
|
|
62
83
|
tags (comma-sep, controlled vocabulary — e.g. react, security,
|
|
63
84
|
eval-integrity) categorize the subject so `check` can filter by interest.
|
|
64
85
|
"""
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
86
|
+
try:
|
|
87
|
+
node_id = client.record(type=type, title=title, scope=scope, body=body,
|
|
88
|
+
status=status, found_by=found_by, ttl_days=ttl_days,
|
|
89
|
+
repo=repo, symptom=symptom, fix=fix, files=files, tags=tags)
|
|
90
|
+
except ValueError as e:
|
|
91
|
+
# Hand the agent the actual complaint (unknown tag, bad scope) so it can fix
|
|
92
|
+
# its own call. Raising here surfaces as an opaque "Error executing tool",
|
|
93
|
+
# which reads like an outage and teaches the agent nothing.
|
|
94
|
+
return f"NOT RECORDED — {e}"
|
|
68
95
|
return f"recorded {node_id} ({type}, {scope})"
|
|
69
96
|
|
|
70
97
|
@mcp.tool()
|
|
@@ -68,19 +68,28 @@ class VerifyReq(BaseModel):
|
|
|
68
68
|
def create_app(store: Store | None = None) -> FastAPI:
|
|
69
69
|
app = FastAPI(title="OKL — the sixth surface", version="0.1.0")
|
|
70
70
|
_store = store or Store(os.environ.get("OKL_DATABASE_URL"))
|
|
71
|
-
# Optional shared-secret gate. If OKL_TOKEN is set
|
|
72
|
-
|
|
71
|
+
# Optional shared-secret gate. If OKL_TOKEN is set it covers READS as well as
|
|
72
|
+
# writes. Reads used to be open while writes were gated, which meant a deployed
|
|
73
|
+
# service handed anyone who found the URL a `GET /nodes` dump of the org's entire
|
|
74
|
+
# encoded body — its known defects, its retired identifiers, its architecture
|
|
75
|
+
# decisions. That is a catalogue of where the org is weak, and it is exactly the
|
|
76
|
+
# material the layer exists to collect. If you set a token, you want it private.
|
|
77
|
+
token = os.environ.get("OKL_TOKEN")
|
|
73
78
|
|
|
74
79
|
def _auth(authorization: str | None) -> None:
|
|
75
|
-
if
|
|
80
|
+
if token and authorization != f"Bearer {token}":
|
|
76
81
|
raise HTTPException(status_code=401, detail="missing or bad bearer token")
|
|
77
82
|
|
|
78
83
|
@app.get("/health")
|
|
79
84
|
def health() -> dict[str, Any]:
|
|
85
|
+
# Deliberately open even when a token is set: schedulers and load balancers
|
|
86
|
+
# probe this before they hold any credential, and a deploy that cannot be
|
|
87
|
+
# health-checked never goes live. It returns a count and a backend name, no content.
|
|
80
88
|
return {"ok": True, "nodes": len(_store.all_nodes()), "backend": _store.url.split(":")[0]}
|
|
81
89
|
|
|
82
90
|
@app.post("/check")
|
|
83
|
-
def check(req: CheckReq) -> dict[str, Any]:
|
|
91
|
+
def check(req: CheckReq, authorization: str | None = Header(default=None)) -> dict[str, Any]:
|
|
92
|
+
_auth(authorization)
|
|
84
93
|
return core.check(_store, req.repo, req.task, limit=req.limit,
|
|
85
94
|
interests=req.interests)
|
|
86
95
|
|
|
@@ -96,7 +105,8 @@ def create_app(store: Store | None = None) -> FastAPI:
|
|
|
96
105
|
return {"id": node_id}
|
|
97
106
|
|
|
98
107
|
@app.post("/search")
|
|
99
|
-
def search(req: SearchReq) -> dict[str, Any]:
|
|
108
|
+
def search(req: SearchReq, authorization: str | None = Header(default=None)) -> dict[str, Any]:
|
|
109
|
+
_auth(authorization)
|
|
100
110
|
return {"results": core.search(_store, req.query, req.scope, req.node_types, req.limit)}
|
|
101
111
|
|
|
102
112
|
@app.post("/link")
|
|
@@ -114,12 +124,14 @@ def create_app(store: Store | None = None) -> FastAPI:
|
|
|
114
124
|
raise HTTPException(status_code=400, detail=str(e)) from e
|
|
115
125
|
|
|
116
126
|
@app.get("/metric/recurrence")
|
|
117
|
-
def recurrence() -> dict[str, Any]:
|
|
127
|
+
def recurrence(authorization: str | None = Header(default=None)) -> dict[str, Any]:
|
|
128
|
+
_auth(authorization)
|
|
118
129
|
rows = _store.recurrence_after_arming()
|
|
119
130
|
return {"recurrence_after_arming": rows, "count": len(rows)}
|
|
120
131
|
|
|
121
132
|
@app.get("/nodes")
|
|
122
|
-
def nodes() -> dict[str, Any]:
|
|
133
|
+
def nodes(authorization: str | None = Header(default=None)) -> dict[str, Any]:
|
|
134
|
+
_auth(authorization)
|
|
123
135
|
from dataclasses import asdict
|
|
124
136
|
rows = [asdict(n) for n in _store.all_nodes()]
|
|
125
137
|
return {"nodes": rows, "count": len(rows)}
|
|
@@ -127,11 +139,32 @@ def create_app(store: Store | None = None) -> FastAPI:
|
|
|
127
139
|
return app
|
|
128
140
|
|
|
129
141
|
|
|
130
|
-
|
|
142
|
+
_app: FastAPI | None = None
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
def __getattr__(name: str):
|
|
146
|
+
"""Build `app` on first attribute access, not at import.
|
|
147
|
+
|
|
148
|
+
Every ASGI host — uvicorn, gunicorn, a platform's default start command — is
|
|
149
|
+
pointed at `module:app`, and that is what this module's own docstring tells you
|
|
150
|
+
to run. `app` used to be a module-level `None` that `run()` reassigned as a side
|
|
151
|
+
effect, so `uvicorn okl.service:app` served a None: the process started, bound the
|
|
152
|
+
port, looked healthy to anything watching the port, and answered 500 to every
|
|
153
|
+
request. A deploy that fails at startup is a nuisance; one that comes up and then
|
|
154
|
+
fails every call is an outage that reads as a bug in the caller.
|
|
155
|
+
|
|
156
|
+
PEP 562 module `__getattr__` fires only when normal lookup fails, which is what
|
|
157
|
+
keeps the database out of import time: `import okl.service` still touches nothing,
|
|
158
|
+
so the CLI and the tests can import this module without a configured backend.
|
|
159
|
+
"""
|
|
160
|
+
if name == "app":
|
|
161
|
+
global _app
|
|
162
|
+
if _app is None:
|
|
163
|
+
_app = create_app()
|
|
164
|
+
return _app
|
|
165
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
131
166
|
|
|
132
167
|
|
|
133
168
|
def run(host: str = "0.0.0.0", port: int = 8080) -> None:
|
|
134
169
|
import uvicorn
|
|
135
|
-
|
|
136
|
-
app = create_app()
|
|
137
|
-
uvicorn.run(app, host=host, port=port)
|
|
170
|
+
uvicorn.run(create_app(), host=host, port=port)
|