@owlmeans/viable-common 0.0.39 → 0.0.41

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 (62) hide show
  1. package/README.md +4 -4
  2. package/agent-meta/manifest.json +2 -2
  3. package/agent-meta/skills/agent-presentation/SKILL.md +1 -1
  4. package/agent-meta/skills/viable-common/SKILL.md +331 -391
  5. package/build/areas/index.d.ts +1 -0
  6. package/build/areas/index.d.ts.map +1 -1
  7. package/build/areas/index.js +1 -0
  8. package/build/areas/index.js.map +1 -1
  9. package/build/areas/tenancy.d.ts +55 -0
  10. package/build/areas/tenancy.d.ts.map +1 -0
  11. package/build/areas/tenancy.js +33 -0
  12. package/build/areas/tenancy.js.map +1 -0
  13. package/build/blueprint/types.d.ts +19 -1
  14. package/build/blueprint/types.d.ts.map +1 -1
  15. package/build/blueprint/types.js.map +1 -1
  16. package/build/connect/consts.d.ts +3 -15
  17. package/build/connect/consts.d.ts.map +1 -1
  18. package/build/connect/consts.js +3 -15
  19. package/build/connect/consts.js.map +1 -1
  20. package/build/connect/entrypoints.d.ts +33 -69
  21. package/build/connect/entrypoints.d.ts.map +1 -1
  22. package/build/connect/entrypoints.js +5 -25
  23. package/build/connect/entrypoints.js.map +1 -1
  24. package/build/connect/references.d.ts +1 -31
  25. package/build/connect/references.d.ts.map +1 -1
  26. package/build/connect/references.js +0 -7
  27. package/build/connect/references.js.map +1 -1
  28. package/build/connect/types.d.ts +1 -25
  29. package/build/connect/types.d.ts.map +1 -1
  30. package/build/dev/consts.d.ts +15 -0
  31. package/build/dev/consts.d.ts.map +1 -1
  32. package/build/dev/consts.js +16 -0
  33. package/build/dev/consts.js.map +1 -1
  34. package/build/dev/schemas.d.ts +7 -0
  35. package/build/dev/schemas.d.ts.map +1 -1
  36. package/build/dev/schemas.js +9 -1
  37. package/build/dev/schemas.js.map +1 -1
  38. package/build/dev/types.d.ts +15 -1
  39. package/build/dev/types.d.ts.map +1 -1
  40. package/build/planning/schemas.d.ts.map +1 -1
  41. package/build/planning/schemas.js +25 -0
  42. package/build/planning/schemas.js.map +1 -1
  43. package/build/planning/types.d.ts +9 -0
  44. package/build/planning/types.d.ts.map +1 -1
  45. package/package.json +6 -6
  46. package/src/areas/index.ts +1 -0
  47. package/src/areas/tenancy.ts +69 -0
  48. package/src/blueprint/types.ts +19 -1
  49. package/src/connect/consts.ts +3 -16
  50. package/src/connect/entrypoints.ts +10 -60
  51. package/src/connect/references.ts +4 -26
  52. package/src/connect/types.ts +1 -22
  53. package/src/dev/consts.ts +16 -0
  54. package/src/dev/schemas.ts +13 -1
  55. package/src/dev/types.ts +15 -1
  56. package/src/planning/schemas.ts +25 -0
  57. package/src/planning/types.ts +9 -0
  58. package/tests/access-schema.spec.ts +37 -0
  59. package/tests/connect-convert.spec.ts +5 -11
  60. package/tests/connect-entrypoints.spec.ts +33 -14
  61. package/tests/planning.spec.ts +26 -0
  62. package/tests/tenancy.spec.ts +48 -0
