@bizagent/dsh 0.0.0-stage → 1.0.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 (181) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/LICENSE +21 -0
  3. package/README.md +61 -2
  4. package/cordis.patch.yml +14 -0
  5. package/docs/README.md +23 -0
  6. package/docs/architecture.md +154 -0
  7. package/docs/concepts.md +192 -0
  8. package/docs/configuration.md +154 -0
  9. package/docs/quickstart.md +103 -0
  10. package/docs/quickstart.zh-CN.md +86 -0
  11. package/docs/tools.md +607 -0
  12. package/docs/user-guide.zh-CN.md +132 -0
  13. package/docs/verification.md +69 -0
  14. package/examples/README.md +64 -0
  15. package/examples/demo-runtime.mjs +71 -0
  16. package/examples/learning-lab.mjs +263 -0
  17. package/examples/scenarios.mjs +45 -0
  18. package/lib/agent-preset.d.ts +20 -0
  19. package/lib/agent-preset.js +46 -0
  20. package/lib/agent-session-address.d.ts +14 -0
  21. package/lib/agent-session-address.js +15 -0
  22. package/lib/agent-session-coordinator.d.ts +24 -0
  23. package/lib/agent-session-coordinator.js +101 -0
  24. package/lib/cli.d.ts +3 -0
  25. package/lib/cli.js +142 -0
  26. package/lib/client-types/agent-session-address.d.ts +13 -0
  27. package/lib/client-types/client/agent-mentions.d.ts +3 -0
  28. package/lib/client-types/client/api.d.ts +180 -0
  29. package/lib/client-types/client/components.d.ts +22 -0
  30. package/lib/client-types/client/controller.d.ts +23 -0
  31. package/lib/client-types/client/frame-participants.d.ts +5 -0
  32. package/lib/client-types/client/home-name.d.ts +9 -0
  33. package/lib/client-types/client/human-inbox.d.ts +8 -0
  34. package/lib/client-types/client/index.d.ts +13 -0
  35. package/lib/client-types/client/locales.d.ts +796 -0
  36. package/lib/client-types/client/markdown.d.ts +12 -0
  37. package/lib/client-types/client/project-artifact-link.d.ts +10 -0
  38. package/lib/client-types/client/project-outcome-review-inbox.d.ts +13 -0
  39. package/lib/client-types/client/projects.d.ts +11 -0
  40. package/lib/client-types/client/sample-guide.d.ts +9 -0
  41. package/lib/client-types/client/session-resource.d.ts +34 -0
  42. package/lib/client-types/client/session-surfaces.d.ts +24 -0
  43. package/lib/client-types/client/styles.d.ts +1 -0
  44. package/lib/client-types/config.d.ts +55 -0
  45. package/lib/client-types/cron.d.ts +8 -0
  46. package/lib/client-types/domain.d.ts +818 -0
  47. package/lib/client-types/frame-reference.d.ts +4 -0
  48. package/lib/client-types/home-directory.d.ts +5 -0
  49. package/lib/client-types/home-space.d.ts +49 -0
  50. package/lib/client-types/home-store.d.ts +125 -0
  51. package/lib/client-types/inbox-stalled-topic.d.ts +15 -0
  52. package/lib/client-types/inbox-wake-policy.d.ts +18 -0
  53. package/lib/client-types/known-format.d.ts +2 -0
  54. package/lib/client-types/learning-scheduler.d.ts +36 -0
  55. package/lib/client-types/learning-session-runner.d.ts +49 -0
  56. package/lib/client-types/learning-trigger.d.ts +17 -0
  57. package/lib/client-types/learning-usage.d.ts +17 -0
  58. package/lib/client-types/ledger.d.ts +190 -0
  59. package/lib/client-types/local-owner.d.ts +7 -0
  60. package/lib/client-types/message-source.d.ts +9 -0
  61. package/lib/client-types/organization-location.d.ts +19 -0
  62. package/lib/client-types/project-ledger.d.ts +182 -0
  63. package/lib/client-types/project-seat-context.d.ts +17 -0
  64. package/lib/client-types/safe-file.d.ts +23 -0
  65. package/lib/client-types/sample-organization.d.ts +9 -0
  66. package/lib/client-types/service.d.ts +596 -0
  67. package/lib/client-types/shared-knowledge.d.ts +116 -0
  68. package/lib/client-types/ui-contract.d.ts +276 -0
  69. package/lib/client-types/untrusted-text.d.ts +15 -0
  70. package/lib/client-types/work-ledger.d.ts +108 -0
  71. package/lib/client-types/work-output-eval.d.ts +15 -0
  72. package/lib/client-types/worklog-ledger.d.ts +88 -0
  73. package/lib/client-types/worklog-snapshot.d.ts +33 -0
  74. package/lib/client.js +5463 -0
  75. package/lib/config.d.ts +56 -0
  76. package/lib/config.js +115 -0
  77. package/lib/cron.d.ts +9 -0
  78. package/lib/cron.js +127 -0
  79. package/lib/domain.d.ts +819 -0
  80. package/lib/domain.js +662 -0
  81. package/lib/frame-admission.d.ts +6 -0
  82. package/lib/frame-admission.js +39 -0
  83. package/lib/frame-reference.d.ts +5 -0
  84. package/lib/frame-reference.js +12 -0
  85. package/lib/home-archive.d.ts +17 -0
  86. package/lib/home-archive.js +177 -0
  87. package/lib/home-directory.d.ts +6 -0
  88. package/lib/home-directory.js +38 -0
  89. package/lib/home-space.d.ts +50 -0
  90. package/lib/home-space.js +120 -0
  91. package/lib/home-store.d.ts +126 -0
  92. package/lib/home-store.js +837 -0
  93. package/lib/host.d.ts +22 -0
  94. package/lib/host.js +15 -0
  95. package/lib/human-inbox-transport.d.ts +23 -0
  96. package/lib/human-inbox-transport.js +105 -0
  97. package/lib/human-notification-runner.d.ts +83 -0
  98. package/lib/human-notification-runner.js +138 -0
  99. package/lib/human-notification-webhook.d.ts +18 -0
  100. package/lib/human-notification-webhook.js +102 -0
  101. package/lib/human-turn-completion.d.ts +61 -0
  102. package/lib/human-turn-completion.js +157 -0
  103. package/lib/inbox-return-notifier.d.ts +46 -0
  104. package/lib/inbox-return-notifier.js +279 -0
  105. package/lib/inbox-stalled-escalation-runner.d.ts +31 -0
  106. package/lib/inbox-stalled-escalation-runner.js +68 -0
  107. package/lib/inbox-stalled-topic.d.ts +16 -0
  108. package/lib/inbox-stalled-topic.js +44 -0
  109. package/lib/inbox-wake-policy.d.ts +19 -0
  110. package/lib/inbox-wake-policy.js +28 -0
  111. package/lib/inbox-wake-runner.d.ts +45 -0
  112. package/lib/inbox-wake-runner.js +481 -0
  113. package/lib/index.d.ts +29 -0
  114. package/lib/index.js +480 -0
  115. package/lib/known-format.d.ts +3 -0
  116. package/lib/known-format.js +8 -0
  117. package/lib/learning-scheduler.d.ts +37 -0
  118. package/lib/learning-scheduler.js +182 -0
  119. package/lib/learning-session-runner.d.ts +50 -0
  120. package/lib/learning-session-runner.js +435 -0
  121. package/lib/learning-trigger.d.ts +18 -0
  122. package/lib/learning-trigger.js +92 -0
  123. package/lib/learning-usage.d.ts +18 -0
  124. package/lib/learning-usage.js +49 -0
  125. package/lib/ledger.d.ts +191 -0
  126. package/lib/ledger.js +752 -0
  127. package/lib/local-human-host.d.ts +11 -0
  128. package/lib/local-human-host.js +191 -0
  129. package/lib/local-human-token.d.ts +9 -0
  130. package/lib/local-human-token.js +32 -0
  131. package/lib/local-owner.d.ts +8 -0
  132. package/lib/local-owner.js +26 -0
  133. package/lib/message-source.d.ts +10 -0
  134. package/lib/message-source.js +2 -0
  135. package/lib/organization-location.d.ts +20 -0
  136. package/lib/organization-location.js +80 -0
  137. package/lib/organization-workspace.d.ts +22 -0
  138. package/lib/organization-workspace.js +32 -0
  139. package/lib/project-ledger.d.ts +183 -0
  140. package/lib/project-ledger.js +189 -0
  141. package/lib/project-review-transport.d.ts +23 -0
  142. package/lib/project-review-transport.js +155 -0
  143. package/lib/project-seat-context.d.ts +18 -0
  144. package/lib/project-seat-context.js +34 -0
  145. package/lib/proposal-review-transport.d.ts +26 -0
  146. package/lib/proposal-review-transport.js +104 -0
  147. package/lib/route-guard.d.ts +31 -0
  148. package/lib/route-guard.js +80 -0
  149. package/lib/safe-file.d.ts +24 -0
  150. package/lib/safe-file.js +93 -0
  151. package/lib/sample-organization.d.ts +10 -0
  152. package/lib/sample-organization.js +46 -0
  153. package/lib/service.d.ts +597 -0
  154. package/lib/service.js +4076 -0
  155. package/lib/session-surface.d.ts +11 -0
  156. package/lib/session-surface.js +109 -0
  157. package/lib/shared-knowledge-review-transport.d.ts +28 -0
  158. package/lib/shared-knowledge-review-transport.js +85 -0
  159. package/lib/shared-knowledge.d.ts +117 -0
  160. package/lib/shared-knowledge.js +240 -0
  161. package/lib/tools.d.ts +7 -0
  162. package/lib/tools.js +955 -0
  163. package/lib/ui-contract.d.ts +277 -0
  164. package/lib/ui-contract.js +12 -0
  165. package/lib/ui-host.d.ts +24 -0
  166. package/lib/ui-host.js +471 -0
  167. package/lib/untrusted-text.d.ts +16 -0
  168. package/lib/untrusted-text.js +50 -0
  169. package/lib/work-ledger.d.ts +109 -0
  170. package/lib/work-ledger.js +107 -0
  171. package/lib/work-output-eval.d.ts +16 -0
  172. package/lib/work-output-eval.js +58 -0
  173. package/lib/work-runner.d.ts +47 -0
  174. package/lib/work-runner.js +352 -0
  175. package/lib/worklog-ledger.d.ts +89 -0
  176. package/lib/worklog-ledger.js +91 -0
  177. package/lib/worklog-snapshot.d.ts +34 -0
  178. package/lib/worklog-snapshot.js +100 -0
  179. package/lib/writer-guard.d.ts +11 -0
  180. package/lib/writer-guard.js +207 -0
  181. package/package.json +172 -4
