yichus 0.2.0 → 0.3.0

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.
@@ -0,0 +1,97 @@
1
+ package Demo/Portable/RefundPolicy
2
+
3
+ # Every refund decision, as one pure step. Given the refund and the record
4
+ # of every request and answer so far, `step` says what happens next: the
5
+ # one request the engine must perform, or the decision. The engine holds
6
+ # no refund rule of its own. `replay` reads a stored record back through
7
+ # `step`, with no effects, and refuses a record this program would not
8
+ # have made.
9
+
10
+ from Bosatsu/Predef import Int, Bool, String, List, Option, Some, None, True, False, LT, cmp_Int, eq_Int, eq_String, length_List, foldl_List
11
+
12
+ export (Refund(), Request(), Answer(), Exchange(), Decision(), Next(), step, replay, same_request, same_refund, small_refund_cents, most_requests)
13
+
14
+ struct Refund(order: String, amount_cents: Int, reason: String)
15
+
16
+ # The only things a refund can ask the outside world for. A new kind of
17
+ # request is a change to this enum and to every engine, never a rule an
18
+ # engine adds by itself.
19
+ enum Request:
20
+ AskJudge(reason: String)
21
+
22
+ # An answer, decoded at the boundary into a typed value before the program
23
+ # sees it.
24
+ enum Answer:
25
+ Judged(covered: Bool)
26
+ JudgeFailed(why: String)
27
+
28
+ struct Exchange(request: Request, answer: Answer)
29
+
30
+ enum Decision:
31
+ Approved
32
+ Denied
33
+ # The judge failed every time it was asked: a person decides.
34
+ Escalated
35
+
36
+ # Named so as to clash with no constructor an engine imports: `Done` is a
37
+ # route's reply and a loop's end, and `Decided` a decision call's result.
38
+ enum Next:
39
+ Ruled(decision: Decision)
40
+ Perform(request: Request)
41
+
42
+ # Refunds below this are approved without asking.
43
+ small_refund_cents = 2000
44
+
45
+ # A refund asks the judge at most this many times, then a person decides.
46
+ most_requests = 3
47
+
48
+ def last_answer(record: List[Exchange]) -> Option[Answer]:
49
+ foldl_List(record, None, (_, exchange) -> (
50
+ Exchange(_, answer) = exchange
51
+ Some(answer)
52
+ ))
53
+
54
+ def step(refund: Refund, record: List[Exchange]) -> Next:
55
+ Refund(_, amount, reason) = refund
56
+ match cmp_Int(amount, small_refund_cents):
57
+ case LT: Ruled(Approved)
58
+ case _:
59
+ match last_answer(record):
60
+ case Some(Judged(True)): Ruled(Approved)
61
+ case Some(Judged(False)): Ruled(Denied)
62
+ case _:
63
+ # No answer yet, or the judge failed: ask again until the limit.
64
+ match cmp_Int(length_List(record), most_requests):
65
+ case LT: Perform(AskJudge(reason))
66
+ case _: Ruled(Escalated)
67
+
68
+ def same_request(left: Request, right: Request) -> Bool:
69
+ match (left, right):
70
+ case (AskJudge(l), AskJudge(r)): eq_String(l, r)
71
+
72
+ # Two requests for one order ask for the same refund.
73
+ def same_refund(left: Refund, right: Refund) -> Bool:
74
+ Refund(lo, la, lr) = left
75
+ Refund(ro, ra, rr) = right
76
+ if eq_String(lo, ro):
77
+ if eq_Int(la, ra): eq_String(lr, rr)
78
+ else: False
79
+ else: False
80
+
81
+ # A record replays when each exchange answers the request `step` made at
82
+ # that point. The result is the step the record leads to; None means the
83
+ # record is not one this program makes.
84
+ def replay(refund: Refund, record: List[Exchange]) -> Option[Next]:
85
+ def answered(prefix: Option[List[Exchange]], exchange: Exchange) -> Option[List[Exchange]]:
86
+ match prefix:
87
+ case None: None
88
+ case Some(made):
89
+ Exchange(asked, _) = exchange
90
+ match step(refund, made):
91
+ case Perform(expected):
92
+ if same_request(expected, asked): Some([*made, exchange])
93
+ else: None
94
+ case Ruled(_): None
95
+ match foldl_List(record, Some([]), answered):
96
+ case Some(made): Some(step(refund, made))
97
+ case None: None
@@ -0,0 +1,75 @@
1
+ package Demo/Portable/RefundTest
2
+
3
+ # Native Bosatsu tests for the refund example: the laws of `step` and
4
+ # `replay`, and the boundary that turns the model's text into an answer.
5
+ # They evaluate (PortableRefundExampleTest.scala), so they pin behaviour, not just
6
+ # types.
7
+
8
+ # Test, TestSuite and Assertion come from the implicit Predef import.
9
+ from Bosatsu/Predef import String, True, False, Bool, eq_String
10
+ from Yichus/OpenRouter import ModelResult, Completed, TimedOut
11
+ from Demo/Portable/RefundPolicy import (
12
+ Refund, AskJudge, Judged, JudgeFailed, Exchange, Approved, Denied, Escalated, Ruled, Perform, step, replay
13
+ )
14
+ from Demo/Portable/RefundDesk import answer_of
15
+
16
+ export (tests)
17
+
18
+ small = Refund("order-1", 1500, "it arrived broken")
19
+ large = Refund("order-2", 9000, "it never arrived")
20
+ asked = AskJudge("it never arrived")
21
+ failed = Exchange(asked, JudgeFailed("the call failed: busy"))
22
+
23
+ def completed(content: String) -> ModelResult: Completed("id", "openai/gpt-4.1-mini", "openai", content, 10, 5)
24
+
25
+ def is_judged(result: ModelResult, covered: Bool, said: String) -> Test:
26
+ match answer_of(result):
27
+ case Judged(got):
28
+ match (covered, got):
29
+ case (True, True) | (False, False): Assertion(True, said)
30
+ case _: Assertion(False, said)
31
+ case JudgeFailed(_): Assertion(False, said)
32
+
33
+ def is_failed(result: ModelResult, said: String) -> Test:
34
+ match answer_of(result):
35
+ case JudgeFailed(_): Assertion(True, said)
36
+ case Judged(_): Assertion(False, said)
37
+
38
+ tests = TestSuite("Refund example", [
39
+ match step(small, []):
40
+ case Ruled(Approved): Assertion(True, "a small refund is approved without asking")
41
+ case _: Assertion(False, "a small refund is approved without asking"),
42
+ match step(large, []):
43
+ case Perform(AskJudge(reason)): Assertion(eq_String(reason, "it never arrived"), "a large refund asks the judge about its reason")
44
+ case _: Assertion(False, "a large refund asks the judge about its reason"),
45
+ match step(Refund("order-3", 2000, "it never arrived"), []):
46
+ case Perform(_): Assertion(True, "a refund of exactly the limit asks the judge")
47
+ case _: Assertion(False, "a refund of exactly the limit asks the judge"),
48
+ match step(large, [Exchange(asked, Judged(True))]):
49
+ case Ruled(Approved): Assertion(True, "a covered reason is approved")
50
+ case _: Assertion(False, "a covered reason is approved"),
51
+ match step(large, [Exchange(asked, Judged(False))]):
52
+ case Ruled(Denied): Assertion(True, "a reason not covered is denied")
53
+ case _: Assertion(False, "a reason not covered is denied"),
54
+ match step(large, [failed, failed]):
55
+ case Perform(_): Assertion(True, "a failed judge is asked again")
56
+ case _: Assertion(False, "a failed judge is asked again"),
57
+ match step(large, [failed, failed, failed]):
58
+ case Ruled(Escalated): Assertion(True, "after three failures a person decides")
59
+ case _: Assertion(False, "after three failures a person decides"),
60
+ match replay(large, [Exchange(AskJudge("something else"), Judged(True))]):
61
+ case None: Assertion(True, "a record answering a request the policy did not make does not replay")
62
+ case Some(_): Assertion(False, "a record answering a request the policy did not make does not replay"),
63
+ match replay(small, [Exchange(asked, Judged(True))]):
64
+ case None: Assertion(True, "a record past the decision does not replay")
65
+ case Some(_): Assertion(False, "a record past the decision does not replay"),
66
+ match replay(large, [failed, Exchange(asked, Judged(False))]):
67
+ case Some(Ruled(Denied)): Assertion(True, "a stored record replays to its decision")
68
+ case _: Assertion(False, "a stored record replays to its decision"),
69
+ is_judged(completed("{ \"covered\" : true }"), True, "any valid JSON spelling of a judgment is read"),
70
+ is_judged(completed("{\"covered\":false}"), False, "a judgment that the reason is not covered is read"),
71
+ is_failed(completed("{\"covered\":\"yes\"}"), "a judgment of the wrong type is a failure"),
72
+ is_failed(completed("covered"), "an answer that is not JSON is a failure"),
73
+ is_failed(completed("{\"covered\":true,\"covered\":false}"), "an answer that says covered twice is a failure"),
74
+ is_failed(TimedOut(20000), "a call that timed out is a failure"),
75
+ ])
@@ -0,0 +1,107 @@
1
+ # Guiding the person through the studio
2
+
3
+ The person does not read documentation. The yich.us studio page only tells them what the studio is and how to connect you. Everything they need to know while building comes from you, a little at a time, when it becomes relevant. This file is what you need to know to do that. Do not read it out. Bring in one piece when the conversation reaches it, in their words, and point at the part of the page you mean with `yichus_point`.
4
+
5
+ `yichus_page` shows you what the page shows them right now, and what they have on screen and last clicked.
6
+
7
+ ## When you give them the link
8
+
9
+ Tell them three things, briefly:
10
+
11
+ - The page runs on their computer only. Nobody else can open it, and it keeps the same address when you restart the studio.
12
+ - It updates by itself after every check, so they can leave it open in a tab.
13
+ - They can ask you about anything on it.
14
+
15
+ Do not explain the sections until something appears in them.
16
+
17
+ ## The page, top to bottom
18
+
19
+ - What you asked for: their description, exactly as recorded.
20
+ - What is happening now: the step you are on, in one sentence.
21
+ - Questions for you: shown while a question is open. Saving an answer there and answering you in the chat do the same thing.
22
+ - What your program must do: each choice they made, with its status.
23
+ - What happens when: each thing the app does, followed to every way it can end.
24
+ - How your app is built: the parts of the app, stacked on the parts they are built from.
25
+ - The shape of your program: a picture of the whole program as it is now.
26
+
27
+ ## The steps you go through
28
+
29
+ The studio moves you through these steps in order, after every change. Tell the person which one you are on when it changes, in one sentence. The page's "What is happening now" says the same.
30
+
31
+ 1. Describe: their description is recorded word for word.
32
+ 2. Where it runs: Render, Vercel, Netlify, Amazon Web Services, Google Cloud, Microsoft Azure, or not sure yet. You build the pieces that place can run, so nothing has to be rebuilt later.
33
+ 3. Write the first version.
34
+ 4. Make it run: fix the mistakes that stop it from running.
35
+ 5. Name the parts in their words, so the questions and the pictures use their words instead of the code's.
36
+ 6. Ask them the questions the studio found.
37
+ 7. What happens when: for each thing the app does, write down in their words every way it can go and what they should get at the end. The studio checks each one.
38
+ 8. Match their answers: write the answers into the program as rules the checks hold it to.
39
+ 9. Fix whatever does not hold yet.
40
+ 10. Test failures: for answers about crashes and outside services, you write a failure scenario and the studio runs it.
41
+ 11. Done: everything they chose holds.
42
+
43
+ When a check fails, the code changes, never the rule. If they ask why something is taking a while, this is usually the answer.
44
+
45
+ ## The questions
46
+
47
+ Questions come from what the program does, never from what things are called. A new feature can bring new questions. When you ask one, say why it matters, using the question's own "why", and give every choice.
48
+
49
+ | Asked when | The question | The choices |
50
+ | --- | --- | --- |
51
+ | The app answers requests | One server, or several copies sharing one database? | Several copies (safer, the default); one server (shown as an assumption on every result) |
52
+ | The app saves a kind of record | Who may see one of these records, and who may change it? | Only its creator; the creator and people they share with; everyone reads, certain roles change; anyone |
53
+ | An action reads a record and then changes it | Two people change the same record at the same moment. What should happen? | Both changes are kept; the second is refused and they try again |
54
+ | An action saves in more than one step | If the server stops halfway, may part of the change stay saved? | All or nothing; partial is fine |
55
+ | An action calls an outside service or an AI model | If that service is down or slow, what should the person see? | An error with nothing saved; save the request and finish it later; no preference |
56
+
57
+ They can answer in the chat or on the page; both are the same. The page keeps their words next to the choice, and a later answer replaces an earlier one. The answers live in their project folder, so a new session starts from the same decisions. Tell them that once, the first time they answer.
58
+
59
+ ## What each status means
60
+
61
+ Each choice they made shows on the page under "What your program must do", with one of these statuses. When a status changes, say what it means if they have not seen it before.
62
+
63
+ | On the page | Meaning |
64
+ | --- | --- |
65
+ | waiting for your answer | A question they have not answered yet. Ask it. |
66
+ | proven | Checked against every path through the program. No input, user or timing can break it. |
67
+ | holds in every scenario checked | The failure scenario ran and stopped or failed the app at every step it covers. Strong, but limited to that scenario. |
68
+ | holds, under an assumption | Proven only while the assumption is true, usually that exactly one server runs. A second copy can break it. |
69
+ | decided | A choice that sets up the other checks, such as several copies of the server. |
70
+ | broken | The check found a way it fails. You have the evidence; say in their terms what goes wrong, then fix it. |
71
+ | the code says otherwise | The rule in the program does not match their answer yet. You update it. |
72
+ | scenario being written | You are writing the failure scenario this answer needs. |
73
+ | not decided yet | The check ran but could not decide. You find out why. |
74
+ | no check needed | They chose an option that adds no check, such as "partial is fine". |
75
+ | answered, waiting for your agent to check | They answered on the page; the result appears after your next check. |
76
+
77
+ The evidence for a status comes to you, never to the page. When they ask why, explain it in product terms.
78
+
79
+ ## Reading the pictures with them
80
+
81
+ Everything in a box marked "Yichus output" comes straight from the checks. The pictures are drawn by the checks too, but their words in italics are the names and descriptions you chose, and nothing checks those words. Never let a picture's italic words or your notes stand in for a check.
82
+
83
+ "What happens when" follows each thing the app does to every way it can end.
84
+
85
+ - Each question the app asks itself, such as "are you signed in?", splits into the ways it can go. A way that ends at once sits just under its question. The way that leads on furthest continues straight down after an arrow. Any other way that leads on is folded until they open it.
86
+ - Each way ends in a mark: a bar when the request is turned away, a ring with a dot when it is answered.
87
+ - Under the mark are the things they said should be true there. A tick means the check finds it holds, a cross means it does not, and a question mark means the check cannot say.
88
+ - A red ending is one where something they expect is false, or where someone could reach another person's records. Green means an ending that was broken is now fixed. A dashed outline means the check cannot say.
89
+ - "Not described yet" marks a part you have not put into words. It is still checked.
90
+
91
+ "How your app is built" stacks the parts of the app. Each box is one part, in their words, with the records it changes or only looks at. Parts in a higher band are built from parts in the bands below, and requests come in at the top. A wider box is one more of the app is built on.
92
+
93
+ "The shape of your program" is the system map. Requests come in on the left, and the records and outside services sit on the right. Tapping a part shows it on its own. A label that looks like code is a part you have not named yet. For a person who wants to learn the marks, https://yich.us/system-map-blocks.html explains each one, and https://yich.us/lessons/index.html has short lessons with practice.
94
+
95
+ ## Your notes and what you can see
96
+
97
+ Your `yichus_point` notes appear as bubbles marked "Your agent · not checked", each beside the part it is about. Tell the person once that the bubbles are you pointing, like a finger on the page, and that the statuses and checks say whether something holds. They can close a bubble with its ×.
98
+
99
+ You can see what the page shows and what they have in view. When they say "this one" or "the red one", call `yichus_page` before you answer. If they ask what you can see, it is only which parts of the studio page are on screen, which folds they opened, and what they last clicked, nothing else from their browser.
100
+
101
+ ## What the studio does not check
102
+
103
+ Say these plainly when they come up, and before the person treats "done" as a guarantee:
104
+
105
+ - Whether they chose well. The studio checks that the program does what they chose, not that the choice suits their users.
106
+ - Questions it does not ask yet: payments, email, file uploads and scheduled jobs.
107
+ - Mathematical proofs and whole-system model checking. A failure scenario covers only the steps it names.
@@ -0,0 +1,159 @@
1
+ # Worker authority in generated APIs
2
+
3
+ A worker is trusted deployment code, but its network caller still needs an
4
+ identity. `worker_route` declares that identity separately from a user's roles.
5
+ The declaration participates in the same typed route, permission and access
6
+ analysis as other service routes:
7
+
8
+ ```bosatsu
9
+ worker_route("/complete", handler("complete", complete),
10
+ "judgment", ["job-manager"], [WritePerm("results")])
11
+ ```
12
+
13
+ Import `worker_route`, `handler` and `WritePerm` from `Yichus/Service`.
14
+ The named handler takes the usual `Db`, injected `Principal` and typed inputs,
15
+ and returns `IO`. The name and role list must be statically readable, nonblank
16
+ literals or aliases; roles are nonempty and distinct. A matching
17
+ `GuardedOrRoles` or `PublicReadRoleWrite` resource policy still governs the
18
+ handler's effects. Shared `Table` and `TxTable` helper parameters retain the
19
+ calling route's exact resource and authority; a dynamic table or an ordinary
20
+ unprivileged caller does not inherit another route's grants.
21
+
22
+ The name `judgment` is a logical deployment binding. It is not a credential,
23
+ a client-supplied field or a session claim. A user session with `job-manager`
24
+ cannot call this route. The server adapter maps a verified machine subject to
25
+ the declared logical name and deployment-owned role grants. Worker credentials
26
+ can call only worker routes for that name; they cannot impersonate users on
27
+ ordinary routes. Worker principals have an opaque namespaced identity, derived
28
+ from the logical name and verified subject; application code must not parse it.
29
+
30
+ ## Secret bootstrap
31
+
32
+ A deployment without a machine identity provider can give each worker a long
33
+ random secret instead. The worker sends it as its Bearer token, and the API
34
+ holds the same secrets in its environment. The generated engine exports the
35
+ verifier:
36
+
37
+ ```js
38
+ import {
39
+ attachYichusWorkerVerifier, yichusSecretWorkerVerifier, startYichusApiEngine,
40
+ } from './engine.mjs';
41
+
42
+ attachYichusWorkerVerifier(yichusSecretWorkerVerifier([
43
+ { name: 'ingest', secret: process.env.INGEST_WORKER_SECRET, roles: ['ingest'] },
44
+ ]));
45
+ await startYichusApiEngine(8080);
46
+ ```
47
+
48
+ Each secret is at least 32 characters, and no two workers share one. Make it with
49
+ `openssl rand -base64 48`, or with any other source of random bytes. A token is
50
+ compared by its SHA-256 digest against every worker's in full. Rotating a secret
51
+ means restarting the engine with the new one. Unlike Clerk M2M tokens, a secret
52
+ does not expire on its own, so keep it on servers only.
53
+
54
+ This fits a page host such as Vercel that receives a provider's webhook or a
55
+ public form. Its route handler checks the provider's signature, or the form's
56
+ bot check, and then calls the engine's worker route with the secret.
57
+
58
+ ## Clerk bootstrap
59
+
60
+ The workspace package `@yichus/clerk-worker-auth` verifies **opaque Clerk M2M
61
+ tokens** online. It does not accept session JWTs or M2M JWTs. Configure a
62
+ receiving machine for the API and a sending machine for the judgment worker;
63
+ the token's scopes must include the receiving machine. Keep the receiving
64
+ machine's secret on the API server and the sending credential on the worker.
65
+ Grant creation and token issuance are deployment setup, not frontend actions.
66
+
67
+ With Node 20 or newer, first set the listener's authentication environment
68
+ using the [session configuration](hosted-sessions.md), then attach the adapter
69
+ before starting the generated engine:
70
+
71
+ ```js
72
+ import {
73
+ attachYichusWorkerVerifier, attachYichusStore, startYichusApiEngine,
74
+ } from './engine.mjs';
75
+ import { createClerkWorkerVerifier } from '@yichus/clerk-worker-auth';
76
+
77
+ attachYichusWorkerVerifier(createClerkWorkerVerifier({
78
+ machineSecretKey: process.env.CLERK_API_MACHINE_SECRET,
79
+ targetMachine: process.env.CLERK_API_MACHINE_ID,
80
+ workers: [{
81
+ name: 'judgment',
82
+ subject: process.env.CLERK_JUDGMENT_MACHINE_ID,
83
+ roles: ['job-manager'],
84
+ }],
85
+ }));
86
+ await attachYichusStore('memory'); // Local example; configure the production store separately.
87
+ await startYichusApiEngine(8080);
88
+ ```
89
+
90
+ These are illustrative environment variable names. No Clerk resource or
91
+ secret is provisioned by this example. The package is currently a workspace
92
+ package; deployment packaging must include it, rather than assuming a registry
93
+ release. The listener always requires a supported `YICHUS_AUTH` mode, even when all
94
+ routes are worker routes; the worker verifier supplements that configuration.
95
+ The engine refuses to start with worker routes but no worker verifier.
96
+
97
+ A worker sends its opaque token as `Authorization: Bearer <token>` on the
98
+ worker route and supplies an `Idempotency-Key` for a mutation it may retry.
99
+ Body and result JSON come from the generated OpenAPI operation. Its
100
+ `x-yichus-worker` extension identifies the required worker; the operation
101
+ description lists every required grant.
102
+ The user-authenticated `/mcp` tool list excludes worker operations. This change
103
+ does not introduce machine authentication for that MCP endpoint. Generated
104
+ user frontends refuse worker routes as views rather than suggesting that a
105
+ user session could supply their authority; ordinary user views can coexist
106
+ with separately authenticated worker routes in the same API.
107
+
108
+ The in-process counterpart is
109
+ `yichusApiEngine.handleWithWorkerToken(path, token, args, { idempotencyKey })`.
110
+ Direct `handle`, raw role arrays and forged proof objects cannot obtain worker
111
+ authority. Installing a verifier is a trusted host capability: client code
112
+ running its own local engine cannot confer authority on the deployed API.
113
+ Never ship the Clerk adapter configured with a secret into a browser.
114
+
115
+ ## Refusals, retries and trust limits
116
+
117
+ Each request, including an idempotent replay, calls Clerk's
118
+ `POST /v1/m2m_tokens/verify` with the receiving machine's secret. The adapter
119
+ checks the provider's subject, target scopes, revocation and expiry. Custom
120
+ claims never select a worker name or role. The Clerk adapter copies configured grants at construction; attaching it stores
121
+ the verifier reference. Replacing the verifier invalidates earlier and pending
122
+ authority proofs.
123
+ The runtime preserves its private proof through async dispatch and rechecks
124
+ route authority before reading an idempotency record and before executing.
125
+ A restart during authentication refuses the in-flight request.
126
+
127
+ Invalid credentials return 401; an unconfigured sender, wrong target or
128
+ insufficient route grant returns 403. Provider outages, malformed provider
129
+ responses and verifier failures return 503 without returning credentials or
130
+ provider error bodies. Verification defaults to a five-second timeout and
131
+ has no successful-authentication cache. An outage therefore prevents even a
132
+ previously completed request from being replayed through this entry.
133
+
134
+ Revocation is checked at request authentication, not continuously throughout
135
+ an already executing request. A worker is trusted to implement its program;
136
+ its credential does not prove that its result is correct. The handler must
137
+ still enforce leases, fences, current revisions, payload validation and atomic
138
+ state transitions. Static access verification covers declared service entries;
139
+ it does not certify every exported function a custom host could invoke.
140
+
141
+ ## Executed evidence and Icetakes integration
142
+
143
+ `ApiWorkerAuthorityTest` compiles the real Bosatsu fixture, reads its authority
144
+ through API MCP, and exercises the generated HTTP and in-process engine with
145
+ the actual adapter and a local Clerk stand-in. It covers forged/user authority,
146
+ wrong sender/target, revoked retry, outage, restart and role loss before replay.
147
+ Runtime tests also cover proof replacement and malformed verifier results;
148
+ adapter tests cover timeout, provider failures and ignored custom grants.
149
+ The CLI proves all nine API properties for the fixture; negative variants
150
+ reject dynamic tables and an unprivileged caller to its shared helper.
151
+
152
+ These are local fixtures, not a live Clerk deployment. Icetakes still needs
153
+ its worker entry wired and audited against the actual stage protocol, live
154
+ machine provisioning, token rotation and outage/retry checks. Keep its combined
155
+ provider-dispatch path until that conversion is validated. The whole-product
156
+ G34 deployment verification work remains separate.
157
+
158
+ Provider contracts: [Clerk M2M verification](https://clerk.com/docs/reference/backend/m2m-tokens/verify)
159
+ and [M2M tokens](https://clerk.com/docs/guides/development/machine-auth/m2m-tokens).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "yichus",
3
- "version": "0.2.0",
3
+ "version": "0.3.0",
4
4
  "description": "Local Bosatsu compilation, Yichus analysis, and generated apps using the WebMCP engine",
5
5
  "license": "MIT",
6
6
  "homepage": "https://yich.us",
@@ -20,7 +20,7 @@
20
20
  "dependencies": {
21
21
  "zod": "^4.3.6",
22
22
  "@modelcontextprotocol/sdk": "^1.26.0",
23
- "@yichus/api-mcp": "0.1.1"
23
+ "@yichus/api-mcp": "0.1.2"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@types/node": "^22.0.0",