@@ -0,0 +1,69 @@
1
+ import type { Blueprint } from '../blueprint/types.js'
2
+ import { ProjectArea } from './consts.js'
3
+
4
+ /**
5
+ * Whether a generated application splits its audiences into tenant ORGANIZATIONS.
6
+ *
7
+ * Two flags rather than one, because the two audiences split independently: one back office can
8
+ * serve end users who each belong to their own company (`users` only), every reseller can run its
9
+ * own staff over one shared public (`operators` only), and a B2B tool usually does both. An
10
+ * organization is an IAM-managed tenant — never a table the target keeps for itself.
11
+ *
12
+ * Both false is today's application: one organization, the project owner's, that every person acts in.
13
+ */
14
+ export interface ProjectTenancy {
15
+ /** The staff of the back office are split into tenant organizations. */
16
+ operators: boolean
17
+ /** The end users of the front office are distributed across tenant organizations. */
18
+ users: boolean
19
+ }
20
+
21
+ /**
22
+ * A tenancy decision as the project card records it (`fields.tenancy`).
23
+ *
24
+ * The flags plus where they came from. `quotes` are the requester's OWN sentences, verbatim — the
25
+ * evidence a later reader (a person reviewing the board, a run asked to reconsider) checks the
26
+ * decision against, which a paraphrase would quietly replace with the model's reading of it.
27
+ * `by` says whether a model inferred it or the owner set it; `at` is when, an ISO timestamp.
28
+ */
29
+ export interface ViableTenancyDecision extends ProjectTenancy {
30
+ quotes?: { operators?: string; users?: string }
31
+ by: 'model' | 'owner'
32
+ at: string
33
+ }
34
+
35
+ /** The longest requester sentence a tenancy decision quotes — a card refuses a longer one. */
36
+ export const TENANCY_QUOTE_MAX = 1024
37
+
38
+ /** A single-organization application — what every project is until a decision says otherwise. */
39
+ export const NO_TENANCY: ProjectTenancy = Object.freeze({ operators: false, users: false })
40
+
41
+ /**
42
+ * The tenancy of a resolved blueprint.
43
+ *
44
+ * Total, like `landingGatePreferenceOf`: an absent layer, an absent key and a value this deploy
45
+ * cannot read all answer {@link NO_TENANCY}. Only a literal `true` turns a flag on, because a flag
46
+ * read as on by mistake splits a product's people into organizations nobody asked for, while one
47
+ * read as off leaves the application exactly as it was before tenancy existed.
48
+ */
49
+ export const tenancyOf = (blueprint?: Pick<Blueprint, 'experience'> | null): ProjectTenancy => {
50
+ const tenancy = blueprint?.experience?.tenancy
51
+ const operators = tenancy?.operators === true
52
+ const users = tenancy?.users === true
53
+
54
+ return operators || users ? Object.freeze({ operators, users }) : NO_TENANCY
55
+ }
56
+
57
+ /**
58
+ * Whether an AREA's people act inside a tenant organization.
59
+ *
60
+ * Guest has no organization to act in, and admin is the project owner, who stands above every
61
+ * tenant rather than inside one — so both are never tenanted, whatever the flags say.
62
+ */
63
+ export const tenantedArea = (area: ProjectArea, tenancy: ProjectTenancy): boolean => {
64
+ switch (area) {
65
+ case ProjectArea.User: return tenancy.users === true
66
+ case ProjectArea.Operator: return tenancy.operators === true
67
+ default: return false
68
+ }
69
+ }
@@ -1,3 +1,4 @@
1
+ import type { ProjectTenancy } from '../areas/tenancy.js'
1
2
  import type { SubProject } from '../slot/consts.js'
2
3
  import type { ViableSkill } from '../skills/consts.js'
3
4
  import type { ViablePersona } from '../skills/roles.js'
@@ -21,7 +22,7 @@ import type { TopologyDescriptor } from '../topology/types.js'
21
22
  * OPTIONAL and says what its product should do for the people using it. Optional because a
22
23
  * blueprint registered before the layer existed — or a patch serialized into a run row — must keep
23
24
  * resolving: every reader asks through a helper that answers the default for an absent layer
24
- * ({@link landingGatePreferenceOf}).
25
+ * ({@link landingGatePreferenceOf}, `tenancyOf`).
25
26
  */
