@contractspec/example.locale-jurisdiction-gate 3.7.6 → 3.7.10
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.
- package/.turbo/turbo-build.log +72 -51
- package/AGENTS.md +50 -27
- package/CHANGELOG.md +21 -0
- package/README.md +87 -42
- package/dist/browser/entities/index.js +2 -2
- package/dist/browser/entities/models.js +2 -2
- package/dist/browser/events.js +1 -1
- package/dist/browser/forms/assistant-context.form.js +213 -0
- package/dist/browser/forms/index.js +213 -0
- package/dist/browser/index.js +362 -40
- package/dist/browser/locale-jurisdiction-gate.feature.js +66 -1
- package/dist/browser/operations/assistant.js +3 -3
- package/dist/browser/operations/index.js +3 -3
- package/dist/browser/policy/assistant-gate.policy.js +62 -0
- package/dist/browser/policy/index.js +62 -1
- package/dist/browser/translations/assistant-gate.en-GB.translation.js +48 -0
- package/dist/browser/translations/assistant-gate.en-US.translation.js +50 -0
- package/dist/browser/translations/assistant-gate.fr-FR.translation.js +52 -0
- package/dist/browser/translations/index.js +148 -0
- package/dist/contracts.test.d.ts +1 -0
- package/dist/entities/index.js +2 -2
- package/dist/entities/models.js +2 -2
- package/dist/events.js +1 -1
- package/dist/forms/assistant-context.form.d.ts +22 -0
- package/dist/forms/assistant-context.form.js +214 -0
- package/dist/forms/index.d.ts +1 -0
- package/dist/forms/index.js +214 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.js +362 -40
- package/dist/locale-jurisdiction-gate.feature.js +66 -1
- package/dist/node/entities/index.js +2 -2
- package/dist/node/entities/models.js +2 -2
- package/dist/node/events.js +1 -1
- package/dist/node/forms/assistant-context.form.js +213 -0
- package/dist/node/forms/index.js +213 -0
- package/dist/node/index.js +362 -40
- package/dist/node/locale-jurisdiction-gate.feature.js +66 -1
- package/dist/node/operations/assistant.js +3 -3
- package/dist/node/operations/index.js +3 -3
- package/dist/node/policy/assistant-gate.policy.js +62 -0
- package/dist/node/policy/index.js +62 -1
- package/dist/node/translations/assistant-gate.en-GB.translation.js +48 -0
- package/dist/node/translations/assistant-gate.en-US.translation.js +50 -0
- package/dist/node/translations/assistant-gate.fr-FR.translation.js +52 -0
- package/dist/node/translations/index.js +148 -0
- package/dist/operations/assistant.js +3 -3
- package/dist/operations/index.js +3 -3
- package/dist/policy/assistant-gate.policy.d.ts +1 -0
- package/dist/policy/assistant-gate.policy.js +63 -0
- package/dist/policy/index.d.ts +2 -1
- package/dist/policy/index.js +62 -1
- package/dist/translations/assistant-gate.en-GB.translation.d.ts +1 -0
- package/dist/translations/assistant-gate.en-GB.translation.js +49 -0
- package/dist/translations/assistant-gate.en-US.translation.d.ts +1 -0
- package/dist/translations/assistant-gate.en-US.translation.js +51 -0
- package/dist/translations/assistant-gate.fr-FR.translation.d.ts +1 -0
- package/dist/translations/assistant-gate.fr-FR.translation.js +53 -0
- package/dist/translations/index.d.ts +3 -0
- package/dist/translations/index.js +149 -0
- package/package.json +105 -7
- package/src/contracts.test.ts +32 -0
- package/src/docs/locale-jurisdiction-gate.docblock.ts +21 -21
- package/src/entities/models.ts +87 -87
- package/src/events.ts +55 -55
- package/src/example.ts +28 -28
- package/src/forms/assistant-context.form.ts +112 -0
- package/src/forms/index.ts +1 -0
- package/src/handlers/demo.handlers.test.ts +46 -46
- package/src/handlers/demo.handlers.ts +133 -133
- package/src/index.ts +5 -3
- package/src/locale-jurisdiction-gate.feature.ts +40 -34
- package/src/operations/assistant.ts +82 -82
- package/src/policy/assistant-gate.policy.ts +65 -0
- package/src/policy/guard.test.ts +18 -18
- package/src/policy/guard.ts +75 -75
- package/src/policy/index.ts +2 -1
- package/src/policy/types.ts +12 -12
- package/src/translations/assistant-gate.en-GB.translation.ts +46 -0
- package/src/translations/assistant-gate.en-US.translation.ts +48 -0
- package/src/translations/assistant-gate.fr-FR.translation.ts +51 -0
- package/src/translations/index.ts +3 -0
- package/tsconfig.json +7 -15
- package/tsdown.config.js +7 -13
|
@@ -1,51 +1,51 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
2
|
+
enforceAllowedScope,
|
|
3
|
+
enforceCitations,
|
|
4
|
+
validateEnvelope,
|
|
5
5
|
} from '../policy/guard';
|
|
6
6
|
|
|
7
7
|
type AllowedScope = 'education_only' | 'generic_info' | 'escalation_required';
|
|
8
8
|
|
|
9
9
|
interface AssistantAnswerIR {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
10
|
+
locale: string;
|
|
11
|
+
jurisdiction: string;
|
|
12
|
+
allowedScope: AllowedScope;
|
|
13
|
+
sections: { heading: string; body: string }[];
|
|
14
|
+
citations: {
|
|
15
|
+
kbSnapshotId: string;
|
|
16
|
+
sourceType: string;
|
|
17
|
+
sourceId: string;
|
|
18
|
+
title?: string;
|
|
19
|
+
excerpt?: string;
|
|
20
|
+
}[];
|
|
21
|
+
disclaimers?: string[];
|
|
22
|
+
riskFlags?: string[];
|
|
23
|
+
refused?: boolean;
|
|
24
|
+
refusalReason?: string;
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
export interface DemoAssistantHandlers {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
28
|
+
answer(input: {
|
|
29
|
+
envelope: {
|
|
30
|
+
traceId: string;
|
|
31
|
+
locale: string;
|
|
32
|
+
kbSnapshotId: string;
|
|
33
|
+
allowedScope: AllowedScope;
|
|
34
|
+
regulatoryContext: { jurisdiction: string };
|
|
35
|
+
};
|
|
36
|
+
question: string;
|
|
37
|
+
}): Promise<AssistantAnswerIR>;
|
|
38
38
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
39
|
+
explainConcept(input: {
|
|
40
|
+
envelope: {
|
|
41
|
+
traceId: string;
|
|
42
|
+
locale: string;
|
|
43
|
+
kbSnapshotId: string;
|
|
44
|
+
allowedScope: AllowedScope;
|
|
45
|
+
regulatoryContext: { jurisdiction: string };
|
|
46
|
+
};
|
|
47
|
+
conceptKey: string;
|
|
48
|
+
}): Promise<AssistantAnswerIR>;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
51
|
/**
|
|
@@ -56,105 +56,105 @@ export interface DemoAssistantHandlers {
|
|
|
56
56
|
* - Enforces allowedScope (education_only blocks actionable language)
|
|
57
57
|
*/
|
|
58
58
|
export function createDemoAssistantHandlers(): DemoAssistantHandlers {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
59
|
+
async function answer(input: {
|
|
60
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
61
|
+
envelope: DemoAssistantHandlers['answer'] extends (a: infer A) => any
|
|
62
|
+
? A extends { envelope: infer E }
|
|
63
|
+
? E
|
|
64
|
+
: never
|
|
65
|
+
: never;
|
|
66
|
+
question: string;
|
|
67
|
+
}): Promise<AssistantAnswerIR> {
|
|
68
|
+
const env = validateEnvelope(input.envelope);
|
|
69
|
+
if (!env.ok) {
|
|
70
|
+
return {
|
|
71
|
+
locale: input.envelope.locale ?? 'en-US',
|
|
72
|
+
jurisdiction:
|
|
73
|
+
input.envelope.regulatoryContext?.jurisdiction ?? 'UNKNOWN',
|
|
74
|
+
allowedScope: input.envelope.allowedScope ?? 'education_only',
|
|
75
|
+
sections: [
|
|
76
|
+
{
|
|
77
|
+
heading: 'Request blocked',
|
|
78
|
+
body: env.error.message,
|
|
79
|
+
},
|
|
80
|
+
],
|
|
81
|
+
citations: [],
|
|
82
|
+
disclaimers: [
|
|
83
|
+
'This system refuses to answer without a valid envelope.',
|
|
84
|
+
],
|
|
85
|
+
riskFlags: [env.error.code],
|
|
86
|
+
refused: true,
|
|
87
|
+
refusalReason: env.error.code,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
90
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
91
|
+
const draft: AssistantAnswerIR = {
|
|
92
|
+
locale: env.value.locale,
|
|
93
|
+
jurisdiction: env.value.regulatoryContext?.jurisdiction ?? 'UNKNOWN',
|
|
94
|
+
allowedScope: env.value.allowedScope ?? 'education_only',
|
|
95
|
+
sections: [
|
|
96
|
+
{
|
|
97
|
+
heading: 'Answer (demo)',
|
|
98
|
+
body: `You asked: "${input.question}". This demo answer is derived from the KB snapshot only.`,
|
|
99
|
+
},
|
|
100
|
+
],
|
|
101
|
+
citations: [
|
|
102
|
+
{
|
|
103
|
+
kbSnapshotId: env.value.kbSnapshotId ?? 'unknown',
|
|
104
|
+
sourceType: 'ruleVersion',
|
|
105
|
+
sourceId: 'rv_demo',
|
|
106
|
+
title: 'Demo rule version',
|
|
107
|
+
excerpt: 'Demo excerpt',
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
disclaimers: ['Educational demo only.'],
|
|
111
|
+
riskFlags: [],
|
|
112
|
+
};
|
|
113
113
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
114
|
+
const scope = enforceAllowedScope(env.value.allowedScope, draft);
|
|
115
|
+
if (!scope.ok) {
|
|
116
|
+
return {
|
|
117
|
+
...draft,
|
|
118
|
+
sections: [
|
|
119
|
+
{ heading: 'Escalation required', body: scope.error.message },
|
|
120
|
+
],
|
|
121
|
+
citations: draft.citations,
|
|
122
|
+
refused: true,
|
|
123
|
+
refusalReason: scope.error.code,
|
|
124
|
+
riskFlags: [...(draft.riskFlags ?? []), scope.error.code],
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
127
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
128
|
+
const cited = enforceCitations(draft);
|
|
129
|
+
if (!cited.ok) {
|
|
130
|
+
return {
|
|
131
|
+
...draft,
|
|
132
|
+
sections: [{ heading: 'Request blocked', body: cited.error.message }],
|
|
133
|
+
citations: [],
|
|
134
|
+
refused: true,
|
|
135
|
+
refusalReason: cited.error.code,
|
|
136
|
+
riskFlags: [...(draft.riskFlags ?? []), cited.error.code],
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
139
|
|
|
140
|
-
|
|
141
|
-
|
|
140
|
+
return draft;
|
|
141
|
+
}
|
|
142
142
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
143
|
+
async function explainConcept(input: {
|
|
144
|
+
envelope: DemoAssistantHandlers['explainConcept'] extends (
|
|
145
|
+
a: infer A
|
|
146
|
+
) => any // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
147
|
+
? A extends { envelope: infer E }
|
|
148
|
+
? E
|
|
149
|
+
: never
|
|
150
|
+
: never;
|
|
151
|
+
conceptKey: string;
|
|
152
|
+
}): Promise<AssistantAnswerIR> {
|
|
153
|
+
return await answer({
|
|
154
|
+
envelope: input.envelope,
|
|
155
|
+
question: `Explain concept: ${input.conceptKey}`,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
158
|
|
|
159
|
-
|
|
159
|
+
return { answer, explainConcept };
|
|
160
160
|
}
|
package/src/index.ts
CHANGED
|
@@ -5,11 +5,13 @@
|
|
|
5
5
|
* allowedScope must be explicit, and answers must cite a KB snapshot.
|
|
6
6
|
*/
|
|
7
7
|
export * from './entities';
|
|
8
|
-
export * from './operations';
|
|
9
8
|
export * from './events';
|
|
10
|
-
export
|
|
9
|
+
export { default as example } from './example';
|
|
10
|
+
export * from './forms';
|
|
11
11
|
export * from './handlers';
|
|
12
12
|
export * from './locale-jurisdiction-gate.feature';
|
|
13
|
-
export
|
|
13
|
+
export * from './operations';
|
|
14
|
+
export * from './policy';
|
|
15
|
+
export * from './translations';
|
|
14
16
|
|
|
15
17
|
import './docs';
|
|
@@ -1,41 +1,47 @@
|
|
|
1
1
|
import { defineFeature } from '@contractspec/lib.contracts-spec';
|
|
2
|
+
import { AssistantGatePolicy } from './policy/assistant-gate.policy';
|
|
2
3
|
|
|
3
4
|
export const LocaleJurisdictionGateFeature = defineFeature({
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
5
|
+
meta: {
|
|
6
|
+
key: 'locale-jurisdiction-gate',
|
|
7
|
+
version: '1.0.0',
|
|
8
|
+
title: 'Locale + Jurisdiction Gate',
|
|
9
|
+
description:
|
|
10
|
+
'Fail-closed gating for assistant calls requiring locale/jurisdiction/snapshot/scope and citations.',
|
|
11
|
+
domain: 'knowledge',
|
|
12
|
+
owners: ['@examples'],
|
|
13
|
+
tags: ['assistant', 'policy', 'locale', 'jurisdiction', 'knowledge'],
|
|
14
|
+
stability: 'experimental',
|
|
15
|
+
},
|
|
16
|
+
operations: [
|
|
17
|
+
{ key: 'assistant.answer', version: '1.0.0' },
|
|
18
|
+
{ key: 'assistant.explainConcept', version: '1.0.0' },
|
|
19
|
+
],
|
|
20
|
+
events: [
|
|
21
|
+
{ key: 'assistant.answer.requested', version: '1.0.0' },
|
|
22
|
+
{ key: 'assistant.answer.blocked', version: '1.0.0' },
|
|
23
|
+
{ key: 'assistant.answer.delivered', version: '1.0.0' },
|
|
24
|
+
],
|
|
25
|
+
presentations: [],
|
|
26
|
+
opToPresentation: [],
|
|
27
|
+
presentationsTargets: [],
|
|
28
|
+
capabilities: {
|
|
29
|
+
requires: [{ key: 'knowledge', version: '1.0.0' }],
|
|
30
|
+
},
|
|
30
31
|
|
|
31
|
-
|
|
32
|
+
policies: [
|
|
33
|
+
{
|
|
34
|
+
key: AssistantGatePolicy.meta.key,
|
|
35
|
+
version: AssistantGatePolicy.meta.version,
|
|
36
|
+
},
|
|
37
|
+
],
|
|
32
38
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
39
|
+
knowledge: [
|
|
40
|
+
{ key: 'locale-jurisdiction-gate.knowledge.rules', version: '1.0.0' },
|
|
41
|
+
],
|
|
36
42
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
43
|
+
docs: [
|
|
44
|
+
'docs.examples.locale-jurisdiction-gate.goal',
|
|
45
|
+
'docs.examples.locale-jurisdiction-gate.reference',
|
|
46
|
+
],
|
|
41
47
|
});
|
|
@@ -1,98 +1,98 @@
|
|
|
1
1
|
import { defineCommand } from '@contractspec/lib.contracts-spec';
|
|
2
|
-
import {
|
|
2
|
+
import { defineSchemaModel, ScalarTypeEnum } from '@contractspec/lib.schema';
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
AssistantAnswerIRModel,
|
|
6
|
+
LLMCallEnvelopeModel,
|
|
7
7
|
} from '../entities/models';
|
|
8
8
|
|
|
9
9
|
const AssistantQuestionInput = defineSchemaModel({
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
10
|
+
name: 'AssistantQuestionInput',
|
|
11
|
+
description: 'Input for assistant calls with mandatory envelope.',
|
|
12
|
+
fields: {
|
|
13
|
+
envelope: { type: LLMCallEnvelopeModel, isOptional: false },
|
|
14
|
+
question: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
15
|
+
},
|
|
16
16
|
});
|
|
17
17
|
|
|
18
18
|
const AssistantConceptInput = defineSchemaModel({
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
19
|
+
name: 'AssistantConceptInput',
|
|
20
|
+
description: 'Input for explaining a concept with mandatory envelope.',
|
|
21
|
+
fields: {
|
|
22
|
+
envelope: { type: LLMCallEnvelopeModel, isOptional: false },
|
|
23
|
+
conceptKey: { type: ScalarTypeEnum.String_unsecure(), isOptional: false },
|
|
24
|
+
},
|
|
25
25
|
});
|
|
26
26
|
|
|
27
27
|
export const AssistantAnswerContract = defineCommand({
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
28
|
+
meta: {
|
|
29
|
+
key: 'assistant.answer',
|
|
30
|
+
version: '1.0.0',
|
|
31
|
+
stability: 'experimental',
|
|
32
|
+
owners: ['@examples'],
|
|
33
|
+
tags: ['assistant', 'policy', 'locale', 'jurisdiction', 'knowledge'],
|
|
34
|
+
description:
|
|
35
|
+
'Answer a user question using a KB snapshot with strict locale/jurisdiction gating.',
|
|
36
|
+
goal: 'Provide policy-safe answers that cite a KB snapshot or refuse.',
|
|
37
|
+
context:
|
|
38
|
+
'Called by UI or workflows; must fail-closed if envelope is invalid or citations are missing.',
|
|
39
|
+
},
|
|
40
|
+
io: {
|
|
41
|
+
input: AssistantQuestionInput,
|
|
42
|
+
output: AssistantAnswerIRModel,
|
|
43
|
+
errors: {
|
|
44
|
+
LOCALE_REQUIRED: {
|
|
45
|
+
description: 'Locale is required and must be supported',
|
|
46
|
+
http: 400,
|
|
47
|
+
gqlCode: 'LOCALE_REQUIRED',
|
|
48
|
+
when: 'locale is missing or unsupported',
|
|
49
|
+
},
|
|
50
|
+
JURISDICTION_REQUIRED: {
|
|
51
|
+
description: 'Jurisdiction is required',
|
|
52
|
+
http: 400,
|
|
53
|
+
gqlCode: 'JURISDICTION_REQUIRED',
|
|
54
|
+
when: 'jurisdiction is missing',
|
|
55
|
+
},
|
|
56
|
+
KB_SNAPSHOT_REQUIRED: {
|
|
57
|
+
description: 'KB snapshot id is required',
|
|
58
|
+
http: 400,
|
|
59
|
+
gqlCode: 'KB_SNAPSHOT_REQUIRED',
|
|
60
|
+
when: 'kbSnapshotId is missing',
|
|
61
|
+
},
|
|
62
|
+
CITATIONS_REQUIRED: {
|
|
63
|
+
description: 'Answers must include citations to a KB snapshot',
|
|
64
|
+
http: 422,
|
|
65
|
+
gqlCode: 'CITATIONS_REQUIRED',
|
|
66
|
+
when: 'answer has no citations',
|
|
67
|
+
},
|
|
68
|
+
SCOPE_VIOLATION: {
|
|
69
|
+
description:
|
|
70
|
+
'Answer violates allowed scope and must be refused/escalated',
|
|
71
|
+
http: 403,
|
|
72
|
+
gqlCode: 'SCOPE_VIOLATION',
|
|
73
|
+
when: 'output includes forbidden content under the given allowedScope',
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
policy: { auth: 'user' },
|
|
78
78
|
});
|
|
79
79
|
|
|
80
80
|
export const AssistantExplainConceptContract = defineCommand({
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
81
|
+
meta: {
|
|
82
|
+
key: 'assistant.explainConcept',
|
|
83
|
+
version: '1.0.0',
|
|
84
|
+
stability: 'experimental',
|
|
85
|
+
owners: ['@examples'],
|
|
86
|
+
tags: ['assistant', 'policy', 'knowledge', 'concepts'],
|
|
87
|
+
description:
|
|
88
|
+
'Explain a concept using a KB snapshot with strict locale/jurisdiction gating.',
|
|
89
|
+
goal: 'Explain concepts with citations or refuse.',
|
|
90
|
+
context: 'Same constraints as assistant.answer.',
|
|
91
|
+
},
|
|
92
|
+
io: {
|
|
93
|
+
input: AssistantConceptInput,
|
|
94
|
+
output: AssistantAnswerIRModel,
|
|
95
|
+
errors: AssistantAnswerContract.io.errors,
|
|
96
|
+
},
|
|
97
|
+
policy: { auth: 'user' },
|
|
98
98
|
});
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import {
|
|
2
|
+
OwnersEnum,
|
|
3
|
+
StabilityEnum,
|
|
4
|
+
TagsEnum,
|
|
5
|
+
} from '@contractspec/lib.contracts-spec/ownership';
|
|
6
|
+
import { definePolicy } from '@contractspec/lib.contracts-spec/policy';
|
|
7
|
+
|
|
8
|
+
export const AssistantGatePolicy = definePolicy({
|
|
9
|
+
meta: {
|
|
10
|
+
key: 'locale-jurisdiction-gate.policy.gate',
|
|
11
|
+
version: '1.0.0',
|
|
12
|
+
title: 'Assistant Locale and Jurisdiction Gate',
|
|
13
|
+
description:
|
|
14
|
+
'Requires explicit locale, jurisdiction, knowledge snapshot, and allowed scope before assistant requests may proceed.',
|
|
15
|
+
domain: 'assistant',
|
|
16
|
+
scope: 'operation',
|
|
17
|
+
owners: [OwnersEnum.PlatformFinance],
|
|
18
|
+
tags: [TagsEnum.I18n, 'assistant', 'policy', 'jurisdiction'],
|
|
19
|
+
stability: StabilityEnum.Experimental,
|
|
20
|
+
},
|
|
21
|
+
rules: [
|
|
22
|
+
{
|
|
23
|
+
effect: 'deny',
|
|
24
|
+
actions: ['assistant.answer', 'assistant.explainConcept'],
|
|
25
|
+
resource: { type: 'assistant-call' },
|
|
26
|
+
conditions: [
|
|
27
|
+
{
|
|
28
|
+
expression:
|
|
29
|
+
'!context.locale || !context.jurisdiction || !context.kbSnapshotId || !context.allowedScope',
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
reason:
|
|
33
|
+
'Assistant requests fail closed until locale, jurisdiction, kbSnapshotId, and allowedScope are explicit.',
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
effect: 'deny',
|
|
37
|
+
actions: ['assistant.answer', 'assistant.explainConcept'],
|
|
38
|
+
resource: { type: 'assistant-call' },
|
|
39
|
+
conditions: [
|
|
40
|
+
{
|
|
41
|
+
expression:
|
|
42
|
+
"!['en-US', 'en-GB', 'fr-FR'].includes(context.locale ?? '')",
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
reason: 'Only the explicitly reviewed assistant locales are permitted.',
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
effect: 'allow',
|
|
49
|
+
actions: ['assistant.answer', 'assistant.explainConcept'],
|
|
50
|
+
resource: { type: 'assistant-call' },
|
|
51
|
+
conditions: [
|
|
52
|
+
{
|
|
53
|
+
expression:
|
|
54
|
+
"['en-US', 'en-GB', 'fr-FR'].includes(context.locale ?? '') && !!context.jurisdiction && !!context.kbSnapshotId && !!context.allowedScope",
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
reason:
|
|
58
|
+
'Explicit context is present, so the request may continue to citation and scope validation.',
|
|
59
|
+
},
|
|
60
|
+
],
|
|
61
|
+
pii: {
|
|
62
|
+
fields: ['kbSnapshotId'],
|
|
63
|
+
retentionDays: 30,
|
|
64
|
+
},
|
|
65
|
+
});
|