@openshain/core 0.4.0 → 0.4.1

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.
@@ -11,6 +11,7 @@ export declare const ConfigFileSchema: z.ZodObject<{
11
11
  en: "en";
12
12
  ja: "ja";
13
13
  }>>;
14
+ timezone: z.ZodOptional<z.ZodString>;
14
15
  }, z.core.$strict>;
15
16
  principal: z.ZodObject<{
16
17
  id: z.ZodString;
@@ -63,6 +64,7 @@ export interface Config {
63
64
  company: {
64
65
  name: string;
65
66
  language: Language;
67
+ timezone: string;
66
68
  };
67
69
  principal: {
68
70
  id: string;
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { hostTimezone, isTimezone } from "../time.js";
2
3
  const identifier = z
3
4
  .string()
4
5
  .regex(/^[a-z][a-z0-9_-]*$/, "use lowercase letters, digits, _ or -, starting with a letter");
@@ -35,6 +36,16 @@ export const ConfigFileSchema = z.strictObject({
35
36
  company: z.strictObject({
36
37
  name: z.string().min(1).max(200),
37
38
  language: z.enum(LANGUAGES).default("ja"),
39
+ // The company's own clock decides every business date, so a workspace answers the same
40
+ // whether it runs on a laptop in Tokyo or in a container set to UTC.
41
+ // No default in the schema: it would bake the machine that generated it into the published
42
+ // JSON Schema. The fallback is applied when the file is turned into a Config.
43
+ timezone: z
44
+ .string()
45
+ .min(1)
46
+ .max(100)
47
+ .refine(isTimezone, "not a timezone name, such as Asia/Tokyo")
48
+ .optional(),
38
49
  }),
39
50
  principal: z.strictObject({ id: identifier, name: z.string().min(1).max(200) }),
40
51
  profession: z.strictObject({ id: identifier, instructions: z.string().min(1).max(100_000) }),
@@ -70,7 +81,11 @@ export const ConfigFileSchema = z.strictObject({
70
81
  export function toConfig(file) {
71
82
  return {
72
83
  version: file.version,
73
- company: { name: file.company.name, language: file.company.language },
84
+ company: {
85
+ name: file.company.name,
86
+ language: file.company.language,
87
+ timezone: file.company.timezone ?? hostTimezone(),
88
+ },
74
89
  principal: { id: file.principal.id, name: file.principal.name },
75
90
  profession: { id: file.profession.id, instructions: file.profession.instructions },
76
91
  ...(file.model && {
package/dist/index.d.ts CHANGED
@@ -7,6 +7,7 @@ export { type EventId, newEventId, newWorkId, parseEventId, parseWorkId, type Wo
7
7
  export type { ModelDescription, ModelMessage, ModelProvider, ModelRequest, ModelResponse, UserPart, } from "./model/types.ts";
8
8
  export { type CallOptions, type CreateRuntimeOptions, createRuntime, createToolCaller, createToolRegistry, MAX_TOOL_TEXT_CHARS, type PendingApprovalResult, REVIEW_DIR_NAME, type Runtime, type RuntimeProviders, type ToolSummary, } from "./runtime.ts";
9
9
  export { jsonSchemas, type SchemaName } from "./schemas.ts";
10
+ export { businessDate, companyTime, hostTimezone, isTimezone } from "./time.ts";
10
11
  export { ASK_USER, RUNTIME_PROVIDER_ID } from "./tool/ask-user.ts";
11
12
  export { loadToolModule } from "./tool/load-module.ts";
12
13
  export { RESERVED_PATHS, resolveWorkspacePath } from "./tool/paths.ts";
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ export { ERROR_CODES, isOpenshainError, OpenshainError } from "./errors.js";
6
6
  export { newEventId, newWorkId, parseEventId, parseWorkId, } from "./ids.js";
7
7
  export { createRuntime, createToolCaller, createToolRegistry, MAX_TOOL_TEXT_CHARS, REVIEW_DIR_NAME, } from "./runtime.js";
8
8
  export { jsonSchemas } from "./schemas.js";
9
+ export { businessDate, companyTime, hostTimezone, isTimezone } from "./time.js";
9
10
  export { ASK_USER, RUNTIME_PROVIDER_ID } from "./tool/ask-user.js";
10
11
  export { loadToolModule } from "./tool/load-module.js";
11
12
  export { RESERVED_PATHS, resolveWorkspacePath } from "./tool/paths.js";
package/dist/runtime.js CHANGED
@@ -3,6 +3,7 @@ import { join } from "node:path";
3
3
  import { evaluate, loadAuthority, OPEN_AUTHORITY, } from "./authority/policy.js";
4
4
  import { loadConfig } from "./config/load.js";
5
5
  import { isOpenshainError, OpenshainError } from "./errors.js";
6
+ import { businessDate } from "./time.js";
6
7
  import { loadToolModule } from "./tool/load-module.js";
7
8
  import { ToolRegistry } from "./tool/registry.js";
8
9
  import { uuidv7 } from "./uuid.js";
@@ -112,7 +113,7 @@ async function callTool(input) {
112
113
  principal: config.principal.id,
113
114
  profession: config.profession.id,
114
115
  workType: (await work.current()).type,
115
- businessDate: businessDate(),
116
+ businessDate: businessDate(config.company.timezone),
116
117
  });
117
118
  if (judged.kind === "deny")
118
119
  return reject("denied", judged.reason);
@@ -264,11 +265,6 @@ function pathOf(input) {
264
265
  return segments.join("/");
265
266
  }
266
267
  /** Today's date on this machine's clock, YYYY-MM-DD. */
267
- function businessDate() {
268
- const now = new Date();
269
- const pad = (n) => String(n).padStart(2, "0");
270
- return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
271
- }
272
268
  function isRejectionCode(code) {
273
269
  return TOOL_REJECTION_CODES.includes(code);
274
270
  }
package/dist/time.d.ts ADDED
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The company's clock. Every date the runtime judges against — a decision's effective days, a
3
+ * delegation's validity — is a date in the company's timezone, not in the host's. A workspace
4
+ * carried between a laptop in Tokyo and a container in UTC has to answer the same question the
5
+ * same way, so the timezone is part of the configuration rather than the environment.
6
+ */
7
+ /** True when the name is a timezone this runtime knows (`Asia/Tokyo`, `UTC`, ...). */
8
+ export declare function isTimezone(name: string): boolean;
9
+ /** The timezone of the machine, used when the configuration names none. */
10
+ export declare function hostTimezone(): string;
11
+ /** The business date (`YYYY-MM-DD`) in the company's timezone. */
12
+ export declare function businessDate(timezone: string, at?: Date): string;
13
+ /** The moment as the company reads it: `2026-09-09T14:03:11+09:00`. */
14
+ export declare function companyTime(timezone: string, at?: Date): string;
package/dist/time.js ADDED
@@ -0,0 +1,50 @@
1
+ /**
2
+ * The company's clock. Every date the runtime judges against — a decision's effective days, a
3
+ * delegation's validity — is a date in the company's timezone, not in the host's. A workspace
4
+ * carried between a laptop in Tokyo and a container in UTC has to answer the same question the
5
+ * same way, so the timezone is part of the configuration rather than the environment.
6
+ */
7
+ /** True when the name is a timezone this runtime knows (`Asia/Tokyo`, `UTC`, ...). */
8
+ export function isTimezone(name) {
9
+ try {
10
+ new Intl.DateTimeFormat("en-US", { timeZone: name });
11
+ return true;
12
+ }
13
+ catch {
14
+ return false;
15
+ }
16
+ }
17
+ /** The timezone of the machine, used when the configuration names none. */
18
+ export function hostTimezone() {
19
+ const name = Intl.DateTimeFormat().resolvedOptions().timeZone;
20
+ return name && isTimezone(name) ? name : "UTC";
21
+ }
22
+ /** The business date (`YYYY-MM-DD`) in the company's timezone. */
23
+ export function businessDate(timezone, at = new Date()) {
24
+ // en-CA writes a date as YYYY-MM-DD.
25
+ return new Intl.DateTimeFormat("en-CA", {
26
+ timeZone: timezone,
27
+ year: "numeric",
28
+ month: "2-digit",
29
+ day: "2-digit",
30
+ }).format(at);
31
+ }
32
+ /** The moment as the company reads it: `2026-09-09T14:03:11+09:00`. */
33
+ export function companyTime(timezone, at = new Date()) {
34
+ const parts = new Intl.DateTimeFormat("en-CA", {
35
+ timeZone: timezone,
36
+ hour12: false,
37
+ year: "numeric",
38
+ month: "2-digit",
39
+ day: "2-digit",
40
+ hour: "2-digit",
41
+ minute: "2-digit",
42
+ second: "2-digit",
43
+ timeZoneName: "longOffset",
44
+ }).formatToParts(at);
45
+ const of = (type) => parts.find((part) => part.type === type)?.value ?? "";
46
+ // "GMT+09:00" for a zone with an offset, "GMT" for UTC itself.
47
+ const zone = of("timeZoneName").replace("GMT", "");
48
+ const hour = of("hour") === "24" ? "00" : of("hour");
49
+ return `${of("year")}-${of("month")}-${of("day")}T${hour}:${of("minute")}:${of("second")}${zone === "" ? "+00:00" : zone}`;
50
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openshain/core",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Contracts (provider interfaces), fundamental objects, and the work runtime",
5
5
  "keywords": [
6
6
  "openshain",
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { hostTimezone, isTimezone } from "../time.ts";
2
3
 
3
4
  const identifier = z
4
5
  .string()
@@ -43,6 +44,16 @@ export const ConfigFileSchema = z.strictObject({
43
44
  company: z.strictObject({
44
45
  name: z.string().min(1).max(200),
45
46
  language: z.enum(LANGUAGES).default("ja"),
47
+ // The company's own clock decides every business date, so a workspace answers the same
48
+ // whether it runs on a laptop in Tokyo or in a container set to UTC.
49
+ // No default in the schema: it would bake the machine that generated it into the published
50
+ // JSON Schema. The fallback is applied when the file is turned into a Config.
51
+ timezone: z
52
+ .string()
53
+ .min(1)
54
+ .max(100)
55
+ .refine(isTimezone, "not a timezone name, such as Asia/Tokyo")
56
+ .optional(),
46
57
  }),
47
58
  principal: z.strictObject({ id: identifier, name: z.string().min(1).max(200) }),
48
59
  profession: z.strictObject({ id: identifier, instructions: z.string().min(1).max(100_000) }),
@@ -96,7 +107,7 @@ export interface ModelConfig {
96
107
 
97
108
  export interface Config {
98
109
  version: 1;
99
- company: { name: string; language: Language };
110
+ company: { name: string; language: Language; timezone: string };
100
111
  principal: { id: string; name: string };
101
112
  profession: { id: string; instructions: string };
102
113
  /** The model the interactive CLI runs on. Absent when the workspace is used from other agents only. */
@@ -109,7 +120,11 @@ export interface Config {
109
120
  export function toConfig(file: ConfigFile): Config {
110
121
  return {
111
122
  version: file.version,
112
- company: { name: file.company.name, language: file.company.language },
123
+ company: {
124
+ name: file.company.name,
125
+ language: file.company.language,
126
+ timezone: file.company.timezone ?? hostTimezone(),
127
+ },
113
128
  principal: { id: file.principal.id, name: file.principal.name },
114
129
  profession: { id: file.profession.id, instructions: file.profession.instructions },
115
130
  ...(file.model && {
package/src/index.ts CHANGED
@@ -62,6 +62,7 @@ export {
62
62
  type ToolSummary,
63
63
  } from "./runtime.ts";
64
64
  export { jsonSchemas, type SchemaName } from "./schemas.ts";
65
+ export { businessDate, companyTime, hostTimezone, isTimezone } from "./time.ts";
65
66
  export { ASK_USER, RUNTIME_PROVIDER_ID } from "./tool/ask-user.ts";
66
67
  export { loadToolModule } from "./tool/load-module.ts";
67
68
  export { RESERVED_PATHS, resolveWorkspacePath } from "./tool/paths.ts";
package/src/runtime.ts CHANGED
@@ -11,6 +11,7 @@ import { loadConfig } from "./config/load.ts";
11
11
  import type { Config, ModelConfig } from "./config/schema.ts";
12
12
  import { isOpenshainError, OpenshainError } from "./errors.ts";
13
13
  import type { ModelProvider } from "./model/types.ts";
14
+ import { businessDate } from "./time.ts";
14
15
  import { loadToolModule } from "./tool/load-module.ts";
15
16
  import type { HiddenTool } from "./tool/registry.ts";
16
17
  import { type RegisteredTool, ToolRegistry } from "./tool/registry.ts";
@@ -215,7 +216,7 @@ async function callTool(input: {
215
216
  principal: config.principal.id,
216
217
  profession: config.profession.id,
217
218
  workType: (await work.current()).type,
218
- businessDate: businessDate(),
219
+ businessDate: businessDate(config.company.timezone),
219
220
  });
220
221
  if (judged.kind === "deny") return reject("denied", judged.reason);
221
222
  if (judged.kind === "approval_required" || judged.kind === "review_required") {
@@ -370,11 +371,6 @@ function pathOf(input: unknown): string | undefined {
370
371
  }
371
372
 
372
373
  /** Today's date on this machine's clock, YYYY-MM-DD. */
373
- function businessDate(): string {
374
- const now = new Date();
375
- const pad = (n: number) => String(n).padStart(2, "0");
376
- return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())}`;
377
- }
378
374
 
379
375
  function isRejectionCode(code: string): code is ToolRejectionCode {
380
376
  return (TOOL_REJECTION_CODES as readonly string[]).includes(code);
package/src/time.ts ADDED
@@ -0,0 +1,54 @@
1
+ /**
2
+ * The company's clock. Every date the runtime judges against — a decision's effective days, a
3
+ * delegation's validity — is a date in the company's timezone, not in the host's. A workspace
4
+ * carried between a laptop in Tokyo and a container in UTC has to answer the same question the
5
+ * same way, so the timezone is part of the configuration rather than the environment.
6
+ */
7
+
8
+ /** True when the name is a timezone this runtime knows (`Asia/Tokyo`, `UTC`, ...). */
9
+ export function isTimezone(name: string): boolean {
10
+ try {
11
+ new Intl.DateTimeFormat("en-US", { timeZone: name });
12
+ return true;
13
+ } catch {
14
+ return false;
15
+ }
16
+ }
17
+
18
+ /** The timezone of the machine, used when the configuration names none. */
19
+ export function hostTimezone(): string {
20
+ const name = Intl.DateTimeFormat().resolvedOptions().timeZone;
21
+ return name && isTimezone(name) ? name : "UTC";
22
+ }
23
+
24
+ /** The business date (`YYYY-MM-DD`) in the company's timezone. */
25
+ export function businessDate(timezone: string, at: Date = new Date()): string {
26
+ // en-CA writes a date as YYYY-MM-DD.
27
+ return new Intl.DateTimeFormat("en-CA", {
28
+ timeZone: timezone,
29
+ year: "numeric",
30
+ month: "2-digit",
31
+ day: "2-digit",
32
+ }).format(at);
33
+ }
34
+
35
+ /** The moment as the company reads it: `2026-09-09T14:03:11+09:00`. */
36
+ export function companyTime(timezone: string, at: Date = new Date()): string {
37
+ const parts = new Intl.DateTimeFormat("en-CA", {
38
+ timeZone: timezone,
39
+ hour12: false,
40
+ year: "numeric",
41
+ month: "2-digit",
42
+ day: "2-digit",
43
+ hour: "2-digit",
44
+ minute: "2-digit",
45
+ second: "2-digit",
46
+ timeZoneName: "longOffset",
47
+ }).formatToParts(at);
48
+ const of = (type: Intl.DateTimeFormatPartTypes) =>
49
+ parts.find((part) => part.type === type)?.value ?? "";
50
+ // "GMT+09:00" for a zone with an offset, "GMT" for UTC itself.
51
+ const zone = of("timeZoneName").replace("GMT", "");
52
+ const hour = of("hour") === "24" ? "00" : of("hour");
53
+ return `${of("year")}-${of("month")}-${of("day")}T${hour}:${of("minute")}:${of("second")}${zone === "" ? "+00:00" : zone}`;
54
+ }