@@ -0,0 +1,189 @@
1
+ import { defineDomain, domainTable } from '@deepseek-ai/dsh-storage-domain';
2
+ import { z } from 'zod';
3
+ import { HomeAddressSchema, stableId } from './domain.js';
4
+ const ProjectWorkSnapshotSchema = z.object({
5
+ id: z.string().min(1),
6
+ status: z.enum(['completed', 'cancelled']),
7
+ resultMessageId: z.string().min(1).optional(),
8
+ });
9
+ const ProjectClaimSchema = z.object({
10
+ id: z.string().min(1),
11
+ summary: z.string().min(1).max(200),
12
+ body: z.string().min(1).max(15000),
13
+ doneWhenAssessment: z.string().min(1).max(4000),
14
+ work: z.array(ProjectWorkSnapshotSchema),
15
+ artifacts: z.array(z.object({
16
+ path: z.string().max(500).refine(path => path.startsWith('artifacts/')
17
+ && !path.includes('\\') && !path.includes('\0')
18
+ && path.split('/').every(part => part !== '' && part !== '.' && part !== '..'), 'Project artifact path must be canonical under artifacts/'),
19
+ sha256: z.string().regex(/^[a-f0-9]{64}$/),
20
+ bytes: z.number().int().nonnegative().max(8 * 1024 * 1024),
21
+ snapshot: z.literal(true),
22
+ })).max(10).optional(),
23
+ submittedBySessionId: z.string().min(1),
24
+ submittedAt: z.string().min(1),
25
+ });
26
+ const ProjectReviewSchema = z.object({
27
+ claimId: z.string().min(1),
28
+ decision: z.enum(['accept', 'rework']),
29
+ reason: z.string().min(1).max(4000),
30
+ reviewedBySessionId: z.string().min(1).optional(),
31
+ reviewedByPrincipal: z.string().trim().min(1).max(200).optional(),
32
+ reviewedAt: z.string().min(1),
33
+ }).superRefine((review, context) => {
34
+ if ((review.reviewedBySessionId === undefined) === (review.reviewedByPrincipal === undefined)) {
35
+ context.addIssue({ code: 'custom', message: 'Project review needs exactly one Session or human principal' });
36
+ }
37
+ });
38
+ export const ProjectSeatSchema = z.object({
39
+ id: z.string().regex(/^seat-[a-f0-9]{20}$/),
40
+ address: z.string().min(1).max(200),
41
+ homeAddress: HomeAddressSchema,
42
+ responsibility: z.string().min(1).max(1000),
43
+ status: z.enum(['active', 'released']),
44
+ joinedAt: z.string().datetime(),
45
+ updatedAt: z.string().datetime(),
46
+ releasedAt: z.string().datetime().optional(),
47
+ }).superRefine((seat, context) => {
48
+ if ((seat.status === 'released') !== (seat.releasedAt !== undefined)) {
49
+ context.addIssue({ code: 'custom', path: ['releasedAt'], message: 'released Seat needs a release time' });
50
+ }
51
+ });
52
+ const ProjectSeatEventSchema = z.object({
53
+ seatAddress: z.string().min(1).max(200),
54
+ homeAddress: HomeAddressSchema,
55
+ action: z.enum(['joined', 'responsibility-updated', 'released', 'rejoined']),
56
+ responsibility: z.string().min(1).max(1000),
57
+ bySessionId: z.string().min(1),
58
+ at: z.string().datetime(),
59
+ });
60
+ export const FormalProjectSchema = z.object({
61
+ schemaVersion: z.literal(1),
62
+ id: z.string().min(1),
63
+ idempotencyKey: z.string().min(1).max(128),
64
+ lead: HomeAddressSchema,
65
+ driver: z.string().regex(/^project:project-[a-f0-9]{20}$/).optional(),
66
+ createdBySessionId: z.string().min(1),
67
+ title: z.string().min(1).max(120),
68
+ goal: z.string().min(1).max(4000),
69
+ doneWhen: z.string().min(1).max(2000),
70
+ status: z.enum(['open', 'awaiting_acceptance', 'accepted']),
71
+ revision: z.number().int().positive(),
72
+ claims: z.array(ProjectClaimSchema),
73
+ reviews: z.array(ProjectReviewSchema),
74
+ seats: z.array(ProjectSeatSchema).max(32).optional(),
75
+ seatEvents: z.array(ProjectSeatEventSchema).optional(),
76
+ currentClaimId: z.string().min(1).optional(),
77
+ acceptedClaimId: z.string().min(1).optional(),
78
+ createdAt: z.string().min(1),
79
+ updatedAt: z.string().min(1),
80
+ }).superRefine((project, context) => {
81
+ if (project.driver !== undefined && project.driver !== `project:${project.id}`) {
82
+ context.addIssue({ code: 'custom', path: ['driver'], message: 'Project driver must be its own Main address' });
83
+ }
84
+ const claimIds = new Set(project.claims.map(claim => claim.id));
85
+ const reviewedIds = new Set(project.reviews.map(review => review.claimId));
86
+ const seats = project.seats ?? [];
87
+ if (new Set(seats.map(seat => seat.homeAddress)).size !== seats.length
88
+ || new Set(seats.map(seat => seat.address)).size !== seats.length) {
89
+ context.addIssue({ code: 'custom', path: ['seats'], message: 'Project Seat Home and address must be unique' });
90
+ }
91
+ for (const [index, seat] of seats.entries()) {
92
+ if (seat.id !== stableId('seat', seat.homeAddress)
93
+ || seat.address !== `seat:${project.id}:${seat.id}`
94
+ || seat.homeAddress === project.lead || seat.homeAddress.startsWith('personal:')) {
95
+ context.addIssue({ code: 'custom', path: ['seats', index], message: 'Project Seat identity is invalid' });
96
+ }
97
+ }
98
+ for (const [index, event] of (project.seatEvents ?? []).entries()) {
99
+ if (!seats.some(seat => seat.address === event.seatAddress && seat.homeAddress === event.homeAddress)) {
100
+ context.addIssue({ code: 'custom', path: ['seatEvents', index], message: 'Project Seat event has no matching Seat' });
101
+ }
102
+ }
103
+ if (claimIds.size !== project.claims.length) {
104
+ context.addIssue({ code: 'custom', path: ['claims'], message: 'duplicate Project claim' });
105
+ }
106
+ for (const [index, claim] of project.claims.entries()) {
107
+ const paths = (claim.artifacts ?? []).map(artifact => artifact.path);
108
+ if (new Set(paths).size !== paths.length) {
109
+ context.addIssue({ code: 'custom', path: ['claims', index, 'artifacts'], message: 'duplicate Project artifact' });
110
+ }
111
+ }
112
+ if (reviewedIds.size !== project.reviews.length) {
113
+ context.addIssue({ code: 'custom', path: ['reviews'], message: 'duplicate Project review' });
114
+ }
115
+ if (project.revision !== 1 + project.claims.length + project.reviews.length
116
+ + (project.seatEvents?.length ?? 0)) {
117
+ context.addIssue({ code: 'custom', path: ['revision'], message: 'Project revision does not match its history' });
118
+ }
119
+ if (project.currentClaimId !== undefined && !claimIds.has(project.currentClaimId)) {
120
+ context.addIssue({ code: 'custom', path: ['currentClaimId'], message: 'current claim is missing' });
121
+ }
122
+ if (project.currentClaimId !== project.claims.at(-1)?.id) {
123
+ context.addIssue({ code: 'custom', path: ['currentClaimId'], message: 'current claim must be the latest version' });
124
+ }
125
+ if (project.acceptedClaimId !== undefined && !claimIds.has(project.acceptedClaimId)) {
126
+ context.addIssue({ code: 'custom', path: ['acceptedClaimId'], message: 'accepted claim is missing' });
127
+ }
128
+ if (project.status === 'awaiting_acceptance' && project.currentClaimId === undefined) {
129
+ context.addIssue({ code: 'custom', path: ['currentClaimId'], message: 'submission is required' });
130
+ }
131
+ if (project.status === 'accepted' && project.acceptedClaimId === undefined) {
132
+ context.addIssue({ code: 'custom', path: ['acceptedClaimId'], message: 'acceptance is required' });
133
+ }
134
+ if (project.status === 'accepted' && (project.acceptedClaimId !== project.currentClaimId
135
+ || project.reviews.at(-1)?.decision !== 'accept'
136
+ || project.reviews.at(-1)?.claimId !== project.currentClaimId)) {
137
+ context.addIssue({ code: 'custom', path: ['acceptedClaimId'], message: 'accepted Project requires a review of the current claim' });
138
+ }
139
+ if (project.status === 'awaiting_acceptance' && reviewedIds.has(project.currentClaimId ?? '')) {
140
+ context.addIssue({ code: 'custom', path: ['status'], message: 'pending claim is already reviewed' });
141
+ }
142
+ if (project.status === 'open' && project.claims.length > 0
143
+ && (project.reviews.at(-1)?.decision !== 'rework'
144
+ || project.reviews.at(-1)?.claimId !== project.currentClaimId)) {
145
+ context.addIssue({ code: 'custom', path: ['status'], message: 'open Project with a claim requires rework' });
146
+ }
147
+ for (const [index, review] of project.reviews.entries()) {
148
+ if (!claimIds.has(review.claimId)) {
149
+ context.addIssue({ code: 'custom', path: ['reviews', index], message: 'reviewed claim is missing' });
150
+ }
151
+ }
152
+ });
153
+ export const BIZAGENT_PROJECT_DOMAIN = defineDomain({
154
+ name: 'bizagent_project',
155
+ version: 0,
156
+ tables: { projects: domainTable(FormalProjectSchema) },
157
+ });
158
+ export class ProjectLedger {
159
+ domain;
160
+ mutationTail = Promise.resolve();
161
+ constructor(domain) {
162
+ this.domain = domain;
163
+ }
164
+ static async open(facility) {
165
+ return new ProjectLedger(await facility.open(BIZAGENT_PROJECT_DOMAIN));
166
+ }
167
+ async withWrite(update) {
168
+ const task = this.mutationTail.catch(() => undefined).then(update);
169
+ this.mutationTail = task;
170
+ return task;
171
+ }
172
+ get(id) {
173
+ const record = this.domain.table('projects').get(id);
174
+ return record === undefined ? undefined : FormalProjectSchema.parse(record);
175
+ }
176
+ list() {
177
+ return [...this.domain.table('projects').entries()]
178
+ .map(([, record]) => FormalProjectSchema.parse(record))
179
+ .sort((a, b) => b.createdAt.localeCompare(a.createdAt) || b.id.localeCompare(a.id));
180
+ }
181
+ async put(project) {
182
+ const parsed = FormalProjectSchema.parse(project);
183
+ await this.domain.table('projects').put(parsed.id, parsed);
184
+ }
185
+ async close() {
186
+ await this.domain.close();
187
+ }
188
+ }
189
+ //# sourceMappingURL=project-ledger.js.map
@@ -0,0 +1,23 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ import type { FormalProject } from './project-ledger.js';
3
+ import type { BizAgentService } from './service.js';
4
+ export interface ProjectReviewPrincipal {
5
+ userId: string;
6
+ }
7
+ export interface ProjectReviewRouteOptions {
8
+ authenticateUser: (request: IncomingMessage) => ProjectReviewPrincipal | null | Promise<ProjectReviewPrincipal | null>;
9
+ authorizeMutation: (request: IncomingMessage, principal: ProjectReviewPrincipal) => boolean | Promise<boolean>;
10
+ /** Check the current Project and its lead against this human's actual grant. */
11
+ authorizeReview: (principal: ProjectReviewPrincipal, project: Readonly<FormalProject>) => boolean | Promise<boolean>;
12
+ }
13
+ /** Optional human Project acceptance route; Agent reviews continue through their bound Sessions. */
14
+ export declare function createProjectReviewHttpHandler(service: BizAgentService, options: ProjectReviewRouteOptions): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
15
+ /** Mount on the local DSH web server behind the loopback Host / same-origin route guard. */
16
+ export declare function registerProjectReviewRoute(webServer: {
17
+ register: (route: {
18
+ kind: 'exact';
19
+ path: string;
20
+ handler: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
21
+ }) => () => void;
22
+ }, service: BizAgentService, options: ProjectReviewRouteOptions): () => void;
23
+ //# sourceMappingURL=project-review-transport.d.ts.map
@@ -0,0 +1,155 @@
1
+ import { z } from 'zod';
2
+ import { stableId } from './domain.js';
3
+ import { guardLocalRoute } from './route-guard.js';
4
+ import { BIZAGENT_PROJECT_REVIEW_API } from './ui-contract.js';
5
+ import { ProjectSnapshotStore, WorkSnapshotStore } from './worklog-snapshot.js';
6
+ const MAX_BODY_BYTES = 8 * 1024;
7
+ const Id = z.string().trim().min(1).max(200);
8
+ const Review = z.strictObject({ projectId: Id, claimId: Id,
9
+ expectedRevision: z.number().int().positive(),
10
+ decision: z.enum(['accept', 'rework']), reason: z.string().trim().min(1).max(4000) });
11
+ /** Optional human Project acceptance route; Agent reviews continue through their bound Sessions. */
12
+ export function createProjectReviewHttpHandler(service, options) {
13
+ return async (req, res) => {
14
+ res.setHeader('Cache-Control', 'no-store');
15
+ res.setHeader('Content-Type', 'application/json; charset=utf-8');
16
+ res.setHeader('X-Content-Type-Options', 'nosniff');
17
+ if (req.method !== 'GET' && req.method !== 'POST') {
18
+ res.setHeader('Allow', 'GET, POST');
19
+ reply(res, 405, failure('method-not-allowed', 'Use GET or POST for Project review.'));
20
+ return;
21
+ }
22
+ try {
23
+ const principal = await options.authenticateUser(req);
24
+ if (!principal?.userId.trim()) {
25
+ reply(res, 401, failure('authentication-required', 'Sign in to review a Project.'));
26
+ return;
27
+ }
28
+ if (req.method === 'GET') {
29
+ const query = new URL(req.url ?? '', 'http://bizagent.local').searchParams;
30
+ if (query.get('inbox') === '1') {
31
+ const offset = z.string().regex(/^(0|[1-9][0-9]{0,5})$/)
32
+ .transform(Number).parse(query.get('offset') ?? '0');
33
+ reply(res, 200, await service.listPendingProjectOutcomeReviewsAsHuman(principal.userId, (_actor, current) => options.authorizeReview(principal, current), offset));
34
+ return;
35
+ }
36
+ const projectId = Id.parse(query.get('projectId'));
37
+ const project = service.projectLedger?.get(projectId);
38
+ if (!project || !await options.authorizeReview(principal, project)) {
39
+ reply(res, 403, failure('review-forbidden', 'This Project cannot be reviewed here.'));
40
+ return;
41
+ }
42
+ if (query.has('artifactPath')) {
43
+ const claimId = Id.parse(query.get('claimId'));
44
+ const artifactPath = z.string().min(1).max(500).parse(query.get('artifactPath'));
45
+ if (query.has('workId')) {
46
+ const workId = Id.parse(query.get('workId'));
47
+ const claim = project.status === 'awaiting_acceptance'
48
+ && project.currentClaimId === claimId
49
+ ? project.claims.find(item => item.id === claimId) : undefined;
50
+ const reference = claim?.work.find(item => item.id === workId
51
+ && item.status === 'completed');
52
+ const work = service.workLedger?.get(workId);
53
+ const artifact = work?.projectId === project.id && work.status === 'completed'
54
+ && work.resultMessageId === reference?.resultMessageId
55
+ ? work.resultArtifacts?.find(item => item.path === artifactPath) : undefined;
56
+ if (!reference || !artifact || !work) {
57
+ reply(res, 404, failure('artifact-unavailable', 'Work artifact is unavailable.'));
58
+ return;
59
+ }
60
+ const bytes = await new WorkSnapshotStore(service.store.rootFor(work.assignee))
61
+ .read(artifact.sha256, artifact.bytes);
62
+ res.setHeader('Content-Type', 'application/octet-stream');
63
+ res.setHeader('Content-Disposition', 'attachment; filename="work-artifact"');
64
+ res.setHeader('Content-Length', bytes.byteLength);
65
+ res.end(bytes);
66
+ return;
67
+ }
68
+ const artifact = project.claims.find(item => item.id === claimId)?.artifacts
69
+ ?.find(item => item.path === artifactPath);
70
+ if (!artifact) {
71
+ reply(res, 404, failure('artifact-unavailable', 'Project artifact is unavailable.'));
72
+ return;
73
+ }
74
+ const bytes = await new ProjectSnapshotStore(service.store.rootFor(project.lead))
75
+ .read(artifact.sha256, artifact.bytes);
76
+ res.setHeader('Content-Type', 'application/octet-stream');
77
+ res.setHeader('Content-Disposition', 'attachment; filename="project-artifact"');
78
+ res.setHeader('Content-Length', bytes.byteLength);
79
+ res.end(bytes);
80
+ return;
81
+ }
82
+ const latestReview = project.reviews?.at(-1);
83
+ const reworkId = latestReview?.decision === 'rework'
84
+ && latestReview.claimId === project.currentClaimId
85
+ ? stableId('project-outcome-rework', `${project.id}:${latestReview.claimId}`) : undefined;
86
+ reply(res, 200, { canReview: project.status === 'awaiting_acceptance',
87
+ revision: project.revision, claimId: project.currentClaimId ?? null,
88
+ ...(reworkId === undefined ? {} : { outcomeReworkNotification: {
89
+ claimId: latestReview.claimId,
90
+ state: service.ledger.getInboxMessage(reworkId)
91
+ && service.ledger.getInboxDelivery(reworkId, project.lead)?.status === 'delivered'
92
+ ? 'delivered' : 'pending',
93
+ } }) });
94
+ return;
95
+ }
96
+ if (!await options.authorizeMutation(req, principal)) {
97
+ reply(res, 403, failure('mutation-forbidden', 'This request cannot review a Project.'));
98
+ return;
99
+ }
100
+ if (!String(req.headers['content-type'] ?? '').toLowerCase().startsWith('application/json')) {
101
+ reply(res, 415, failure('content-type', 'Send JSON to review a Project.'));
102
+ return;
103
+ }
104
+ const input = Review.parse(await readJson(req));
105
+ const result = await service.reviewProjectAsHuman(principal.userId, input, (_actor, current) => options.authorizeReview(principal, current));
106
+ reply(res, 200, result);
107
+ }
108
+ catch (error) {
109
+ if (error instanceof z.ZodError || error instanceof RequestError) {
110
+ reply(res, 400, failure('invalid-request', 'Check the Project review fields.'));
111
+ }
112
+ else if (error instanceof Error && error.message.includes('not authorized')) {
113
+ reply(res, 403, failure('review-forbidden', 'This Project cannot be reviewed here.'));
114
+ }
115
+ else if (error instanceof Error && error.message.includes('revision conflict')) {
116
+ reply(res, 409, failure('revision-conflict', 'Project changed. Refresh before reviewing.'));
117
+ }
118
+ else {
119
+ reply(res, 409, failure('review-rejected', 'Project review could not be completed. Refresh and check its current state.'));
120
+ }
121
+ }
122
+ };
123
+ }
124
+ /** Mount on the local DSH web server behind the loopback Host / same-origin route guard. */
125
+ export function registerProjectReviewRoute(webServer, service, options) {
126
+ return webServer.register({ kind: 'exact', path: BIZAGENT_PROJECT_REVIEW_API,
127
+ handler: guardLocalRoute(createProjectReviewHttpHandler(service, options)) });
128
+ }
129
+ class RequestError extends Error {
130
+ }
131
+ async function readJson(req) {
132
+ let total = 0;
133
+ const chunks = [];
134
+ for await (const chunk of req) {
135
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
136
+ total += bytes.length;
137
+ if (total > MAX_BODY_BYTES)
138
+ throw new RequestError('request body too large');
139
+ chunks.push(bytes);
140
+ }
141
+ try {
142
+ return JSON.parse(Buffer.concat(chunks).toString('utf8'));
143
+ }
144
+ catch {
145
+ throw new RequestError('invalid JSON');
146
+ }
147
+ }
148
+ function failure(code, message) {
149
+ return { error: { code, message } };
150
+ }
151
+ function reply(res, status, body) {
152
+ res.statusCode = status;
153
+ res.end(JSON.stringify(body));
154
+ }
155
+ //# sourceMappingURL=project-review-transport.js.map
@@ -0,0 +1,18 @@
1
+ import type { HomeAddress, ProjectSeatAddress } from './domain.js';
2
+ import type { FormalProject, ProjectSeat } from './project-ledger.js';
3
+ export interface ProjectSeatContext {
4
+ projectId: string;
5
+ projectRevision: number;
6
+ title: string;
7
+ goal: string;
8
+ doneWhen: string;
9
+ status: FormalProject['status'];
10
+ lead: HomeAddress;
11
+ seatAddress: ProjectSeatAddress;
12
+ homeAddress: HomeAddress;
13
+ responsibility: string;
14
+ }
15
+ export declare function projectSeatContext(project: FormalProject, seat: ProjectSeat): ProjectSeatContext;
16
+ /** Situates a Home-bound Session in one Seat without claiming a separate execution identity. */
17
+ export declare function renderProjectSeatContext(context: ProjectSeatContext): string;
18
+ //# sourceMappingURL=project-seat-context.d.ts.map
@@ -0,0 +1,34 @@
1
+ import { inlineText, untrustedBlock } from './untrusted-text.js';
2
+ export function projectSeatContext(project, seat) {
3
+ return {
4
+ projectId: project.id, projectRevision: project.revision,
5
+ title: project.title, goal: project.goal, doneWhen: project.doneWhen,
6
+ status: project.status,
7
+ lead: project.lead, seatAddress: seat.address,
8
+ homeAddress: seat.homeAddress, responsibility: seat.responsibility,
9
+ };
10
+ }
11
+ /** Situates a Home-bound Session in one Seat without claiming a separate execution identity. */
12
+ export function renderProjectSeatContext(context) {
13
+ return [
14
+ '# Project Seat context',
15
+ `Project ID: ${inlineText(context.projectId)}`,
16
+ `Project revision: ${context.projectRevision}`,
17
+ `Project status: ${inlineText(context.status)}`,
18
+ `Project title: ${inlineText(context.title)}`,
19
+ `Project lead: ${inlineText(context.lead)}`,
20
+ `Your Seat address: ${inlineText(context.seatAddress)}`,
21
+ `Your execution Home: ${inlineText(context.homeAddress)}`,
22
+ '',
23
+ untrustedBlock('Your responsibility', context.responsibility),
24
+ '',
25
+ untrustedBlock('Project goal', context.goal),
26
+ '',
27
+ untrustedBlock('Done when', context.doneWhen),
28
+ '',
29
+ 'This Session uses the execution Home above. The Seat address routes and attributes Project messages; it does not change tool authority.',
30
+ 'Read the current Project with bizagent_project_read before relying on its Work or artifacts. The revision above may become stale.',
31
+ 'For a Project reply, use bizagent_inbox_send with reply_to. A new Project message needs project_id. An ordinary reply does not complete formal Work.',
32
+ ].join('\n');
33
+ }
34
+ //# sourceMappingURL=project-seat-context.js.map
@@ -0,0 +1,26 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ import { type MemoryProposal } from './domain.js';
3
+ import type { BizAgentService } from './service.js';
4
+ export declare const BIZAGENT_PROPOSAL_REVIEW_API = "/api/bizagent/v1/proposals/decision";
5
+ export interface ProposalReviewPrincipal {
6
+ userId: string;
7
+ }
8
+ export interface ProposalReviewRouteOptions {
9
+ /** Resolve identity from a host-verified human session, never from the request body. */
10
+ authenticateHuman(request: IncomingMessage): ProposalReviewPrincipal | null | Promise<ProposalReviewPrincipal | null>;
11
+ /** Enforce host CSRF protection or a non-cookie credential. */
12
+ authorizeMutation(request: IncomingMessage, principal: ProposalReviewPrincipal): boolean | Promise<boolean>;
13
+ /** Check this proposal, action, and target Home; return a durable authority reference on success. */
14
+ authorizeProposal(request: IncomingMessage, principal: ProposalReviewPrincipal, proposal: MemoryProposal, action: 'accept' | 'reject'): string | null | Promise<string | null>;
15
+ }
16
+ /** Register explicitly in an authenticated host; the local alpha UI does not install this route. */
17
+ export declare function createProposalReviewHttpHandler(service: BizAgentService, options: ProposalReviewRouteOptions): (req: IncomingMessage, res: ServerResponse) => Promise<void>;
18
+ /** Mount on the local DSH web server behind the loopback Host / same-origin route guard. */
19
+ export declare function registerProposalReviewRoute(webServer: {
20
+ register: (route: {
21
+ kind: 'exact';
22
+ path: string;
23
+ handler: (req: IncomingMessage, res: ServerResponse) => Promise<void>;
24
+ }) => () => void;
25
+ }, service: BizAgentService, options: ProposalReviewRouteOptions): () => void;
26
+ //# sourceMappingURL=proposal-review-transport.d.ts.map
@@ -0,0 +1,104 @@
1
+ import { z } from 'zod';
2
+ import { parseHomeAddress } from './domain.js';
3
+ import { guardLocalRoute } from './route-guard.js';
4
+ import { BizAgentUiFacade } from './ui-host.js';
5
+ import { BIZAGENT_UI_API } from './ui-contract.js';
6
+ export const BIZAGENT_PROPOSAL_REVIEW_API = `${BIZAGENT_UI_API}/proposals/decision`;
7
+ const Decision = z.strictObject({
8
+ ownerAddress: z.string().min(3).max(200),
9
+ proposalId: z.string().min(1).max(200),
10
+ action: z.enum(['accept', 'reject']),
11
+ decision: z.string().trim().min(1).max(4000),
12
+ });
13
+ /** Register explicitly in an authenticated host; the local alpha UI does not install this route. */
14
+ export function createProposalReviewHttpHandler(service, options) {
15
+ const facade = new BizAgentUiFacade(service);
16
+ return async (req, res) => {
17
+ res.setHeader('Cache-Control', 'no-store');
18
+ res.setHeader('Content-Type', 'application/json; charset=utf-8');
19
+ res.setHeader('X-Content-Type-Options', 'nosniff');
20
+ if (req.method !== 'GET' && req.method !== 'POST') {
21
+ res.setHeader('Allow', 'GET, POST');
22
+ respond(res, 405, 'method-not-allowed');
23
+ return;
24
+ }
25
+ try {
26
+ const principal = await options.authenticateHuman(req);
27
+ if (!principal?.userId.trim()) {
28
+ respond(res, 401, 'authentication-required');
29
+ return;
30
+ }
31
+ if (req.method === 'GET') {
32
+ const query = new URL(req.url ?? '', 'http://bizagent.local').searchParams;
33
+ const ownerAddress = parseHomeAddress(query.get('ownerAddress') ?? '');
34
+ const proposalId = query.get('proposalId') ?? '';
35
+ const proposal = service.ledger.getProposal(proposalId);
36
+ if (!proposal || proposal.toAddress !== ownerAddress) {
37
+ respond(res, 404, 'proposal-not-found');
38
+ return;
39
+ }
40
+ const accept = !!(await options.authorizeProposal(req, principal, proposal, 'accept'))?.trim();
41
+ const reject = !!(await options.authorizeProposal(req, principal, proposal, 'reject'))?.trim();
42
+ res.statusCode = 200;
43
+ res.end(JSON.stringify({ actions: { accept, reject } }));
44
+ return;
45
+ }
46
+ if (!await options.authorizeMutation(req, principal)) {
47
+ respond(res, 403, 'mutation-forbidden');
48
+ return;
49
+ }
50
+ if (!String(req.headers['content-type'] ?? '').toLowerCase().startsWith('application/json')) {
51
+ respond(res, 415, 'content-type');
52
+ return;
53
+ }
54
+ const input = Decision.parse(await readJson(req));
55
+ const ownerAddress = parseHomeAddress(input.ownerAddress);
56
+ const proposal = service.ledger.getProposal(input.proposalId);
57
+ if (!proposal || proposal.toAddress !== ownerAddress) {
58
+ respond(res, 404, 'proposal-not-found');
59
+ return;
60
+ }
61
+ const authorityRef = await options.authorizeProposal(req, principal, proposal, input.action);
62
+ if (!authorityRef?.trim()) {
63
+ respond(res, 403, 'proposal-forbidden');
64
+ return;
65
+ }
66
+ await service.decideProposalAsHuman({ ...input, ownerAddress,
67
+ reviewerId: principal.userId, authorityRef });
68
+ res.statusCode = 200;
69
+ res.end(JSON.stringify(await facade.home(ownerAddress)));
70
+ }
71
+ catch (error) {
72
+ respond(res, error instanceof z.ZodError || error instanceof RequestError ? 400 : 409, error instanceof z.ZodError || error instanceof RequestError ? 'invalid-request' : 'proposal-conflict');
73
+ }
74
+ };
75
+ }
76
+ /** Mount on the local DSH web server behind the loopback Host / same-origin route guard. */
77
+ export function registerProposalReviewRoute(webServer, service, options) {
78
+ return webServer.register({ kind: 'exact', path: BIZAGENT_PROPOSAL_REVIEW_API,
79
+ handler: guardLocalRoute(createProposalReviewHttpHandler(service, options)) });
80
+ }
81
+ class RequestError extends Error {
82
+ }
83
+ async function readJson(req) {
84
+ const parts = [];
85
+ let bytes = 0;
86
+ for await (const chunk of req) {
87
+ const part = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
88
+ bytes += part.length;
89
+ if (bytes > 16 * 1024)
90
+ throw new RequestError('proposal decision exceeds 16 KiB');
91
+ parts.push(part);
92
+ }
93
+ try {
94
+ return JSON.parse(Buffer.concat(parts).toString('utf8'));
95
+ }
96
+ catch {
97
+ throw new RequestError('invalid JSON');
98
+ }
99
+ }
100
+ function respond(res, status, error) {
101
+ res.statusCode = status;
102
+ res.end(JSON.stringify({ error }));
103
+ }
104
+ //# sourceMappingURL=proposal-review-transport.js.map
@@ -0,0 +1,31 @@
1
+ import type { IncomingMessage, ServerResponse } from 'node:http';
2
+ import type { Context } from '@deepseek-ai/cordis';
3
+ /**
4
+ * Browser-trust fence for every HTTP route this plugin registers on the local DSH web server.
5
+ *
6
+ * It mirrors DSH's own API guard: DNS rebinding (a page on an attacker domain resolving to
7
+ * 127.0.0.1) still names the attacker domain in `Host`, so the Host must be loopback; a
8
+ * cross-site page is rejected by Fetch Metadata; and every mutation must carry an `Origin`
9
+ * whose host:port equals the `Host`. This is not an authentication layer.
10
+ */
11
+ export type RouteRejection = 'untrusted-host' | 'cross-site-request' | 'origin-rejected';
12
+ export declare function isLoopbackHostname(hostname: string): boolean;
13
+ /** Return why a request must be rejected, or undefined when it may reach the handler. */
14
+ export declare function localRouteRejection(request: Pick<IncomingMessage, 'method' | 'headers'>): RouteRejection | undefined;
15
+ /** Wrap a route handler so untrusted browser requests get a 403 before any handler code runs. */
16
+ export declare function guardLocalRoute<Req extends IncomingMessage, Res extends ServerResponse>(handler: (request: Req, response: Res) => void | Promise<void>): (request: Req, response: Res) => Promise<void>;
17
+ /** Write the standard 403 response and return true when the request fails the fence. */
18
+ export declare function rejectUntrustedRequest(request: Pick<IncomingMessage, 'method' | 'headers'>, response: Pick<ServerResponse, 'setHeader' | 'end'> & {
19
+ statusCode: number;
20
+ }): boolean;
21
+ /** DSH Web's own browser login (the cookie minted from the URL `dsh web` prints). */
22
+ export interface OperatorAuthentication {
23
+ /** 401 or 403 when the request lacks DSH's browser login; undefined when it may proceed. */
24
+ requestRejection(request: Pick<IncomingMessage, 'headers'>): 401 | 403 | undefined;
25
+ }
26
+ /**
27
+ * DSH's browser login, looked up per request. DSH 0.2 Web provides it as the `connection`
28
+ * service; hosts without it get only the loopback fence above.
29
+ */
30
+ export declare function dshOperatorAuthentication(ctx: Context): OperatorAuthentication | undefined;
31
+ //# sourceMappingURL=route-guard.d.ts.map