@tideorg/mcp 1.4.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.
Files changed (76) hide show
  1. package/GAP_REGISTER.md +117 -0
  2. package/README.md +38 -0
  3. package/adapters/AGENTS.md +241 -0
  4. package/adapters/CLAUDE.md +146 -0
  5. package/adapters/replit.md +224 -0
  6. package/canon/anti-patterns.md +2133 -0
  7. package/canon/concepts.md +816 -0
  8. package/canon/custom-contracts.md +533 -0
  9. package/canon/delegation.md +195 -0
  10. package/canon/feature-mapping.md +637 -0
  11. package/canon/framework-matrix.md +1125 -0
  12. package/canon/invariants.md +851 -0
  13. package/canon/redirect-handler.md +254 -0
  14. package/canon/tidecloak-bootstrap.md +287 -0
  15. package/canon/tidecloak-endpoints.md +294 -0
  16. package/canon/troubleshooting.md +1494 -0
  17. package/canon/version-policy.md +89 -0
  18. package/mcp-server/dist/index.d.ts +2 -0
  19. package/mcp-server/dist/index.js +605 -0
  20. package/package.json +45 -0
  21. package/playbooks/add-auth-nextjs-existing.md +799 -0
  22. package/playbooks/add-auth-nextjs-fresh.md +654 -0
  23. package/playbooks/add-rbac-nextjs.md +190 -0
  24. package/playbooks/bootstrap-realm-from-template.md +170 -0
  25. package/playbooks/configure-e2ee-roles-and-policies.md +196 -0
  26. package/playbooks/deploy-tidecloak-docker.md +792 -0
  27. package/playbooks/diagnose-broken-login.md +234 -0
  28. package/playbooks/diagnose-missing-roles-or-claims.md +253 -0
  29. package/playbooks/initialize-admin-and-link-account.md +235 -0
  30. package/playbooks/migrate-from-existing-auth.md +198 -0
  31. package/playbooks/protect-api-nextjs.md +451 -0
  32. package/playbooks/protect-routes-nextjs.md +544 -0
  33. package/playbooks/setup-forseti-e2ee.md +756 -0
  34. package/playbooks/setup-iga-admin-panel.md +344 -0
  35. package/playbooks/setup-server-delegation.md +142 -0
  36. package/playbooks/start-tidecloak-dev.md +130 -0
  37. package/playbooks/verify-jwt-server-side.md +439 -0
  38. package/prompts/add-admin-approval-flow.md +50 -0
  39. package/prompts/build-private-customer-portal.md +85 -0
  40. package/prompts/migrate-generic-auth-to-tide.md +67 -0
  41. package/prompts/secure-existing-app.md +80 -0
  42. package/reference-apps/INDEX.md +87 -0
  43. package/reference-apps/encrypted-communication/anti-patterns.md +123 -0
  44. package/reference-apps/encrypted-communication/bootstrap-sequence.md +152 -0
  45. package/reference-apps/encrypted-communication/manifest.yaml +100 -0
  46. package/reference-apps/encrypted-communication/role-policy-matrix.md +80 -0
  47. package/reference-apps/encrypted-communication/scenario.md +134 -0
  48. package/reference-apps/git-pr-signing-service/anti-patterns.md +61 -0
  49. package/reference-apps/git-pr-signing-service/bootstrap-sequence.md +157 -0
  50. package/reference-apps/git-pr-signing-service/manifest.yaml +80 -0
  51. package/reference-apps/git-pr-signing-service/role-policy-matrix.md +99 -0
  52. package/reference-apps/git-pr-signing-service/scenario.md +169 -0
  53. package/reference-apps/iga-admin-governance/anti-patterns.md +133 -0
  54. package/reference-apps/iga-admin-governance/bootstrap-sequence.md +153 -0
  55. package/reference-apps/iga-admin-governance/manifest.yaml +87 -0
  56. package/reference-apps/iga-admin-governance/role-policy-matrix.md +67 -0
  57. package/reference-apps/iga-admin-governance/scenario.md +126 -0
  58. package/reference-apps/organisation-password-manager/anti-patterns.md +71 -0
  59. package/reference-apps/organisation-password-manager/bootstrap-sequence.md +127 -0
  60. package/reference-apps/organisation-password-manager/manifest.yaml +86 -0
  61. package/reference-apps/organisation-password-manager/role-policy-matrix.md +59 -0
  62. package/reference-apps/organisation-password-manager/scenario.md +107 -0
  63. package/reference-apps/policy-governed-signing/anti-patterns.md +67 -0
  64. package/reference-apps/policy-governed-signing/bootstrap-sequence.md +128 -0
  65. package/reference-apps/policy-governed-signing/manifest.yaml +78 -0
  66. package/reference-apps/policy-governed-signing/role-policy-matrix.md +62 -0
  67. package/reference-apps/policy-governed-signing/scenario.md +113 -0
  68. package/skills/tide-diagnostics/SKILL.md +99 -0
  69. package/skills/tide-integration/SKILL.md +136 -0
  70. package/skills/tide-learning-capture/SKILL.md +174 -0
  71. package/skills/tide-rbac-and-e2ee/SKILL.md +214 -0
  72. package/skills/tide-reviewer/SKILL.md +133 -0
  73. package/skills/tide-route-and-api-protection/SKILL.md +201 -0
  74. package/skills/tide-scenario-resolver/SKILL.md +81 -0
  75. package/skills/tide-setup/SKILL.md +218 -0
  76. package/skills/tide-solutions-architect/SKILL.md +130 -0