26
27
  export interface Blueprint {
27
28
  id: string
@@ -85,6 +86,14 @@ export enum LandingGatePreference {
85
86
  */
86
87
  export interface ExperienceLayer {
87
88
  landingGate: LandingGatePreference
89
+ /**
90
+ * Whether the product splits its audiences into tenant organizations.
91
+ *
92
+ * Written by resolution from {@link BlueprintRef.tenancy} — the project's own decision — and by
93
+ * no blueprint or case table, which cannot know it. Absent is a single-organization application;
94
+ * always read through `tenancyOf`.
95
+ */
96
+ tenancy?: ProjectTenancy
88
97
  }
89
98
 
90
99
  const LANDING_GATE_PREFERENCES = new Set<string>(Object.values(LandingGatePreference))
@@ -266,5 +275,14 @@ export interface BlueprintRef {
266
275
  * which is the only reading that cannot silently take a capability away from a live project.
267
276
  */
268
277
  case?: string
278
+ /**
279
+ * Whether the product splits its audiences into tenant organizations — the project card's
280
+ * decision, resolved into `experience.tenancy`.
281
+ *
282
+ * Flags rather than a key, because the decision is the project's own and no table could hold it;
283
+ * two booleans are still no artifact. Absent, or both false, applies nothing, so a run row of a
284
+ * project nobody decided for serializes exactly as it did before tenancy existed.
285
+ */
286
+ tenancy?: ProjectTenancy
269
287
  overrides?: BlueprintPatch
270
288
  }
@@ -260,9 +260,9 @@ export const CONNECT_INQUIRY_MAX_TEXT = 2_000
260
260
  /**
261
261
  * How long a session survives without a sign of life.
262
262
  *
263
- * A connector refreshes its presence on every socket ping, every long-poll and every submit. Ten
264
- * minutes is generous on purpose: a parent agent running one model task in a subagent can be
265
- * silent for several, and expiring a session under it would fail a run that was progressing.
263
+ * A connector refreshes its presence on every long poll and every submit. Ten minutes is generous
264
+ * on purpose: a parent agent running one model task in a subagent can be silent for several, and
265
+ * expiring a session under it would fail a run that was progressing.
266
266
  */
267
267
  export const CONNECT_SESSION_EXPIRE_MS = 600_000
268
268
 
@@ -354,10 +354,6 @@ export const CONNECT_MARKER_DIR = '.viable'
354
354
  export const CONNECT_ENV_BEGIN = '# --- viable:managed ---'
355
355
  export const CONNECT_ENV_END = '# --- /viable:managed ---'
356
356
 
357
- /** Socket events a connector session carries. */
358
- export const CONNECT_EVENT_OP = 'connect:op'
359
- export const CONNECT_EVENT_SESSION = 'connect:session'
360
-
361
357
  /** Redis resource aliases the platform registers for the relay. */
362
358
  export const CONNECT_OP_PUBSUB = 'connect-op-pubsub'
363
359
  export const CONNECT_RESULT_PUBSUB = 'connect-result-pubsub'
@@ -381,14 +377,10 @@ export const RES_CONNECT_SESSION = 'connect-session'
381
377
  */
382
378
  export const connect = Object.freeze({
383
379
  base: 'viable:manager-api:connect:base',
384
- capabilities: 'viable:manager-api:connect:capabilities',
385
380
  session: Object.freeze({
386
381
  open: 'viable:manager-api:connect:session:open',
387
382
  openDelegated: 'viable:manager-api:connect:session:open-delegated',
388
- get: 'viable:manager-api:connect:session:get',
389
- heartbeat: 'viable:manager-api:connect:session:heartbeat',
390
383
  close: 'viable:manager-api:connect:session:close',
391
- socket: 'viable:manager-api:connect:session:socket',
392
384
  }),
393
385
  op: Object.freeze({
394
386
  pull: 'viable:manager-api:connect:op:pull',
@@ -402,10 +394,6 @@ export const connect = Object.freeze({
402
394
  attach: 'viable:manager-api:connect:project:attach',
403
395
  reinit: 'viable:manager-api:connect:project:reinit',
404
396
  modify: 'viable:manager-api:connect:project:modify',
405
- settings: 'viable:manager-api:connect:project:settings',
406
- llm: 'viable:manager-api:connect:project:llm',
407
- /** The per-project converter inference mode. Separate from `llm`: it is not a paid capability. */
408
- converterLlm: 'viable:manager-api:connect:project:converter-llm',
409
397
  /**
410
398
  * The project's own branding — copyright, organization, the two legal links, the Google tag.
411
399
  * The platform credit is deliberately NOT here: hiding it is a paid capability with its own
@@ -424,7 +412,6 @@ export const connect = Object.freeze({
424
412
  check: 'viable:manager-api:connect:convert:check',
425
413
  start: 'viable:manager-api:connect:convert:start',
426
414
  proceed: 'viable:manager-api:connect:convert:proceed',
427
- cancel: 'viable:manager-api:connect:convert:cancel',
428
415
  status: 'viable:manager-api:connect:convert:status',
429
416
  purge: 'viable:manager-api:connect:convert:purge',
430
417
  }),
@@ -1,26 +1,23 @@
1
1
  import { contract, openProtocol, protocol, typed } from '@owlmeans/entrypoint'
2
- import type { EntrypointProtocol, OpenRequest, OpenValue } from '@owlmeans/entrypoint'
3
- import { route, RouteMethod, socket } from '@owlmeans/route'
4
- import { ConverterProjectLlmBodySchema } from '../convert/schemas.js'
2
+ import { route, RouteMethod } from '@owlmeans/route'
5
3
  import { connect } from './consts.js'
6
4
  import {
7
5
  ConnectAttachBodySchema, ConnectConfirmBodySchema, ConnectConvertCreateBodySchema,
8
6
  ConnectConvertProceedBodySchema, ConnectCreateBodySchema, ConnectInquiryParamsSchema,
9
7
  ConnectModifyBodySchema, ConnectOpParamsSchema, ConnectOpResultSchema,
10
8
  ConnectPipelineParamsSchema, ConnectPipelineResumeBodySchema, ConnectProjectBrandingSaveSchema,
11
- ConnectProjectIdSchema, ConnectProjectLlmBodySchema, ConnectSessionOpenSchema,
12
- ConnectSessionParamsSchema, ConnectPullQuerySchema, ConnectStoryParamsSchema, InquiryAnswerSchema,
9
+ ConnectProjectIdSchema, ConnectSessionOpenSchema, ConnectSessionParamsSchema, ConnectPullQuerySchema,
10
+ ConnectStoryParamsSchema, InquiryAnswerSchema,
13
11
  } from './schemas.js'
14
12
  import type {
15
13
  ConnectAttachBody, ConnectConfirmBody, ConnectConvertCreateBody, ConnectConvertProceedBody,
16
14
  ConnectCreateBody, ConnectInquiryAnswerBody, ConnectModifyBody,
17
15
  ConnectPipelineParams, ConnectPipelineResumeBody, ConnectProjectBranding,
18
- ConnectProjectBrandingSave, ConnectProjectLlmBody, ConnectSessionOpen,
16
+ ConnectProjectBrandingSave, ConnectSessionOpen,
19
17
  ConnectPipelineState, ConnectProjectStatus, ConnectPullQuery, ConnectSessionParams,
20
18
  ConnectStoryStatus, ConversionStatusView, ConvertCheck,
21
19
  } from './types.js'
22
20
  import type { ConnectOpResult } from './ops.js'
23
- import type { ConverterProjectLlmBody } from '../convert/types.js'
24
21
 
25
22
  /**
26
23
  * What the platform injects when it mounts the connector routes.
@@ -37,19 +34,19 @@ export interface ConnectEntrypointOptions {
37
34
  /**
38
35
  * The gate that decides whether the caller may use the local-LLM mode.
39
36
  *
40
- * Applied to the two routes that can turn it on — opening a delegated session and pinning a
41
- * project to it. Everything else is free: `cloud` is the default and the platform writes it
42
- * back by itself when a plan lapses.
37
+ * Applied to the one route that turns it on — opening a delegated session. Everything else is
38
+ * free: `cloud` is the default, and a project's own override is the platform's browser surface,
39
+ * not the connector's.
43
40
  */
44
41
  localLlm?: { alias: string, params: string[] }
45
- /** The platform-owned websocket-base protocol that carries session updates. */
46
- updateBase: EntrypointProtocol<OpenRequest, OpenValue>
47
42
  /** Path prefix; defaults to `/connect`. */
48
43
  path?: string
49
44
  }
50
45
 
51
46
  /**
52
- * Declare the connector's HTTP and socket surface.
47
+ * Declare the connector's HTTP surface — exactly the routes a connector calls: the session (long
48
+ * poll, no socket), the operation relay, projects, story status, generated files, conversion,
49
+ * inquiry answers and pipeline state.
53
50
  *
54
51
  * One immutable tree, mounted directly by the platform's entrypoint tree. Handlers are bound to these protocols
55
52
  * server-side and onto client entrypoints in the SDK — the same declarations both times, which is
@@ -69,11 +66,6 @@ export const connectProtocols = (opts: ConnectEntrypointOptions) => {
69
66
  return {
70
67
  base,
71
68
 
72
- capabilities: protocol(
73
- route(connect.capabilities, '/capabilities', { parent: base, method: RouteMethod.GET }),
74
- contract(typed()),
75
- ),
76
-
77
69
  // --- session ---------------------------------------------------------------------------
78
70
  session: {
79
71
  open: protocol(
@@ -86,29 +78,12 @@ export const connectProtocols = (opts: ConnectEntrypointOptions) => {
86
78
  }),
87
79
  contract.request({ body: typed<ConnectSessionOpen>(ConnectSessionOpenSchema) }, typed()), paid
88
80
  ),
89
- get: protocol(
90
- route(connect.session.get, '/session/:sessionId', {
91
- parent: base, method: RouteMethod.GET
92
- }),
93
- contract.request({ params: typed<ConnectSessionParams>(ConnectSessionParamsSchema) }, typed())
94
- ),
95
- heartbeat: protocol(
96
- route(connect.session.heartbeat, '/session/:sessionId/heartbeat', {
97
- parent: base, method: RouteMethod.POST
98
- }),
99
- contract.request({ params: typed<ConnectSessionParams>(ConnectSessionParamsSchema) }, typed())
100
- ),
101
81
  close: protocol(
102
82
  route(connect.session.close, '/session/:sessionId/close', {
103
83
  parent: base, method: RouteMethod.POST
104
84
  }),
105
85
  contract.request({ params: typed<ConnectSessionParams>(ConnectSessionParamsSchema) }, typed())
106
86
  ),
107
- socket: protocol(
108
- route(connect.session.socket, '/connect/:sessionId', socket({ parent: opts.updateBase })),
109
- contract.request({ params: typed<ConnectSessionParams>(ConnectSessionParamsSchema) }, typed())
110
- ),
111
-
112
87
  },
113
88
 
114
89
  // --- operations ------------------------------------------------------------------------
@@ -168,27 +143,6 @@ export const connectProtocols = (opts: ConnectEntrypointOptions) => {
168
143
  }),
169
144
  contract.request({ params: typed<{ id: string }>(ConnectProjectIdSchema), body: typed<ConnectModifyBody>(ConnectModifyBodySchema) }, typed<ConnectProjectStatus>())
170
145
  ),
171
- settings: protocol(
172
- route(connect.project.settings, '/project/:id/settings', {
173
- parent: base, method: RouteMethod.GET
174
- }),
175
- contract.request({ params: typed<{ id: string }>(ConnectProjectIdSchema) }, typed())
176
- ),
177
- llm: protocol(
178
- route(connect.project.llm, '/project/:id/llm', {
179
- parent: base, method: RouteMethod.POST
180
- }),
181
- contract.request({ params: typed<{ id: string }>(ConnectProjectIdSchema), body: typed<ConnectProjectLlmBody>(ConnectProjectLlmBodySchema) }, typed()), paid
182
- ),
183
- converterLlm: protocol(
184
- route(connect.project.converterLlm, '/project/:id/converter-llm', {
185
- parent: base, method: RouteMethod.POST,
186
- }),
187
- contract.request({
188
- params: typed<{ id: string }>(ConnectProjectIdSchema),
189
- body: typed<ConverterProjectLlmBody>(ConverterProjectLlmBodySchema),
190
- }, typed()),
191
- ),
192
146
  // Under `base` like every sibling — the guard and the ownership gate — and never under the
193
147
  // paid gate: saving branding is free, and the paid credit switch is not reachable from here.
194
148
  branding: {
@@ -252,10 +206,6 @@ export const connectProtocols = (opts: ConnectEntrypointOptions) => {
252
206
  body: typed<ConnectConvertProceedBody>(ConnectConvertProceedBodySchema),
253
207
  }, typed<ConversionStatusView>()),
254
208
  ),
255
- cancel: protocol(
256
- route(connect.convert.cancel, '/convert/:id/cancel', { parent: base, method: RouteMethod.POST }),
257
- contract.request({ params: typed<{ id: string }>(ConnectProjectIdSchema) }, typed<ConversionStatusView>()),
258
- ),
259
209
  status: protocol(
260
210
  route(connect.convert.status, '/convert/:id', { parent: base, method: RouteMethod.GET }),
261
211
  contract.request({ params: typed<{ id: string }>(ConnectProjectIdSchema) }, typed<ConversionStatusView>()),
@@ -3,13 +3,11 @@ import type { RegisteredEntrypoint, RequestShape } from '@owlmeans/entrypoint'
3
3
  import type { EntrypointReference } from '@owlmeans/context'
4
4
  import { connect } from './consts.js'
5
5
  import type { ConnectOp, ConnectOpResult } from './ops.js'
6
- import type { ConverterProjectLlmBody } from '../convert/index.js'
7
6
  import type {
8
- ConnectAttachBody, ConnectCapabilitiesView, ConnectConfirmBody, ConnectCreateBody,
9
- ConnectConvertCreateBody, ConnectConvertProceedBody, ConnectInquiryAnswerBody,
10
- ConnectModifyBody, ConnectOpSubmission, ConnectPipelineParams, ConnectPipelineResumeBody,
11
- ConnectPipelineState, ConnectProjectBranding, ConnectProjectBrandingSave, ConnectProjectLlmBody,
12
- ConnectProjectSettings, ConnectProjectStatus, ConnectProjectSummary, ConnectPullQuery,
7
+ ConnectAttachBody, ConnectConfirmBody, ConnectCreateBody, ConnectConvertCreateBody,
8
+ ConnectConvertProceedBody, ConnectInquiryAnswerBody, ConnectModifyBody, ConnectOpSubmission,
9
+ ConnectPipelineParams, ConnectPipelineResumeBody, ConnectPipelineState, ConnectProjectBranding,
10
+ ConnectProjectBrandingSave, ConnectProjectStatus, ConnectProjectSummary, ConnectPullQuery,
13
11
  ConnectSessionOpen, ConnectSessionParams, ConnectSessionView, ConnectStoryStatus,
14
12
  ConversionStatusView, ConvertCheck,
15
13
  } from './types.js'
@@ -18,12 +16,9 @@ type ConnectReference<Request extends RequestShape, Response> =
18
16
  EntrypointReference<RegisteredEntrypoint<Request, Response>>
19
17
 
20
18
  export interface ConnectReferences {
21
- capabilities: ConnectReference<{}, ConnectCapabilitiesView>
22
19
  session: {
23
20
  open: ConnectReference<{ body: ConnectSessionOpen }, ConnectSessionView>
24
21
  openDelegated: ConnectReference<{ body: ConnectSessionOpen }, ConnectSessionView>
25
- get: ConnectReference<{ params: ConnectSessionParams }, ConnectSessionView>
26
- heartbeat: ConnectReference<{ params: ConnectSessionParams }, ConnectSessionView>
27
22
  close: ConnectReference<{ params: ConnectSessionParams }, ConnectSessionView>
28
23
  }
29
24
  op: {
@@ -40,11 +35,6 @@ export interface ConnectReferences {
40
35
  attach: ConnectReference<{ body: ConnectAttachBody }, ConnectProjectStatus>
41
36
  reinit: ConnectReference<{ params: { id: string } }, ConnectProjectStatus>
42
37
  modify: ConnectReference<{ params: { id: string }, body: ConnectModifyBody }, ConnectProjectStatus>
43
- settings: ConnectReference<{ params: { id: string } }, ConnectProjectSettings>
44
- llm: ConnectReference<{ params: { id: string }, body: ConnectProjectLlmBody }, ConnectProjectSettings>
45
- converterLlm: ConnectReference<{
46
- params: { id: string }, body: ConverterProjectLlmBody
47
- }, ConnectProjectSettings>
48
38
  branding: {
49
39
  get: ConnectReference<{ params: { id: string } }, ConnectProjectBranding>
50
40
  save: ConnectReference<{
@@ -65,7 +55,6 @@ export interface ConnectReferences {
65
55
  proceed: ConnectReference<{
66
56
  params: { id: string }, body: ConnectConvertProceedBody
67
57
  }, ConversionStatusView>
68
- cancel: ConnectReference<{ params: { id: string } }, ConversionStatusView>
69
58
  status: ConnectReference<{ params: { id: string } }, ConversionStatusView>
70
59
  purge: ConnectReference<{ params: { id: string } }, ConversionStatusView>
71
60
  }
@@ -88,12 +77,9 @@ export interface ConnectReferences {
88
77
  * aliases to UI or domain code.
89
78
  */
90
79
  export const connectRef: ConnectReferences = {
91
- capabilities: entrypointRef<{}, ConnectCapabilitiesView>(connect.capabilities),
92
80
  session: {
93
81
  open: entrypointRef<{ body: ConnectSessionOpen }, ConnectSessionView>(connect.session.open),
94
82
  openDelegated: entrypointRef<{ body: ConnectSessionOpen }, ConnectSessionView>(connect.session.openDelegated),
95
- get: entrypointRef<{ params: ConnectSessionParams }, ConnectSessionView>(connect.session.get),
96
- heartbeat: entrypointRef<{ params: ConnectSessionParams }, ConnectSessionView>(connect.session.heartbeat),
97
83
  close: entrypointRef<{ params: ConnectSessionParams }, ConnectSessionView>(connect.session.close),
98
84
  },
99
85
  op: {
@@ -110,13 +96,6 @@ export const connectRef: ConnectReferences = {
110
96
  attach: entrypointRef<{ body: ConnectAttachBody }, ConnectProjectStatus>(connect.project.attach),
111
97
  reinit: entrypointRef<{ params: { id: string } }, ConnectProjectStatus>(connect.project.reinit),
112
98
  modify: entrypointRef<{ params: { id: string }, body: ConnectModifyBody }, ConnectProjectStatus>(connect.project.modify),
113
- settings: entrypointRef<{ params: { id: string } }, ConnectProjectSettings>(connect.project.settings),
114
- llm: entrypointRef<{
115
- params: { id: string }, body: ConnectProjectLlmBody
116
- }, ConnectProjectSettings>(connect.project.llm),
117
- converterLlm: entrypointRef<{
118
- params: { id: string }, body: ConverterProjectLlmBody
119
- }, ConnectProjectSettings>(connect.project.converterLlm),
120
99
  branding: {
121
100
  get: entrypointRef<{ params: { id: string } }, ConnectProjectBranding>(connect.project.branding.get),
122
101
  save: entrypointRef<{
@@ -136,7 +115,6 @@ export const connectRef: ConnectReferences = {
136
115
  proceed: entrypointRef<{
137
116
  params: { id: string }, body: ConnectConvertProceedBody
138
117
  }, ConversionStatusView>(connect.convert.proceed),
139
- cancel: entrypointRef<{ params: { id: string } }, ConversionStatusView>(connect.convert.cancel),
140
118
  status: entrypointRef<{
141
119
  params: { id: string }
142
120
  }, ConversionStatusView>(connect.convert.status),
@@ -186,8 +186,7 @@ export interface ConnectProfileSettingsView extends ConnectProfileSettings {
186
186
  * work: a conversion reads somebody else's whole repository, which is the one case where handing
187
187
  * the inference to the parent agent is the cheap default rather than the experimental option.
188
188
  *
189
- * OPTIONAL for the same reason {@link ConnectCapabilitiesView.defaults} carries `converterLlm`
190
- * optionally: the two converter fields are answered by a platform build that does not exist yet,
189
+ * OPTIONAL: the two converter fields are answered by a platform build that does not exist yet,
191
190
  * and this package is consumed by the platform through a workspace link rather than a published
192
191
  * range — a required field here is a compile error in every handler that already returns this
193
192
  * view. Tightened to required once the platform's conversion handlers fill them.
@@ -251,26 +250,6 @@ export interface ConnectProjectBranding {
251
250
  */
252
251
  export interface ConnectProjectBrandingSave extends Partial<ConnectProjectBranding> {}
253
252
 
254
- /** What the platform tells a connector about itself. */
255
- export interface ConnectCapabilitiesView {
256
- /** Tier → the platform role names that will be asked for at that tier. */
257
- tiers: Record<ModelTier, string[]>
258
- /** Whether this caller may open a local-LLM session. */
259
- localLlm: boolean
260
- /**
261
- * The default mode for a new session, resolved from the caller's settings.
262
- *
263
- * `converterLlm` is OPTIONAL: an older platform does not send it, and a client that treated its
264
- * absence as a value would pin every conversion to whatever its own default happened to be.
265
- */
266
- defaults: { target: ConnectTarget, llm: ConnectLlm, converterLlm?: ConnectLlm }
267
- limits: {
268
- pullWaitMs: number
269
- modelTaskTimeoutMs: number
270
- maxPending: number
271
- }
272
- }
273
-
274
253
  /** Attach an existing project to a session. */
275
254
  export interface ConnectAttachBody {
276
255
  projectId?: string
package/src/dev/consts.ts CHANGED
@@ -10,6 +10,22 @@ export enum AccessLevel {
10
10
  Admin = 'admin',
11
11
  }
12
12
 
13
+ /**
14
+ * Who holds a permission without anybody granting it — the default CLASS of its definition.
15
+ *
16
+ * Evaluated by the IAM when it issues claims, never copied onto rows, so a person who joins an
17
+ * organization later holds what its members hold with nothing written for them. `User` is every
18
+ * signed-in person of the application; `Member` and `Owner` are the members and the owners of the
19
+ * organization being acted in, and therefore mean anything only for an organization-bound
20
+ * permission. `None` is the ordinary permission: held only where it was granted.
21
+ */
22
+ export enum PermissionDefault {
23
+ None = 'none',
24
+ User = 'user',
25
+ Member = 'member',
26
+ Owner = 'owner',
27
+ }
28
+
13
29
  /**
14
30
  * Alias prefixes of the entrypoints a generated project declares.
15
31
  *
@@ -80,7 +80,10 @@ export const EntrypointRefListSchema: JSONSchemaType<{entries: EntrypointRef[]}>
80
80
  additionalProperties: false
81
81
  }
82
82
 
83
- export const AccessBlockSchema: JSONSchemaType<AccessBlock> = {
83
+ /** The part of an {@link AccessBlock} a model is asked for. */
84
+ type ModelAccessBlock = Omit<AccessBlock, 'defaults' | 'entityScoped'>
85
+
86
+ const ModelAccessBlockSchema: JSONSchemaType<ModelAccessBlock> = {
84
87
  type: "object",
85
88
  title: "AccessBlock",
86
89
  description: "Object with permissions array and access level",
@@ -119,6 +122,15 @@ export const AccessBlockSchema: JSONSchemaType<AccessBlock> = {
119
122
  additionalProperties: false
120
123
  }
121
124
 
125
+ /**
126
+ * The access block a model writes.
127
+ *
128
+ * It names only what a model is asked for: `defaults` and `entityScoped` are written by code and
129
+ * must never reach a prompt, which `tests/access-schema.spec.ts` pins byte for byte. A value it
130
+ * admits is still an {@link AccessBlock}, because both code-written keys are optional.
131
+ */
132
+ export const AccessBlockSchema = ModelAccessBlockSchema as unknown as JSONSchemaType<AccessBlock>
133
+
122
134
  export const AccessListSchema: JSONSchemaType<AccessList> = {
123
135
  type: "object",
124
136
  title: "AccessList",
package/src/dev/types.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { AccessLevel } from "./consts.js"
1
+ import type { AccessLevel, PermissionDefault } from "./consts.js"
2
2
 
3
3
  export interface SourceFile {
4
4
  path: string
@@ -33,6 +33,20 @@ export interface AccessBlock {
33
33
  level: AccessLevel
34
34
  /** Gate params safe for every newly signed-in user; a subset of permissions. */
35
35
  defaultEnabledPermissions?: string[]
36
+ /**
37
+ * The default class of each permission this block names, keyed by the gate param with its
38
+ * `@<routeParam>` selector stripped — the name the definition is registered under.
39
+ *
40
+ * Written by CODE, never by a model: `AccessBlockSchema` does not carry it, so a model answer
41
+ * never holds one, and what decides it is the project's tenancy and the area, not a reading of
42
+ * the story.
43
+ */
44
+ defaults?: Record<string, PermissionDefault>
45
+ /**
46
+ * Whether the permissions of this block are bound to the organization being acted in, rather than
47
+ * held everywhere. Written by code, like {@link AccessBlock.defaults}.
48
+ */
49
+ entityScoped?: boolean
36
50
  }
37
51
 
38
52
  /**
@@ -1,6 +1,7 @@
1
1
  import type { JSONSchemaType } from 'ajv'
2
2
  import { CODE_MAX } from '@owlmeans/planning'
3
3
  import { ProjectArea } from '../areas/consts.js'
4
+ import { TENANCY_QUOTE_MAX } from '../areas/tenancy.js'
4
5
  import { StoryKind } from '../ba/consts.js'
5
6
  import { ConnectLlm, ConnectTarget } from '../connect/consts.js'
6
7
  import { ProjectOriginSchema } from '../convert/schemas.js'
@@ -40,6 +41,30 @@ export const ViableProjectFieldsSchema = {
40
41
  required: ['at'],
41
42
  additionalProperties: false,
42
43
  },
44
+ // An absent `tenancy` is "never decided"; a recorded one always carries both flags, so a
45
+ // reader never has to guess what a missing flag meant.
46
+ tenancy: {
47
+ type: 'object',
48
+ nullable: true,
49
+ properties: {
50
+ operators: { type: 'boolean' },
51
+ users: { type: 'boolean' },
52
+ quotes: {
53
+ type: 'object',
54
+ nullable: true,
55
+ properties: {
56
+ operators: { type: 'string', minLength: 1, maxLength: TENANCY_QUOTE_MAX, nullable: true },
57
+ users: { type: 'string', minLength: 1, maxLength: TENANCY_QUOTE_MAX, nullable: true },
58
+ },
59
+ required: [],
60
+ additionalProperties: false,
61
+ },
62
+ by: { type: 'string', enum: ['model', 'owner'] },
63
+ at: { type: 'string', minLength: 1, maxLength: 32 },
64
+ },
65
+ required: ['operators', 'users', 'by', 'at'],
66
+ additionalProperties: false,
67
+ },
43
68
  },
44
69
  required: [],
45
70
  additionalProperties: false,
@@ -1,5 +1,6 @@
1
1
  import type { Card, Project, Specification } from '@owlmeans/planning'
2
2
  import type { ProjectArea } from '../areas/consts.js'
3
+ import type { ViableTenancyDecision } from '../areas/tenancy.js'
3
4
  import type { StoryKind } from '../ba/consts.js'
4
5
  import type { ConnectLlm, ConnectTarget } from '../connect/consts.js'
5
6
  import type { ProjectOrigin } from '../convert/types.js'
@@ -42,6 +43,14 @@ export type ViableProjectFields = {
42
43
  * "none" and no later run would ask again. `at` is when it was decided, an ISO timestamp.
43
44
  */
44
45
  landing?: ViableLandingDecision
46
+ /**
47
+ * Whether the target splits its operators and its end users into tenant organizations.
48
+ *
49
+ * ABSENT means never decided and reads as a single-organization application, exactly as every
50
+ * project before tenancy. It reaches a run through the blueprint reference (`BlueprintRef.tenancy`)
51
+ * only when a flag is on.
52
+ */
53
+ tenancy?: ViableTenancyDecision
45
54
  }
46
55
 
47
56
  /** The recorded landing-gate decision — see {@link ViableProjectFields.landing}. */
@@ -0,0 +1,37 @@
1
+ import { describe, expect, test } from 'bun:test'
2
+ import { createHash } from 'node:crypto'
3
+ import { AccessBlockSchema, AccessLevel, AccessListSchema, PermissionDefault } from '../src/index.js'
4
+ import type { AccessBlock } from '../src/index.js'
5
+
6
+ const sha256 = (value: unknown): string =>
7
+ createHash('sha256').update(JSON.stringify(value)).digest('hex')
8
+
9
+ // The model-facing schemas, byte for byte. They are rendered into the access prompts verbatim and
10
+ // handed to the model as its answer contract, so a change here changes what every model is asked —
11
+ // update these only together with the prompts that read them.
12
+ const ACCESS_BLOCK_SHA256 = '577c8406a73604128ff016a35fe918960d1316a1a24fe4737be31e41478c6ca8'
13
+ const ACCESS_LIST_SHA256 = '80b3ab836718ce69c97e9854f647f2b5ed794ec4b871e86ae94b0fad4bf68a50'
14
+
15
+ describe('viable-common - the access schema a model is asked for', () => {
16
+ test('names only the model-written keys, never the code-written defaults or binding', () => {
17
+ expect(Object.keys(AccessBlockSchema.properties ?? {})).toEqual(
18
+ ['permissions', 'level', 'defaultEnabledPermissions']
19
+ )
20
+ expect(JSON.stringify(AccessListSchema)).not.toContain('entityScoped')
21
+ expect(JSON.stringify(AccessListSchema)).not.toContain('"defaults"')
22
+ })
23
+
24
+ test('is byte-identical to the schema the access prompts were written against', () => {
25
+ expect(sha256(AccessBlockSchema)).toBe(ACCESS_BLOCK_SHA256)
26
+ expect(sha256(AccessListSchema)).toBe(ACCESS_LIST_SHA256)
27
+ })
28
+
29
+ test('an access block still carries the code-written keys as data', () => {
30
+ const block: AccessBlock = {
31
+ permissions: ['enquiry--view@enquiryId'], level: AccessLevel.Permissioned,
32
+ defaults: { 'enquiry--view': PermissionDefault.Member }, entityScoped: true,
33
+ }
34
+ expect(block.defaults).toEqual({ 'enquiry--view': 'member' })
35
+ expect(Object.values(PermissionDefault)).toEqual(['none', 'user', 'member', 'owner'])
36
+ })
37
+ })