@@ -0,0 +1,533 @@
1
+ # Custom Forseti Contracts - Canon
2
+
3
+ How to write, deploy, and use custom Forseti smart contracts for access control.
4
+
5
+ Forseti contracts are C# code that runs in a sandboxed VM on every ORK node. They control who can sign, encrypt, decrypt, or approve operations. No single ORK can bypass a contract - majority must agree.
6
+
7
+ ---
8
+
9
+ ## What You Can Build
10
+
11
+ A contract is arbitrary C# logic that every ORK evaluates independently before allowing a cryptographic operation. A majority of ORKs must agree. No single server, admin, or compromised node can bypass it.
12
+
13
+ This means you can encode any business rule directly into the cryptographic layer:
14
+
15
+ **Agent guidance**: When a user describes what their app needs to protect, control, or enforce - think in contracts. Any business rule that should be tamper-proof can become a contract. The user does not need to know they need a contract. They say "only managers should approve expenses over $5K" or "patients should control who sees their records" or "we need three people to sign off on a deployment." Your job is to recognize that these are contract problems and write the C# that enforces them.
16
+
17
+ Be creative. The contract API gives you roles, identity, time, geo, payload data, multi-party approval, and separation of duties. Combine them. A user who says "build me a password manager" gets self-encryption. A user who says "build me a password manager with recovery" gets a contract that requires 3-of-5 recovery contacts. A user who says "sign transactions" gets a contract that checks amounts against limits and requires more approvers for larger values. A user who says "encrypt medical records" gets a contract that checks the accessor's role, the time of day, and logs every access attempt.
18
+
19
+ The contract runs on every ORK independently. No server, admin, or single point of compromise can bypass it. This is the differentiator. When the user's requirement is "nobody should be able to cheat this rule" - that is a contract.
20
+
21
+ Do not wait for the user to ask for a Forseti contract. Recognize when their requirements imply one and propose it.
22
+
23
+ You can write any C# logic that fits within the sandbox constraints. There are no pre-built templates you must follow. The SDK gives you building blocks - role checks, time checks, doken inspection, payload parsing, approval counting - but the contract itself is freeform C# code. If the user needs a rule that does not map to any existing pattern, write new logic. Parse the payload bytes. Compare values. Count things. Check conditions. Combine checks in ways nobody has combined them before. The contract is your code, meeting the user's specific need, enforced by the network. Write exactly what the situation requires.
24
+
25
+ ---
26
+
27
+ ## When You Need a Custom Contract
28
+
29
+ Use a built-in contract for simple role-gated multi-approval. Write a custom contract when you need:
30
+
31
+ - Executor validation (who can perform the action)
32
+ - Data validation (what data is being signed/encrypted)
33
+ - Time-based restrictions (business hours, weekdays)
34
+ - Complex approval logic (minimum from different orgs, no self-approval)
35
+ - Direction-specific logic (different rules for encrypt vs decrypt)
36
+ - Amount/threshold checks on payload data
37
+ - Separation of duties enforcement
38
+ - Multi-jurisdictional approval requirements
39
+
40
+ ### Built-in Contracts
41
+
42
+ | Contract ID | Checks | Parameters |
43
+ |-------------|--------|------------|
44
+ | `GenericResourceAccessThresholdRole:1` | Approvers have a **client role** | `role`, `resource`, `threshold` |
45
+ | `GenericRealmAccessThresholdRole:1` | Approvers have a **realm role** | `role`, `resource`, `threshold` |
46
+ | `SimpleTagBasedDecryption:1` | Executor has `_tide_{tag}.encrypt`/`.decrypt` role | tag from data |
47
+
48
+ Built-in contracts only validate approvers (or tags). They do NOT validate the executor or the data. For anything beyond "N people with role X must approve", write a custom contract.
49
+
50
+ ---
51
+
52
+ ## Contract Template
53
+
54
+ Every contract must:
55
+ - Use namespace `Ork.Forseti.Sdk`
56
+ - Have a class named `Contract`
57
+ - Implement `IAccessPolicy`
58
+
59
+ ```csharp
60
+ using Ork.Forseti.Sdk;
61
+
62
+ public class Contract : IAccessPolicy
63
+ {
64
+ // Parameters bound automatically from policy config
65
+ [PolicyParam(Required = true, Description = "Role required to execute")]
66
+ public string Role { get; set; }
67
+
68
+ [PolicyParam(Required = true, Description = "Resource/client to check role against")]
69
+ public string Resource { get; set; }
70
+
71
+ // STEP 1: Validate request data (always runs)
72
+ public PolicyDecision ValidateData(DataContext ctx)
73
+ {
74
+ return PolicyDecision.Allow();
75
+ }
76
+
77
+ // STEP 2: Validate approvers (runs if ApprovalType.EXPLICIT)
78
+ public PolicyDecision ValidateApprovers(ApproversContext ctx)
79
+ {
80
+ return PolicyDecision.Allow();
81
+ }
82
+
83
+ // STEP 3: Validate executor (runs if ExecutionType.PRIVATE)
84
+ public PolicyDecision ValidateExecutor(ExecutorContext ctx)
85
+ {
86
+ var executor = new DokenDto(ctx.Doken);
87
+
88
+ return Decision
89
+ .RequireNotExpired(executor)
90
+ .RequireRole(executor, Resource, Role);
91
+ }
92
+ }
93
+ ```
94
+
95
+ ---
96
+
97
+ ## Three Validation Methods
98
+
99
+ | Method | When it runs | Context | Use for |
100
+ |--------|-------------|---------|---------|
101
+ | `ValidateData(DataContext ctx)` | Always | `ctx.Data`, `ctx.DynamicData`, `ctx.RequestId` | Time checks, data format validation, direction detection |
102
+ | `ValidateApprovers(ApproversContext ctx)` | When `ApprovalType.EXPLICIT` | `ctx.Dokens` (list of approver tokens) | Quorum checks, role-based approval, org diversity |
103
+ | `ValidateExecutor(ExecutorContext ctx)` | When `ExecutionType.PRIVATE` | `ctx.Doken` (executor's token) | Role checks, identity checks, expiry |
104
+
105
+ Each returns `PolicyDecision.Allow()` or `PolicyDecision.Deny("reason")`.
106
+
107
+ ### Direction Detection (Encrypt vs Decrypt)
108
+
109
+ For encryption contracts, detect the direction via `ctx.RequestId`:
110
+
111
+ ```csharp
112
+ public PolicyDecision ValidateData(DataContext ctx)
113
+ {
114
+ if (ctx.RequestId == "PolicyEnabledEncryption:1")
115
+ {
116
+ // Encrypt-specific validation
117
+ return PolicyDecision.Allow();
118
+ }
119
+ if (ctx.RequestId == "PolicyEnabledDecryption:1")
120
+ {
121
+ // Decrypt-specific validation
122
+ return Decision.RequireWeekday();
123
+ }
124
+ return PolicyDecision.Deny("Unknown request type");
125
+ }
126
+ ```
127
+
128
+ ---
129
+
130
+ ## Decision Builder
131
+
132
+ Chain checks with `Decision.`. First failure stops the chain and returns the deny reason.
133
+
134
+ ### Role Checks
135
+
136
+ ```csharp
137
+ Decision
138
+ .RequireRole(doken, "resource", "role") // client role
139
+ .RequireRole(doken, "admin") // realm role (2-arg)
140
+ .RequireAnyRole(doken, "resource", "admin", "mod") // at least one
141
+ .RequireAllRoles(doken, "resource", "read", "write") // must have all
142
+ .ForbidRole(doken, "resource", "blocked") // must NOT have
143
+ ```
144
+
145
+ ### Approval Checks
146
+
147
+ ```csharp
148
+ Decision
149
+ .RequireMinWithRole(approvers, 2, "resource", "approver") // N approvers with role
150
+ .RequireAnyWithRole(approvers, "resource", "approver") // at least one
151
+ .ForbidSelfApproval(requestorId, approvers) // no self-approval
152
+ .RequireDistinctOrgs(approvers, 2) // from N different orgs
153
+ ```
154
+
155
+ ### Time Checks
156
+
157
+ ```csharp
158
+ Decision
159
+ .RequireWeekday() // Mon-Fri
160
+ .RequireBusinessHours() // Mon-Fri 9-17 UTC
161
+ .RequireHourBetween(9, 17) // custom hours
162
+ .ForbidHourBetween(0, 6) // block hours
163
+ .RequireDayOfWeek(DayOfWeek.Monday) // specific day
164
+ ```
165
+
166
+ ### Token Checks
167
+
168
+ ```csharp
169
+ Decision
170
+ .RequireNotExpired(doken)
171
+ .RequireFromAudience(doken, "my-realm")
172
+ .RequireUserId(doken, "user-123")
173
+ ```
174
+
175
+ ### Geo Checks
176
+
177
+ ```csharp
178
+ var country = ForsetiSdk.Claim("country") as string;
179
+ Decision
180
+ .RequireCountry(country, "US", "CA", "AU")
181
+ .ForbidCountry(country, "XX", "YY")
182
+ ```
183
+
184
+ ### Generic
185
+
186
+ ```csharp
187
+ Decision
188
+ .Require(amount <= maxAmount, $"Amount {amount} exceeds limit {maxAmount}")
189
+ .Forbid(isBlacklisted, "User is blacklisted")
190
+ ```
191
+
192
+ ---
193
+
194
+ ## DokenDto - Token Wrapper
195
+
196
+ Wrap doken bytes to access user identity, roles, and expiry.
197
+
198
+ ```csharp
199
+ // Single executor
200
+ var executor = new DokenDto(ctx.Doken);
201
+
202
+ // List of approvers
203
+ var approvers = DokenDto.WrapAll(ctx.Dokens);
204
+
205
+ // Properties
206
+ executor.UserId // user ID string
207
+ executor.Audience // realm/org
208
+ executor.Expiry // unix timestamp
209
+ executor.IsExpired // bool
210
+ executor.IsNull // bool
211
+ executor.HasRole("resource", "admin") // client role
212
+ executor.HasAnyRole("resource", "admin", "mod")
213
+ ```
214
+
215
+ ---
216
+
217
+ ## [PolicyParam] - Declare Parameters
218
+
219
+ Parameters are bound automatically from the policy configuration. Declare them as properties with `[PolicyParam]`.
220
+
221
+ ```csharp
222
+ [PolicyParam(Required = true, Description = "Role name")]
223
+ public string Role { get; set; }
224
+
225
+ [PolicyParam(Default = 2, Min = 1, Max = 10)]
226
+ public int MinApprovers { get; set; }
227
+
228
+ [PolicyParam(AllowedValues = new[] { "low", "medium", "high" })]
229
+ public string Priority { get; set; } = "medium";
230
+ ```
231
+
232
+ | Option | Description |
233
+ |--------|-------------|
234
+ | `Required` | Throws if missing (default: false) |
235
+ | `Default` | Default value if not provided |
236
+ | `Min` / `Max` | Numeric range validation |
237
+ | `AllowedValues` | Enum-like string validation |
238
+ | `Description` | Human-readable description |
239
+
240
+ ---
241
+
242
+ ## ForsetiSdk Runtime
243
+
244
+ Available inside contract methods:
245
+
246
+ ```csharp
247
+ ForsetiSdk.Claim("key") // get a claim value (costs 5 gas)
248
+ ForsetiSdk.Log("message") // log output (costs 25 gas)
249
+ ForsetiSdk.GasUsed // current gas consumed
250
+ ForsetiSdk.GasLimit // total gas budget (default 50,000)
251
+ ```
252
+
253
+ Gas limit is 50,000. Exceeding it throws `OutOfGasException` and the operation fails.
254
+
255
+ ---
256
+
257
+ ## Deploying a Contract
258
+
259
+ ### Step 1: Upload Contract to TideCloak (Optional)
260
+
261
+ ```
262
+ PUT /admin/realms/{realm}/tide-admin/forseti-contracts
263
+ Content-Type: application/json
264
+ Authorization: Bearer <admin-token>
265
+
266
+ {
267
+ "contractCode": "<C# source code as string>",
268
+ "name": "MyPolicy"
269
+ }
270
+ ```
271
+
272
+ The ORK compiles the contract in its sandbox and stores it. The contract ID is `SHA512(source code)`.
273
+
274
+ Note: This endpoint may not be available on all TideCloak images. If it returns 404/405, the contract is deployed through the policy signing flow (Step 3) instead — the contract source is included in the contract transport.
275
+
276
+ ### Step 2: Create a Policy Using the Contract
277
+
278
+ On the JavaScript side, use `Models` from `@tidecloak/js` and `BasicCustomRequest` from `asgard-tide`:
279
+
280
+ ```typescript
281
+ import { Models, Tools } from '@tidecloak/js'
282
+ const { Policy, ApprovalType, ExecutionType, BaseTideRequest } = Models
283
+ const { TideMemory } = Tools
284
+
285
+ // Create the policy
286
+ const policy = new Policy({
287
+ contractId: contractHash, // SHA512 of the C# source
288
+ modelIds: ['MyModel:1'],
289
+ approvalType: ApprovalType.EXPLICIT, // or IMPLICIT
290
+ executionType: ExecutionType.PRIVATE, // or PUBLIC
291
+ params: [
292
+ ['Role', 'data-access'],
293
+ ['Resource', 'my-app'],
294
+ ],
295
+ })
296
+ ```
297
+
298
+ Important:
299
+ - Import `Models` from `@tidecloak/js`, NOT from `@tidecloak/nextjs` (returns `undefined`)
300
+ - Import `BasicCustomRequest` from `asgard-tide` for signing requests
301
+ - Import `PolicySignRequest` from `heimdall-tide` for policy deployment (alternative flow)
302
+ - Policy params must be `[key, value]` pairs, not a plain object (AP-54)
303
+
304
+ ### Step 3: Deploy the Policy to the ORK Network
305
+
306
+ Policy deployment requires the realm's **admin policy** to authorize the operation. The admin policy is pre-signed during realm setup (IGA bootstrap) and must be attached to every policy creation request. VERIFIED (LEARNINGS-ratidefy-batch-001 L-22).
307
+
308
+ **Fetch the admin policy** (server-side — endpoint does not support CORS):
309
+ ```typescript
310
+ // Server-side: proxy this to the browser, or pre-download as static file
311
+ const adminPolicyUrl = `${authServerUrl}/realms/${realm}/tide-policy-resources/admin-policy`;
312
+ const adminPolicyB64 = await fetch(adminPolicyUrl).then(r => r.text());
313
+ const adminPolicyBytes = Uint8Array.from(atob(adminPolicyB64), c => c.charCodeAt(0));
314
+ ```
315
+
316
+ **Build and deploy the policy** (browser-side — requires authenticated TideCloak session):
317
+ ```typescript
318
+ const tc = (IAMService as any)._tc; // Internal TideCloak instance
319
+
320
+ // 1. Build the policy bytes and contract transport
321
+ const policyBytes = policy.toBytes();
322
+ const contractSource = `using Ork.Forseti.Sdk;\n\npublic class Contract : IAccessPolicy { ... }`;
323
+ const contractTransport = TideMemory.CreateFromArray([
324
+ new Uint8Array(0), // empty bytes
325
+ TideMemory.CreateFromArray([
326
+ new TextEncoder().encode(contractSource),
327
+ new TextEncoder().encode("Contract"), // entry type
328
+ ]),
329
+ ]);
330
+ const draft = TideMemory.CreateFromArray([policyBytes, contractTransport]);
331
+
332
+ // 2. Build the request with admin policy
333
+ const request = new BaseTideRequest("Policy", "1", "Policy:1", draft);
334
+ request.addAuthorizer(dokenBytes); // Admin's doken
335
+ request.policy = new TideMemory(adminPolicyBytes.length);
336
+ request.policy.set(adminPolicyBytes); // CRITICAL — without this, ORK rejects
337
+
338
+ // 3. Send to ORK network
339
+ const signRequest = await tc.createTideRequest(request.encode());
340
+
341
+ // 4. Operator approval via Tide enclave popup
342
+ const result = await tc.requestTideOperatorApproval([
343
+ { id: "policy-deploy", request: signRequest }
344
+ ]);
345
+
346
+ // 5. Execute — ORK produces VVK signature
347
+ const sigs = await tc.executeSignRequest(result[0].request, true);
348
+ const vvkSignature = sigs[0]; // 64-byte Ed25519
349
+
350
+ // 6. Attach VVK signature to policy and store
351
+ policy.signature = new TideMemory(vvkSignature.length);
352
+ policy.signature.set(vvkSignature);
353
+ const signedPolicyBytes = policy.toBytes(); // Store THESE bytes
354
+ ```
355
+
356
+ **Critical notes**:
357
+ - Use `BaseTideRequest` directly, NOT `PolicySignRequest` from `heimdall-tide` — the full flow requires direct ORK interaction. VERIFIED (LEARNINGS-ratidefy-batch-001 L-24).
358
+ - Use `tc.createTideRequest()` → `tc.requestTideOperatorApproval()` → `tc.executeSignRequest()` — the React context's `initializeTideRequest` does not expose the approve/execute steps. VERIFIED (LEARNINGS-ratidefy-batch-001 L-24).
359
+ - `initializeTideRequest` returns a **new object** — it does NOT mutate in place. If using it, capture the return value (AP-59).
360
+ - Store `policy.toBytes()` (with signature attached), NOT `request.encode()` (AP-57).
361
+ - Admin policy is fetched from `/realms/{realm}/tide-policy-resources/admin-policy`. This endpoint has no CORS — fetch server-side and proxy to browser (see RB-008). VERIFIED (LEARNINGS-ratidefy-batch-001 L-22).
362
+
363
+ ### Step 4: Use the Signed Policy
364
+
365
+ For encryption:
366
+ ```typescript
367
+ const ciphertext = await iam.doEncrypt(
368
+ [{ data: plaintext, tags: ['mytag'] }],
369
+ signedPolicyBytes
370
+ )
371
+ ```
372
+
373
+ For decryption:
374
+ ```typescript
375
+ const plaintext = await iam.doDecrypt(ciphertext, signedPolicyBytes)
376
+ ```
377
+
378
+ The signed policy bytes must be stored (e.g., in your database or fetched via admin API) and provided on every encrypt/decrypt call. The ORKs execute the contract and verify the caller's doken against the policy rules.
379
+
380
+ ---
381
+
382
+ ## Example: Multi-Approver with Time Restriction
383
+
384
+ ```csharp
385
+ using Ork.Forseti.Sdk;
386
+
387
+ public class Contract : IAccessPolicy
388
+ {
389
+ [PolicyParam(Required = true)]
390
+ public string ApproverRole { get; set; }
391
+
392
+ [PolicyParam(Required = true)]
393
+ public string ApproverResource { get; set; }
394
+
395
+ [PolicyParam(Default = 2, Min = 1)]
396
+ public int MinApprovers { get; set; }
397
+
398
+ [PolicyParam(Required = true)]
399
+ public string ExecutorRole { get; set; }
400
+
401
+ [PolicyParam(Required = true)]
402
+ public string ExecutorResource { get; set; }
403
+
404
+ public PolicyDecision ValidateData(DataContext ctx)
405
+ {
406
+ // Only allow operations during business hours
407
+ return Decision
408
+ .RequireWeekday()
409
+ .RequireHourBetween(9, 17);
410
+ }
411
+
412
+ public PolicyDecision ValidateApprovers(ApproversContext ctx)
413
+ {
414
+ var approvers = DokenDto.WrapAll(ctx.Dokens);
415
+
416
+ return Decision
417
+ .RequireMinWithRole(approvers, MinApprovers, ApproverResource, ApproverRole);
418
+ }
419
+
420
+ public PolicyDecision ValidateExecutor(ExecutorContext ctx)
421
+ {
422
+ var executor = new DokenDto(ctx.Doken);
423
+
424
+ return Decision
425
+ .RequireNotExpired(executor)
426
+ .RequireRole(executor, ExecutorResource, ExecutorRole);
427
+ }
428
+ }
429
+ ```
430
+
431
+ ---
432
+
433
+ ## Example: Direction-Aware Encryption Contract
434
+
435
+ ```csharp
436
+ using Ork.Forseti.Sdk;
437
+
438
+ public class Contract : IAccessPolicy
439
+ {
440
+ [PolicyParam(Required = true)]
441
+ public string EncryptRole { get; set; }
442
+
443
+ [PolicyParam(Required = true)]
444
+ public string DecryptRole { get; set; }
445
+
446
+ [PolicyParam(Required = true)]
447
+ public string Resource { get; set; }
448
+
449
+ public PolicyDecision ValidateData(DataContext ctx)
450
+ {
451
+ return PolicyDecision.Allow();
452
+ }
453
+
454
+ public PolicyDecision ValidateApprovers(ApproversContext ctx)
455
+ {
456
+ return PolicyDecision.Allow();
457
+ }
458
+
459
+ public PolicyDecision ValidateExecutor(ExecutorContext ctx)
460
+ {
461
+ var executor = new DokenDto(ctx.Doken);
462
+
463
+ if (ctx.RequestId == "PolicyEnabledEncryption:1")
464
+ {
465
+ return Decision
466
+ .RequireNotExpired(executor)
467
+ .RequireRole(executor, Resource, EncryptRole);
468
+ }
469
+
470
+ if (ctx.RequestId == "PolicyEnabledDecryption:1")
471
+ {
472
+ return Decision
473
+ .RequireNotExpired(executor)
474
+ .RequireRole(executor, Resource, DecryptRole);
475
+ }
476
+
477
+ return PolicyDecision.Deny("Unknown operation");
478
+ }
479
+ }
480
+ ```
481
+
482
+ ---
483
+
484
+ ## Sandbox Restrictions
485
+
486
+ Contracts run in a sandboxed VM. The following are blocked:
487
+
488
+ - File I/O (`System.IO`)
489
+ - Network (`System.Net`)
490
+ - Threads (`System.Threading`)
491
+ - Reflection (`System.Reflection`)
492
+ - Process/diagnostics (`System.Diagnostics`)
493
+ - Console (`System.Console`)
494
+ - Non-deterministic calls (`DateTime.Now`, `Guid.NewGuid`, `Random`)
495
+
496
+ A contract using a blocked namespace compiles but fails IL vetting with `BadPolicy.ForbiddenCall` at upload time.
497
+
498
+ Available: `System`, `System.Linq`, `System.Collections.Generic`, `System.Text`, plus SDK types (`Ork.Forseti.Sdk`, `Cryptide`, `Ork.Shared`).
499
+
500
+ ---
501
+
502
+ ## Anti-Patterns
503
+
504
+ - **Wrong namespace**: Use `using Ork.Forseti.Sdk;` not `using Tide.Ork.Classes.Forseti;` (AP-56)
505
+ - **Wrong class name**: Class must be named `Contract` and implement `IAccessPolicy`
506
+ - **Wrong context properties**: Use `ctx.Dokens` (not `ctx.Approvers`) in `ValidateApprovers`, `ctx.Doken` (not `ctx.Executor`) in `ValidateExecutor`. Wrap with `DokenDto.WrapAll(ctx.Dokens)` and `new DokenDto(ctx.Doken)`. VERIFIED (LEARNINGS-ratidefy-batch-001 L-23).
507
+ - **Params as plain object**: Use `[['Role', 'admin']]` not `{ Role: 'admin' }` (AP-54)
508
+ - **Store request.encode() as policy**: Use `policy.toBytes()` not `request.encode()` (AP-57). Related: AP-55 (don't store raw VVK sig either)
509
+ - **Missing admin policy**: Policy deployment requires the realm's admin policy attached to the request. Fetch from `/realms/{realm}/tide-policy-resources/admin-policy` (AP, LEARNINGS-ratidefy-batch-001 L-22)
510
+ - **JSON to createTideRequest**: Pass `Uint8Array` from `signRequest.encode()` (AP-53)
511
+ - **Import BasicCustomRequest from wrong package**: Use `asgard-tide`, not `@tideorg/js` or `@tidecloak/js` (LEARNINGS-ratidefy-batch-001 L-11)
512
+ - **Import from @tidecloak/nextjs**: Use `@tidecloak/js` for Models, `heimdall-tide` for PolicySignRequest, `asgard-tide` for BasicCustomRequest
513
+ - **Call methods on IAMService**: Use `(IAMService as any)._tc` for `createTideRequest`, `executeSignRequest`
514
+ - **Use static IAMService.secureFetch/getToken in React apps**: Use `useTideCloak()` hook instead (AP-58)
515
+ - **Assume initializeTideRequest mutates in place**: It returns a new object — capture the return value (AP-59)
516
+ - **Check `_tide_*` roles in contract**: Those are voucher gates, not access control. Use regular roles (AP-25)
517
+ - **Client-side policy logic**: Forseti runs on ORKs, not in the browser (AP-11)
518
+ - **`PolicyDecision.Approve()`**: Does not exist. Use `PolicyDecision.Allow()`
519
+
520
+ ---
521
+
522
+ ## Error Messages
523
+
524
+ Contract errors propagate to the client as strings:
525
+
526
+ | Error | Meaning |
527
+ |-------|---------|
528
+ | `PolicyDecision.Deny("message")` | Contract explicitly denied |
529
+ | `BadPolicy.ForbiddenCall:{target}` | Contract used a blocked namespace |
530
+ | `BadPolicy.EntryTypeNotFound` | Class `Contract` not found or doesn't implement `IAccessPolicy` |
531
+ | `BadPolicy.BudgetExceeded` | Gas limit exceeded |
532
+ | `VmHost.Timeout` | Contract took too long |
533
+ | `OutOfGasException` | Gas exhausted mid-execution |